diff --git a/.github/actions/build/pull-oci-artifact/action.yml b/.github/actions/build/pull-oci-artifact/action.yml new file mode 100644 index 0000000..91102a1 --- /dev/null +++ b/.github/actions/build/pull-oci-artifact/action.yml @@ -0,0 +1,38 @@ +name: "Pull OCI Artifact" +description: "Pulls an OCI artifact from a registry using ORAS" + +inputs: + image: + description: "OCI reference to pull (e.g. ghcr.io/strimzi/binaries-operators:1.4.0 or ...@sha256:abc)" + required: true + outputDir: + description: "Directory to pull the artifact files into" + required: false + default: "." + registryUser: + description: "Registry username" + required: false + default: ${{ github.actor }} + registryPassword: + description: "Registry password" + required: false + default: ${{ github.token }} + +runs: + using: "composite" + steps: + - name: Login to OCI registry + shell: bash + env: + IMAGE: ${{ inputs.image }} + REGISTRY_USER: ${{ inputs.registryUser }} + REGISTRY_PASSWORD: ${{ inputs.registryPassword }} + # The registry is the part of the image reference before the first slash + run: echo "$REGISTRY_PASSWORD" | oras login "${IMAGE%%/*}" -u "$REGISTRY_USER" --password-stdin + + - name: Pull OCI artifact + shell: bash + env: + IMAGE: ${{ inputs.image }} + OUTPUT_DIR: ${{ inputs.outputDir }} + run: oras pull "$IMAGE" -o "$OUTPUT_DIR" diff --git a/.github/actions/build/push-oci-artifact/action.yml b/.github/actions/build/push-oci-artifact/action.yml new file mode 100644 index 0000000..65472ec --- /dev/null +++ b/.github/actions/build/push-oci-artifact/action.yml @@ -0,0 +1,81 @@ +name: "Push OCI Artifact" +description: "Pushes a file to an OCI registry as an artifact using ORAS" + +inputs: + file: + description: "Path to the file to push (e.g. binaries-operators.tar or target/sova-0.1.0-dist.zip)" + required: true + mediaType: + description: "Media type of the file" + required: false + default: "application/x-tar" + name: + description: "Artifact name in the registry (e.g. binaries-operators)" + required: true + tag: + description: "Artifact tag (e.g. 0.49.0 or 0.1.0-SNAPSHOT)" + required: true + containerRegistry: + description: "OCI registry" + required: false + default: "ghcr.io" + containerOrg: + description: "Organization/namespace in the registry. Defaults to the lowercased repository owner, so forks push into their own namespace" + required: false + default: "" + registryUser: + description: "Registry username" + required: false + default: ${{ github.actor }} + registryPassword: + description: "Registry password" + required: false + default: ${{ github.token }} +outputs: + image: + description: "Full OCI reference with digest (e.g. ghcr.io/strimzi/binaries-operators@sha256:abc)" + value: ${{ steps.push.outputs.image }} + +runs: + using: "composite" + steps: + - name: Login to OCI registry + shell: bash + env: + REGISTRY: ${{ inputs.containerRegistry }} + REGISTRY_USER: ${{ inputs.registryUser }} + REGISTRY_PASSWORD: ${{ inputs.registryPassword }} + run: echo "$REGISTRY_PASSWORD" | oras login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin + + - name: Push OCI artifact + id: push + shell: bash + env: + FILE: ${{ inputs.file }} + MEDIA_TYPE: ${{ inputs.mediaType }} + NAME: ${{ inputs.name }} + TAG: ${{ inputs.tag }} + REGISTRY: ${{ inputs.containerRegistry }} + ORG: ${{ inputs.containerOrg }} + run: | + set -euo pipefail + if [ ! -f "$FILE" ]; then + echo "::error::File not found: $FILE" + exit 1 + fi + + # Registries require lowercase names, which a repository owner is not guaranteed to be + org=$(echo "${ORG:-$GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]') + repository="${REGISTRY}/${org}/${NAME}" + + # ORAS stores the given path as the layer title and recreates it on pull, + # so push from the file's directory to keep only the file name + cd "$(dirname "$FILE")" + digest=$(oras push "${repository}:${TAG}" \ + --annotation "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ + --annotation "org.opencontainers.image.revision=${GITHUB_SHA}" \ + --format go-template='{{.digest}}' \ + "$(basename "$FILE"):${MEDIA_TYPE}") + + echo "Pushed ${repository}:${TAG} (${digest})" + echo "image=${repository}@${digest}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/dependencies/install-oras/action.yml b/.github/actions/dependencies/install-oras/action.yml new file mode 100644 index 0000000..af9455a --- /dev/null +++ b/.github/actions/dependencies/install-oras/action.yml @@ -0,0 +1,16 @@ +name: "Install ORAS" +description: "Install ORAS CLI for pushing and pulling OCI artifacts" + +inputs: + version: + description: "ORAS version" + required: false + default: "1.3.6" + +runs: + using: "composite" + steps: + - name: Install ORAS + uses: oras-project/setup-oras@5e2f2482b30b0cb3e14e67f89a80c88c659a7c63 # v2.0.3 + with: + version: ${{ inputs.version }} diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index 128ddca..759a0fb 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -111,6 +111,10 @@ jobs: - name: Install Helm uses: ./.github/actions/dependencies/install-helm + # Strimzi Operator build pulls the Sova snapshot from GHCR + - name: Install ORAS + uses: ./.github/actions/dependencies/install-oras + - name: Restore Kafka binaries cache if: ${{ inputs.clusterOperatorBuild == true }} uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -423,6 +427,10 @@ jobs: - name: Install AsciiDoctor uses: ./.github/actions/dependencies/install-ascii-doctor + # Strimzi Operator release build pulls the Sova snapshot from GHCR + - name: Install ORAS + uses: ./.github/actions/dependencies/install-oras + - name: Test release-artifacts action uses: ./.github/actions/build/release-artifacts with: diff --git a/.github/workflows/test-dependencies.yml b/.github/workflows/test-dependencies.yml index 89daf21..691b21a 100644 --- a/.github/workflows/test-dependencies.yml +++ b/.github/workflows/test-dependencies.yml @@ -128,6 +128,28 @@ jobs: echo "Installed syft version: ${INSTALLED}" echo "${INSTALLED}" | grep -q "${{ matrix.version }}" + test-install-oras: + name: Test install-oras + runs-on: ubuntu-latest + strategy: + matrix: + version: + - '1.3.6' + - '1.2.3' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Run install-oras action + uses: ./.github/actions/dependencies/install-oras + with: + version: ${{ matrix.version }} + + - name: Verify ORAS version + run: | + INSTALLED=$(oras version | grep '^Version:' | awk '{print $2}') + echo "Installed ORAS version: ${INSTALLED}" + echo "${INSTALLED}" | grep -q "${{ matrix.version }}" + test-install-yq: name: Test install-yq runs-on: ubuntu-latest diff --git a/.github/workflows/test-integrations.yml b/.github/workflows/test-integrations.yml index 4748e0f..5b7bdc7 100644 --- a/.github/workflows/test-integrations.yml +++ b/.github/workflows/test-integrations.yml @@ -45,7 +45,7 @@ jobs: # Kafka Bridge - Full pipeline with containers - repo: "strimzi/strimzi-kafka-bridge" - ref: "release-1.0.x" + ref: "release-1.2.x" artifactSuffix: "kafka-bridge" architecture: "amd64" buildContainers: true @@ -90,7 +90,7 @@ jobs: # Drain Cleaner - Full pipeline with containers and Helm - repo: "strimzi/drain-cleaner" - ref: "1.6.1" + ref: "1.8.0" artifactSuffix: "drain-cleaner" architecture: "amd64" buildContainers: true @@ -165,7 +165,7 @@ jobs: # Kafka Kubernetes Config Provider - Java only (no containers) - repo: "strimzi/kafka-kubernetes-config-provider" - ref: "main" + ref: "release-1.3.x" artifactSuffix: "kafka-kubernetes-config-provider" architecture: "amd64" buildContainers: false @@ -195,7 +195,7 @@ jobs: # Test Container - Java only (no containers) - repo: "strimzi/test-container" - ref: "release-0.116.x" + ref: "release-0.118.x" artifactSuffix: "strimzi-test-container" architecture: "amd64" buildContainers: false @@ -210,7 +210,7 @@ jobs: # Test Connectors - Java only (no containers) - repo: "strimzi/test-connectors" - ref: "release-0.1.x" + ref: "release-0.2.x" artifactSuffix: "strimzi-test-connectors" architecture: "amd64" buildContainers: false @@ -222,7 +222,7 @@ jobs: imagesLocation: "none" skipTests: false checkTests: true - + uses: ./.github/workflows/reusable-test-integrations.yml with: repo: ${{ matrix.project.repo }} @@ -239,4 +239,60 @@ jobs: clusterOperatorBuild: ${{ matrix.project.clusterOperatorBuild || false }} skipTests: ${{ matrix.project.skipTests }} checkTests: ${{ matrix.project.checkTests }} - secrets: inherit \ No newline at end of file + secrets: inherit + + test-oci-artifact: + name: Test push and pull OCI artifact + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup local registry + run: .github/tests/setup-registry.sh + env: + REGISTRY_IMAGE: "registry:3" + REGISTRY_USERNAME: "strimzi" + REGISTRY_PASSWORD: "strimzi" + + - name: Install ORAS + uses: ./.github/actions/dependencies/install-oras + + # The file sits in a subdirectory to check that the pull does not recreate it + - name: Create test file + run: | + mkdir -p target + echo "Strimzi OCI artifact test ${GITHUB_SHA}" > target/test-artifact.txt + + - name: Push OCI artifact + id: push + uses: ./.github/actions/build/push-oci-artifact + with: + file: target/test-artifact.txt + mediaType: "text/plain" + name: "test-artifact" + tag: "6.6.6" + containerRegistry: "registry.strimzi:443" + containerOrg: "strimzi" + registryUser: "strimzi" + registryPassword: "strimzi" + + - name: Verify image output + env: + IMAGE: ${{ steps.push.outputs.image }} + run: | + echo "Pushed: ${IMAGE}" + [[ "$IMAGE" =~ ^registry\.strimzi:443/strimzi/test-artifact@sha256:[0-9a-f]{64}$ ]] + + - name: Pull OCI artifact by tag + uses: ./.github/actions/build/pull-oci-artifact + with: + image: "registry.strimzi:443/strimzi/test-artifact:6.6.6" + outputDir: "pulled" + registryUser: "strimzi" + registryPassword: "strimzi" + + - name: Verify pulled file + run: | + ls -R pulled + cmp target/test-artifact.txt pulled/test-artifact.txt diff --git a/README.md b/README.md index a32ffd9..1c46143 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Actions for installing tools and setting up Kubernetes clusters. | `dependencies/install-yq` | Installs yq YAML processor | `version` (v4.6.3), `architecture` (amd64) | | `dependencies/install-shellcheck` | Installs ShellCheck linter | `version` (0.11.0), `architecture` (amd64) | | `dependencies/install-syft` | Installs Syft SBOM generation tool | `version` (1.20.0), `architecture` (amd64) | +| `dependencies/install-oras` | Installs ORAS CLI for OCI artifacts | `version` (1.3.6) | | `dependencies/install-ascii-doctor` | Installs Ascii Doctor tool | `rubyVersion` (3.2) | | `dependencies/setup-kind` | Creates a Kind cluster with local registry and cloud-provider-kind | `kindVersion` (0.31.0), `controlNodes` (1), `workerNodes` (1), `cloudProviderVersion` (0.6.0) | | `dependencies/setup-minikube` | Creates a Minikube cluster with local registry | `minikubeVersion` (v1.38.0), `kubeVersion` (v1.38.0) | @@ -44,11 +45,26 @@ Actions for building, testing, and releasing Strimzi components. | `build/deploy-java` | Deploys Java artifacts to Maven Central | `projects` (required), `settingsPath` (required) | | `build/release-artifacts` | Builds release artifacts using Makefile | `releaseVersion` (required), `artifactSuffix` (required) | | `build/publish-helm-chart` | Publishes Helm Chart as OCI artifact | `releaseVersion` (required), `helmChartName` (required). Outputs: `image` (full OCI ref with digest) | +| `build/push-oci-artifact` | Pushes a file to an OCI registry (GHCR by default) via ORAS | `file`, `name`, `tag` (required), `mediaType` (application/x-tar), `containerRegistry` (ghcr.io), `containerOrg` (repo owner). Outputs: `image` (full OCI ref with digest) | +| `build/pull-oci-artifact` | Pulls an OCI artifact from a registry via ORAS | `image` (required), `outputDir` (.) | > [!IMPORTANT] > Build actions do **not** install their own dependencies (Java, yq, Helm, Docker, Shellcheck, Syft, etc.). > Callers must install the required dependencies using the appropriate dependency actions **before** invoking a build action. +> [!NOTE] +> `push-oci-artifact` and `pull-oci-artifact` share build outputs between workflows and repositories through GHCR, which keeps them longer than workflow artifacts. +> The pushing job needs `packages: write` permission. A new GHCR package is private by default, so an org admin has to make it public once before other repositories can pull it. +> +> ```yaml +> - uses: strimzi/github-actions/.github/actions/dependencies/install-oras@ +> - uses: strimzi/github-actions/.github/actions/build/push-oci-artifact@ +> with: +> file: binaries-operators.tar +> name: binaries-operators +> tag: ${{ env.TAG }} +> ``` + > [!IMPORTANT] > The `build-binaries` action supports an `clusterOperatorBuild` input (default `false`) that enables Strimzi Kafka Operator specific build steps — Helm chart generation, CRD distribution, dashboard setup, documentation checks, and uncommitted changes verification. > Other repositories should leave this disabled.