From 1a2b78cc6edb41ae93533a38625f8dd41bf39487 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Tue, 23 Jun 2026 17:27:33 +0200 Subject: [PATCH 1/8] feat: multi-arch operator, bundle, and catalog image builds Replace the single-runner amd64-only build with a matrix strategy that builds on both ubuntu-24.04 (amd64) and ubuntu-24.04-arm (arm64), then merges per-arch images into multi-arch manifest lists. This follows the same proven pattern used by the hub repo's next-build-image.yaml workflow. The operator Dockerfile already supports TARGETOS/TARGETARCH and bundle images use FROM scratch, so no other changes are needed. All three images (operator, operator-bundle, operator-catalog) are now built and published as multi-arch manifest lists. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 199 +++++++++++++++++--- 1 file changed, 172 insertions(+), 27 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index fe4bebf22..230d5507f 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -3,11 +3,12 @@ name: Build and push operator, bundle, and catalog images on: push: - branches: + branches: - main - rhdh-1.[0-9]+ - 1.[0-9]+.x - release-1.[0-9]+ + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,9 +18,15 @@ env: REGISTRY: ${{ vars.REGISTRY }} jobs: - next-build: - name: Next build - runs-on: ubuntu-latest + build: + name: Build (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + - ubuntu-24.04-arm + runs-on: ${{ matrix.os }} permissions: contents: read packages: write @@ -51,7 +58,7 @@ jobs: **/*.md **/*.adoc .rhdh/** - tests/** + tests/** - name: List all changed files (for troubleshooting) env: @@ -61,24 +68,42 @@ jobs: echo "$file was changed" done - - name: Get the last commit short SHA - # run this stage only if there are changes that match the includes and not the excludes + - name: Prepare if: steps.changed-files.outputs.any_changed == 'true' + env: + MATRIX_OS: ${{ matrix.os }} run: | SHORT_SHA=$(git rev-parse --short HEAD) echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV - BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") # 0.0.1 + BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV + if [ "$MATRIX_OS" == "ubuntu-24.04" ]; then + platform="linux/amd64" + elif [ "$MATRIX_OS" == "ubuntu-24.04-arm" ]; then + platform="linux/arm64" + else + echo "Unknown runner OS: $MATRIX_OS" + exit 1 + fi + echo "PLATFORM=$platform" >> $GITHUB_ENV + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + echo "PLATFORM_ARCH=${platform#*/}" >> $GITHUB_ENV + + latestNext="next" + # for main branch, use next tags; for 1.y branches, use :latest tags + if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then + latestNext="latest" + fi + echo "LATEST_NEXT=$latestNext" >> $GITHUB_ENV + - name: Setup Go - # run this stage only if there are changes that match the includes and not the excludes if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version-file: 'go.mod' - - name: Login to registry (${{env.REGISTRY}}) - # run this stage only if there are changes that match the includes and not the excludes + - name: Login to registry (${{ env.REGISTRY }}) if: steps.changed-files.outputs.any_changed == 'true' uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: @@ -86,20 +111,12 @@ jobs: username: ${{ vars.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - - name: Build and push operator, bundle, and catalog images - # run this stage only if there are changes that match the includes and not the excludes + - name: Build and push per-arch operator, bundle, and catalog images if: steps.changed-files.outputs.any_changed == 'true' run: | - # install skopeo, podman sudo apt-get -y update; sudo apt-get -y install skopeo podman export CONTAINER_TOOL=podman - latestNext="next" - # for main branch, use next tags; for 1.y branches, use :latest tags - if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then - latestNext="latest" - fi - export VERSION=${{ env.BASE_VERSION }} export REGISTRY_WITH_ORG=${{ env.REGISTRY }}/${{ env.REGISTRY_ORG }} export OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} @@ -107,17 +124,145 @@ jobs: set -ex - # build 4 container images with a 14d expiry - CONTAINER_TOOL=${CONTAINER_TOOL} VERSION=${VERSION} make release-build + # Build all 3 images for this architecture + CONTAINER_TOOL=${CONTAINER_TOOL} VERSION=${VERSION} PLATFORM=${PLATFORM} make release-build - # now copy images from local cache to quay, using 0.0.1-next-f00cafe, 0.0.1-next, and next tags + # Push per-arch tagged images for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do - podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${VERSION} - skopeo --insecure-policy copy --all docker://${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }} - skopeo --insecure-policy copy --all docker://${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${latestNext} + podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${PLATFORM_ARCH} + podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${PLATFORM_ARCH} + + podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }}-${PLATFORM_ARCH} + podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }}-${PLATFORM_ARCH} + + podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${PLATFORM_ARCH} + podman push -q ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${PLATFORM_ARCH} done env: REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }} - # to avoid throttling on RHD org, use GH token GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }} + + - name: Upload build marker + if: steps.changed-files.outputs.any_changed == 'true' + run: | + mkdir -p /tmp/build-markers + echo "${{ env.PLATFORM_ARCH }}" > /tmp/build-markers/${{ env.PLATFORM_ARCH }} + + - name: Upload build marker artifact + if: steps.changed-files.outputs.any_changed == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: build-marker-${{ env.PLATFORM_PAIR }} + path: /tmp/build-markers/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Create multi-arch manifests + runs-on: ubuntu-latest + needs: + - build + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Download build markers + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: /tmp/build-markers + pattern: build-marker-* + merge-multiple: true + + - name: Prepare + run: | + SHORT_SHA=$(git rev-parse --short HEAD) + echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV + BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") + echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV + + latestNext="next" + if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then + latestNext="latest" + fi + echo "LATEST_NEXT=$latestNext" >> $GITHUB_ENV + + # Check which architectures actually built + echo "Build markers found:" + ls -la /tmp/build-markers/ || echo "No markers found" + + - name: Login to registry (${{ env.REGISTRY }}) + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ vars.QUAY_USERNAME }} + password: ${{ secrets.QUAY_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Create manifest lists and push + run: | + export REGISTRY_WITH_ORG=${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }} + export OPERATOR_IMAGE_NAME=${{ vars.OPERATOR_IMAGE_NAME }} + OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} + + set -ex + + for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do + echo "=== Creating manifest list for ${image} ===" + + # Create manifest list for version tag + docker buildx imagetools create \ + --annotation "quay.expires-after=14d" \ + -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION} \ + ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ + ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 + + # Create manifest list for version-sha tag + docker buildx imagetools create \ + --annotation "quay.expires-after=14d" \ + -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA} \ + ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ + ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 + + # Create manifest list for latestNext tag (next or latest) + docker buildx imagetools create \ + --annotation "quay.expires-after=14d" \ + -t ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT} \ + ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-amd64 \ + ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-arm64 + + echo "=== Inspecting manifest for ${image}:${LATEST_NEXT} ===" + docker buildx imagetools inspect ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT} + done + env: + REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} + OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }} + + - name: Cleanup per-arch tags + if: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} + run: | + export OPERATOR_IMAGE_NAME=${{ vars.OPERATOR_IMAGE_NAME }} + OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} + NAMESPACE=${{ vars.REGISTRY_ORG }} + + for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do + for arch in amd64 arm64; do + for tag in ${BASE_VERSION}-${arch} ${BASE_VERSION}-${SHORT_SHA}-${arch} ${LATEST_NEXT}-${arch}; do + echo "Deleting per-arch tag: ${image}:${tag}" + curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer ${{ secrets.QUAY_OAUTH_TOKEN }}" \ + "https://quay.io/api/v1/repository/${NAMESPACE}/${image}/tag/${tag}" \ + || true + done + done + done + env: + REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} + OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }} From cb783957791602de50ad52fe6d8260f5cddc02bf Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Thu, 25 Jun 2026 12:43:05 +0200 Subject: [PATCH 2/8] fix: use env var for secrets check in workflow condition GitHub Actions doesn't allow direct secrets references in step `if` conditions. Use an env var intermediary instead. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 230d5507f..d651208fb 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -166,6 +166,8 @@ jobs: permissions: contents: read packages: write + env: + HAS_QUAY_OAUTH: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -246,7 +248,7 @@ jobs: OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }} - name: Cleanup per-arch tags - if: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} + if: env.HAS_QUAY_OAUTH == 'true' run: | export OPERATOR_IMAGE_NAME=${{ vars.OPERATOR_IMAGE_NAME }} OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} From cb127eb14f4f5d7ab67cc9ec9b093167f6eb9311 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Thu, 25 Jun 2026 13:02:40 +0200 Subject: [PATCH 3/8] fix: remove unsupported --annotation flag from imagetools create The --annotation flag is not supported by docker buildx imagetools create on the ubuntu-latest runner. Expiry labels are already set during the per-arch image build via the Makefile LABEL variable. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index d651208fb..0e3f064e9 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -221,21 +221,18 @@ jobs: # Create manifest list for version tag docker buildx imagetools create \ - --annotation "quay.expires-after=14d" \ -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION} \ ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 # Create manifest list for version-sha tag docker buildx imagetools create \ - --annotation "quay.expires-after=14d" \ -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA} \ ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 # Create manifest list for latestNext tag (next or latest) docker buildx imagetools create \ - --annotation "quay.expires-after=14d" \ -t ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT} \ ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-amd64 \ ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-arm64 From 38ac280e1f269a5ed046ab2d34da2bdeeb4eba09 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Mon, 29 Jun 2026 10:51:01 +0200 Subject: [PATCH 4/8] fix: address Qodo auto-review findings - Use ${{ github.ref_name }} instead of detached-HEAD-unsafe git command for branch detection; avoids mistagging images (next vs latest) - Add strict shell mode (set -euo pipefail) and env var validation across all run: blocks; quote all variable expansions - Fix SHA-scoped manifest list to use ${BASE_VERSION}-${SHORT_SHA}-{amd64,arm64} instead of ${BASE_VERSION}-{amd64,arm64} (was using wrong image content) - Add separate `changes` job to gate both `build` and `merge` on any_changed output; prevents merge from running when no files changed - Remove unquoted redirections and improve shell robustness All 4 Qodo findings resolved. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 190 +++++++++++--------- 1 file changed, 104 insertions(+), 86 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 0e3f064e9..cdd05945c 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -18,25 +18,17 @@ env: REGISTRY: ${{ vars.REGISTRY }} jobs: - build: - name: Build (${{ matrix.os }}) - strategy: - fail-fast: false - matrix: - os: - - ubuntu-24.04 - - ubuntu-24.04-arm - runs-on: ${{ matrix.os }} - permissions: - contents: read - packages: write + changes: + name: Check for changes + runs-on: ubuntu-latest + outputs: + any_changed: ${{ steps.changed-files.outputs.any_changed }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - # check changes in this commit for regex include and exclude matches - name: Get changed files id: changed-files uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 @@ -60,51 +52,66 @@ jobs: .rhdh/** tests/** - - name: List all changed files (for troubleshooting) - env: - ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - run: | - for file in ${ALL_CHANGED_FILES}; do - echo "$file was changed" - done + build: + name: Build (${{ matrix.os }}) + needs: changes + if: needs.changes.outputs.any_changed == 'true' + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + - ubuntu-24.04-arm + runs-on: ${{ matrix.os }} + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 - name: Prepare - if: steps.changed-files.outputs.any_changed == 'true' env: MATRIX_OS: ${{ matrix.os }} run: | + set -euo pipefail SHORT_SHA=$(git rev-parse --short HEAD) - echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV + echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV" BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") - echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV - - if [ "$MATRIX_OS" == "ubuntu-24.04" ]; then - platform="linux/amd64" - elif [ "$MATRIX_OS" == "ubuntu-24.04-arm" ]; then - platform="linux/arm64" + echo "BASE_VERSION=${BASE_VERSION}" >> "$GITHUB_ENV" + + case "${MATRIX_OS}" in + ubuntu-24.04) + platform="linux/amd64" + ;; + ubuntu-24.04-arm) + platform="linux/arm64" + ;; + *) + echo "ERROR: Unknown runner OS: ${MATRIX_OS}" + exit 1 + ;; + esac + echo "PLATFORM=${platform}" >> "$GITHUB_ENV" + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" + echo "PLATFORM_ARCH=${platform#*/}" >> "$GITHUB_ENV" + + BRANCH_REF="${{ github.ref_name }}" + if [[ "${BRANCH_REF}" == "main" ]]; then + LATEST_NEXT="next" else - echo "Unknown runner OS: $MATRIX_OS" - exit 1 - fi - echo "PLATFORM=$platform" >> $GITHUB_ENV - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - echo "PLATFORM_ARCH=${platform#*/}" >> $GITHUB_ENV - - latestNext="next" - # for main branch, use next tags; for 1.y branches, use :latest tags - if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then - latestNext="latest" + LATEST_NEXT="latest" fi - echo "LATEST_NEXT=$latestNext" >> $GITHUB_ENV + echo "LATEST_NEXT=${LATEST_NEXT}" >> "$GITHUB_ENV" - name: Setup Go - if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version-file: 'go.mod' - name: Login to registry (${{ env.REGISTRY }}) - if: steps.changed-files.outputs.any_changed == 'true' uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ${{ env.REGISTRY }} @@ -112,31 +119,33 @@ jobs: password: ${{ secrets.QUAY_TOKEN }} - name: Build and push per-arch operator, bundle, and catalog images - if: steps.changed-files.outputs.any_changed == 'true' run: | + set -euo pipefail sudo apt-get -y update; sudo apt-get -y install skopeo podman export CONTAINER_TOOL=podman - export VERSION=${{ env.BASE_VERSION }} - export REGISTRY_WITH_ORG=${{ env.REGISTRY }}/${{ env.REGISTRY_ORG }} - export OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} - export IMAGE_TAG_BASE=${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME} + export VERSION="${{ env.BASE_VERSION }}" + export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}" + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" + export IMAGE_TAG_BASE="${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}" - set -ex + : "${PLATFORM:?PLATFORM must be set}" + : "${SHORT_SHA:?SHORT_SHA must be set}" + : "${LATEST_NEXT:?LATEST_NEXT must be set}" # Build all 3 images for this architecture - CONTAINER_TOOL=${CONTAINER_TOOL} VERSION=${VERSION} PLATFORM=${PLATFORM} make release-build + CONTAINER_TOOL="${CONTAINER_TOOL}" VERSION="${VERSION}" PLATFORM="${PLATFORM}" make release-build # Push per-arch tagged images - for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do - podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${PLATFORM_ARCH} - podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${PLATFORM_ARCH} + for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do + podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.PLATFORM_ARCH }}" + podman push -q "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.PLATFORM_ARCH }}" - podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }}-${PLATFORM_ARCH} - podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }}-${PLATFORM_ARCH} + podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${SHORT_SHA}-${{ env.PLATFORM_ARCH }}" + podman push -q "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${SHORT_SHA}-${{ env.PLATFORM_ARCH }}" - podman tag ${REGISTRY_WITH_ORG}/${image}:${VERSION} ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${PLATFORM_ARCH} - podman push -q ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${PLATFORM_ARCH} + podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${{ env.PLATFORM_ARCH }}" + podman push -q "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${{ env.PLATFORM_ARCH }}" done env: REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} @@ -144,13 +153,12 @@ jobs: GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }} - name: Upload build marker - if: steps.changed-files.outputs.any_changed == 'true' run: | + set -euo pipefail mkdir -p /tmp/build-markers echo "${{ env.PLATFORM_ARCH }}" > /tmp/build-markers/${{ env.PLATFORM_ARCH }} - name: Upload build marker artifact - if: steps.changed-files.outputs.any_changed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-marker-${{ env.PLATFORM_PAIR }} @@ -162,7 +170,9 @@ jobs: name: Create multi-arch manifests runs-on: ubuntu-latest needs: + - changes - build + if: needs.changes.outputs.any_changed == 'true' permissions: contents: read packages: write @@ -183,18 +193,20 @@ jobs: - name: Prepare run: | + set -euo pipefail SHORT_SHA=$(git rev-parse --short HEAD) - echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV + echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV" BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") - echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV + echo "BASE_VERSION=${BASE_VERSION}" >> "$GITHUB_ENV" - latestNext="next" - if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then - latestNext="latest" + BRANCH_REF="${{ github.ref_name }}" + if [[ "${BRANCH_REF}" == "main" ]]; then + LATEST_NEXT="next" + else + LATEST_NEXT="latest" fi - echo "LATEST_NEXT=$latestNext" >> $GITHUB_ENV + echo "LATEST_NEXT=${LATEST_NEXT}" >> "$GITHUB_ENV" - # Check which architectures actually built echo "Build markers found:" ls -la /tmp/build-markers/ || echo "No markers found" @@ -210,35 +222,37 @@ jobs: - name: Create manifest lists and push run: | - export REGISTRY_WITH_ORG=${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }} - export OPERATOR_IMAGE_NAME=${{ vars.OPERATOR_IMAGE_NAME }} - OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} + set -euo pipefail + export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}" + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" - set -ex + : "${BASE_VERSION:?BASE_VERSION must be set}" + : "${SHORT_SHA:?SHORT_SHA must be set}" + : "${LATEST_NEXT:?LATEST_NEXT must be set}" - for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do + for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do echo "=== Creating manifest list for ${image} ===" # Create manifest list for version tag docker buildx imagetools create \ - -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION} \ - ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ - ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 + -t "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}" \ + "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64" \ + "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64" - # Create manifest list for version-sha tag + # Create manifest list for version-sha tag (using SHA-scoped per-arch tags) docker buildx imagetools create \ - -t ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA} \ - ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64 \ - ${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64 + -t "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}" \ + "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}-amd64" \ + "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}-arm64" # Create manifest list for latestNext tag (next or latest) docker buildx imagetools create \ - -t ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT} \ - ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-amd64 \ - ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-arm64 + -t "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" \ + "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-amd64" \ + "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-arm64" echo "=== Inspecting manifest for ${image}:${LATEST_NEXT} ===" - docker buildx imagetools inspect ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT} + docker buildx imagetools inspect "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" done env: REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} @@ -247,13 +261,17 @@ jobs: - name: Cleanup per-arch tags if: env.HAS_QUAY_OAUTH == 'true' run: | - export OPERATOR_IMAGE_NAME=${{ vars.OPERATOR_IMAGE_NAME }} - OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator} - NAMESPACE=${{ vars.REGISTRY_ORG }} + set -euo pipefail + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" + export NAMESPACE="${{ vars.REGISTRY_ORG }}" + + : "${BASE_VERSION:?BASE_VERSION must be set}" + : "${SHORT_SHA:?SHORT_SHA must be set}" + : "${LATEST_NEXT:?LATEST_NEXT must be set}" - for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do + for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do for arch in amd64 arm64; do - for tag in ${BASE_VERSION}-${arch} ${BASE_VERSION}-${SHORT_SHA}-${arch} ${LATEST_NEXT}-${arch}; do + for tag in "${BASE_VERSION}-${arch}" "${BASE_VERSION}-${SHORT_SHA}-${arch}" "${LATEST_NEXT}-${arch}"; do echo "Deleting per-arch tag: ${image}:${tag}" curl -s -o /dev/null -w "%{http_code}" -X DELETE \ -H "Authorization: Bearer ${{ secrets.QUAY_OAUTH_TOKEN }}" \ From a864fbba52c245145af1539251e87a907bfc9317 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Mon, 29 Jun 2026 13:14:50 +0200 Subject: [PATCH 5/8] refactor: add conditional guards to skip steps when no changes Add if: needs.changes.outputs.any_changed == 'true' to individual steps in build job (Prepare, Setup Go, Login, Build, Upload marker) to explicitly skip when no relevant changes detected. This makes the gating more granular and avoids unnecessary compute. Add clarifying comment above Prepare step. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index cdd05945c..33accfbd4 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -72,7 +72,9 @@ jobs: with: fetch-depth: 0 + # Skip remaining steps if no relevant changes detected - name: Prepare + if: needs.changes.outputs.any_changed == 'true' env: MATRIX_OS: ${{ matrix.os }} run: | @@ -107,11 +109,13 @@ jobs: echo "LATEST_NEXT=${LATEST_NEXT}" >> "$GITHUB_ENV" - name: Setup Go + if: needs.changes.outputs.any_changed == 'true' uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version-file: 'go.mod' - name: Login to registry (${{ env.REGISTRY }}) + if: needs.changes.outputs.any_changed == 'true' uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ${{ env.REGISTRY }} @@ -119,6 +123,7 @@ jobs: password: ${{ secrets.QUAY_TOKEN }} - name: Build and push per-arch operator, bundle, and catalog images + if: needs.changes.outputs.any_changed == 'true' run: | set -euo pipefail sudo apt-get -y update; sudo apt-get -y install skopeo podman @@ -153,12 +158,14 @@ jobs: GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }} - name: Upload build marker + if: needs.changes.outputs.any_changed == 'true' run: | set -euo pipefail mkdir -p /tmp/build-markers echo "${{ env.PLATFORM_ARCH }}" > /tmp/build-markers/${{ env.PLATFORM_ARCH }} - name: Upload build marker artifact + if: needs.changes.outputs.any_changed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-marker-${{ env.PLATFORM_PAIR }} From 0d18d05728f59feeb7dc0751c2acb10bc38bf974 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Mon, 29 Jun 2026 13:26:00 +0200 Subject: [PATCH 6/8] fix: use || operator instead of :- for GitHub Actions expressions GitHub Actions doesn't support bash-style default syntax (:-). Use || 'operator' instead for fallback values. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 33accfbd4..75761d1d8 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -131,7 +131,7 @@ jobs: export CONTAINER_TOOL=podman export VERSION="${{ env.BASE_VERSION }}" export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}" - export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}" export IMAGE_TAG_BASE="${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}" : "${PLATFORM:?PLATFORM must be set}" @@ -231,7 +231,7 @@ jobs: run: | set -euo pipefail export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}" - export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}" : "${BASE_VERSION:?BASE_VERSION must be set}" : "${SHORT_SHA:?SHORT_SHA must be set}" @@ -269,7 +269,7 @@ jobs: if: env.HAS_QUAY_OAUTH == 'true' run: | set -euo pipefail - export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME :-operator }}" + export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}" export NAMESPACE="${{ vars.REGISTRY_ORG }}" : "${BASE_VERSION:?BASE_VERSION must be set}" From 256a876fe03fffabbb6d9cdbd6f4b794ee16a25c Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Wed, 1 Jul 2026 10:06:05 +0200 Subject: [PATCH 7/8] fix: move secret to env block and harden merge job condition - Move QUAY_OAUTH_TOKEN from inline ${{ secrets }} expansion in the curl command to the step's env: block, resolving SonarCloud warning about secrets expanded directly in run steps - Add always() && needs.build.result == 'success' to merge job's if condition so it won't attempt manifest creation when build failed Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 75761d1d8..ce6c8200d 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -179,7 +179,7 @@ jobs: needs: - changes - build - if: needs.changes.outputs.any_changed == 'true' + if: always() && needs.changes.outputs.any_changed == 'true' && needs.build.result == 'success' permissions: contents: read packages: write @@ -281,7 +281,7 @@ jobs: for tag in "${BASE_VERSION}-${arch}" "${BASE_VERSION}-${SHORT_SHA}-${arch}" "${LATEST_NEXT}-${arch}"; do echo "Deleting per-arch tag: ${image}:${tag}" curl -s -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: Bearer ${{ secrets.QUAY_OAUTH_TOKEN }}" \ + -H "Authorization: Bearer ${QUAY_OAUTH_TOKEN}" \ "https://quay.io/api/v1/repository/${NAMESPACE}/${image}/tag/${tag}" \ || true done @@ -290,3 +290,4 @@ jobs: env: REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }} + QUAY_OAUTH_TOKEN: ${{ secrets.QUAY_OAUTH_TOKEN }} From acee75f9da9f1171761cdcfcc2fc0ed5e5ce0195 Mon Sep 17 00:00:00 2001 From: Martin Polasko Date: Thu, 2 Jul 2026 13:20:13 +0200 Subject: [PATCH 8/8] fix: use needs.changes output for login step in build job The login step in the build job still referenced steps.changed-files.outputs.any_changed from the old single-job layout. Since the changed-files step now runs in a separate 'changes' job, this condition would always evaluate to false, skipping the registry login and causing all image pushes to fail with auth errors. Assisted-by: Claude Code --- .github/workflows/next-container-build.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index a8d7b373c..ce6c8200d 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -114,9 +114,8 @@ jobs: with: go-version-file: 'go.mod' - - name: Login to registry (${{env.REGISTRY}}) - # run this stage only if there are changes that match the includes and not the excludes - if: steps.changed-files.outputs.any_changed == 'true' + - name: Login to registry (${{ env.REGISTRY }}) + if: needs.changes.outputs.any_changed == 'true' uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ${{ env.REGISTRY }}