From 27f506021a26ec01fd637fe4c7f9cffc138fffca Mon Sep 17 00:00:00 2001 From: Fortune-Ndlovu Date: Thu, 30 Jul 2026 14:31:31 +0100 Subject: [PATCH 1/8] ci: add cosign signing to community image builds Add keyless cosign signing to the multi-arch merge job so that community images at quay.io/rhdh-community/operator and operator-catalog are signed after the multi-arch manifest is created. This allows consumers to verify image provenance using cosign verify with the GitHub Actions OIDC issuer. Signed-off-by: Fortune Ndlovu --- .github/workflows/next-container-build.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 513eefe0d..d50f1f897 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -243,6 +243,7 @@ jobs: permissions: contents: read packages: write + id-token: write env: HAS_QUAY_OAUTH: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} SHORT_SHA: ${{ needs.changes.outputs.short_sha }} @@ -259,6 +260,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Create manifest lists and push run: | set -euo pipefail @@ -300,6 +304,23 @@ jobs: REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} + - name: Sign the published images + run: | + set -euo pipefail + export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" + + for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-catalog"; do + DIGEST=$(docker buildx imagetools inspect --format '{{json .Manifest}}' "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" | jq -r '.digest') + + for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do + echo "Signing ${REGISTRY_WITH_ORG}/${image}:${tag}@${DIGEST}" + cosign sign --yes "${REGISTRY_WITH_ORG}/${image}:${tag}@${DIGEST}" + done + done + env: + REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} + OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} + - name: Cleanup per-arch tags if: env.HAS_QUAY_OAUTH == 'true' && vars.REGISTRY == 'quay.io' run: | From cc49f5aa204b45a939331521a42f0c5302202864 Mon Sep 17 00:00:00 2001 From: Fortune Ndlovu Date: Fri, 31 Jul 2026 15:20:50 +0100 Subject: [PATCH 2/8] chore(ci): harden cosign signing step Use buildx template for digest retrieval instead of fragile JSON parsing, add credential guard and null-check for graceful fallback. Signed-off-by: Fortune Ndlovu Signed-off-by: Fortune-Ndlovu --- .github/workflows/next-container-build.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index d50f1f897..4449fba56 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -245,6 +245,7 @@ jobs: packages: write id-token: write env: + HAS_QUAY_AUTH: ${{ secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} HAS_QUAY_OAUTH: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} SHORT_SHA: ${{ needs.changes.outputs.short_sha }} BASE_VERSION: ${{ needs.changes.outputs.base_version }} @@ -305,12 +306,18 @@ jobs: OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} - name: Sign the published images + if: env.HAS_QUAY_AUTH == 'true' run: | set -euo pipefail export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-catalog"; do - DIGEST=$(docker buildx imagetools inspect --format '{{json .Manifest}}' "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" | jq -r '.digest') + DIGEST=$(docker buildx imagetools inspect "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" --format '{{println .Digest}}') + + if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then + echo "::warning::Could not resolve digest for ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}, skipping signing" + continue + fi for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do echo "Signing ${REGISTRY_WITH_ORG}/${image}:${tag}@${DIGEST}" From bbae7edbdc26587197e07fe8e27ca30799206b39 Mon Sep 17 00:00:00 2001 From: Fortune Ndlovu Date: Fri, 31 Jul 2026 15:31:46 +0100 Subject: [PATCH 3/8] chore(ci): resolve digest per-tag and fail on signing errors Address review feedback: resolve each tag's digest independently instead of assuming all tags share one digest, and fail the build if digest resolution fails rather than silently skipping signing. Signed-off-by: Fortune Ndlovu Signed-off-by: Fortune-Ndlovu --- .github/workflows/next-container-build.yaml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 4449fba56..741cf3ae6 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -312,16 +312,15 @@ jobs: export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-catalog"; do - DIGEST=$(docker buildx imagetools inspect "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" --format '{{println .Digest}}') - - if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then - echo "::warning::Could not resolve digest for ${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}, skipping signing" - continue - fi - for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do - echo "Signing ${REGISTRY_WITH_ORG}/${image}:${tag}@${DIGEST}" - cosign sign --yes "${REGISTRY_WITH_ORG}/${image}:${tag}@${DIGEST}" + FULL_REF="${REGISTRY_WITH_ORG}/${image}:${tag}" + DIGEST=$(docker buildx imagetools inspect "$FULL_REF" --format '{{println .Digest}}') + if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then + echo "::error::Could not resolve digest for ${FULL_REF}" + exit 1 + fi + echo "Signing ${FULL_REF}@${DIGEST}" + cosign sign --yes "${FULL_REF}@${DIGEST}" done done env: From de732f75be0b035492c3be2638b14653c6d16926 Mon Sep 17 00:00:00 2001 From: Fortune Ndlovu Date: Fri, 31 Jul 2026 15:36:07 +0100 Subject: [PATCH 4/8] chore(ci): fix signing gate var and add env validation Fix HAS_QUAY_AUTH to check vars.QUAY_USERNAME (matching the login step) instead of secrets.QUAY_USERNAME. Add required env var checks with failing parameter expansion, matching the pattern in other steps of the same workflow. Signed-off-by: Fortune Ndlovu Signed-off-by: Fortune-Ndlovu --- .github/workflows/next-container-build.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 741cf3ae6..45775b1de 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -245,7 +245,7 @@ jobs: packages: write id-token: write env: - HAS_QUAY_AUTH: ${{ secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} + HAS_QUAY_AUTH: ${{ vars.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} HAS_QUAY_OAUTH: ${{ secrets.QUAY_OAUTH_TOKEN != '' }} SHORT_SHA: ${{ needs.changes.outputs.short_sha }} BASE_VERSION: ${{ needs.changes.outputs.base_version }} @@ -311,6 +311,13 @@ jobs: set -euo pipefail export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" + : "${REGISTRY:?REGISTRY must be set}" + : "${REGISTRY_ORG:?REGISTRY_ORG must be set}" + : "${OPERATOR_IMAGE_NAME:?OPERATOR_IMAGE_NAME must be set}" + : "${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}-catalog"; do for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do FULL_REF="${REGISTRY_WITH_ORG}/${image}:${tag}" From 55e7a05757c514aeb89de8786087d4e91eea4d48 Mon Sep 17 00:00:00 2001 From: Fortune Ndlovu Date: Fri, 31 Jul 2026 15:50:06 +0100 Subject: [PATCH 5/8] chore(ci): remove redundant env var checks from signing step The preceding "Create manifest lists" step already validates these variables; they are guaranteed non-empty by the time signing runs. Signed-off-by: Fortune Ndlovu Signed-off-by: Fortune-Ndlovu --- .github/workflows/next-container-build.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 45775b1de..ba757887a 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -311,13 +311,6 @@ jobs: set -euo pipefail export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" - : "${REGISTRY:?REGISTRY must be set}" - : "${REGISTRY_ORG:?REGISTRY_ORG must be set}" - : "${OPERATOR_IMAGE_NAME:?OPERATOR_IMAGE_NAME must be set}" - : "${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}-catalog"; do for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do FULL_REF="${REGISTRY_WITH_ORG}/${image}:${tag}" From c05c4f07768aae3f716b4d902e95102f1136f00e Mon Sep 17 00:00:00 2001 From: Fortune Ndlovu Date: Fri, 31 Jul 2026 16:10:59 +0100 Subject: [PATCH 6/8] chore(ci): fix digest retrieval for cosign signing The --format '{{println .Digest}}' template field does not exist in imagetools inspect. Parse the standard output instead. Signed-off-by: Fortune Ndlovu Signed-off-by: Fortune-Ndlovu --- .github/workflows/next-container-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index ba757887a..b87e183af 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -314,7 +314,7 @@ jobs: for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-catalog"; do for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do FULL_REF="${REGISTRY_WITH_ORG}/${image}:${tag}" - DIGEST=$(docker buildx imagetools inspect "$FULL_REF" --format '{{println .Digest}}') + DIGEST=$(docker buildx imagetools inspect "$FULL_REF" | grep '^Digest:' | awk '{print $2}') if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then echo "::error::Could not resolve digest for ${FULL_REF}" exit 1 From deb5b4ef76974bc081ee762c764ba4c094ea8099 Mon Sep 17 00:00:00 2001 From: Fortune-Ndlovu Date: Tue, 4 Aug 2026 13:45:11 +0100 Subject: [PATCH 7/8] chore(ci): add cosign signing to operator-bundle image builds Sign the operator-bundle image tags (base version, SHA-scoped, and latest-next) with keyless cosign after publishing, gated on Quay credentials availability. Also add the required id-token write permission and guard the existing cosign install step in the merge job. --- .github/workflows/next-container-build.yaml | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index b87e183af..2b23c4809 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -86,7 +86,9 @@ jobs: permissions: contents: read packages: write + id-token: write env: + HAS_QUAY_AUTH: ${{ vars.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} SHORT_SHA: ${{ needs.changes.outputs.short_sha }} BASE_VERSION: ${{ needs.changes.outputs.base_version }} LATEST_NEXT: ${{ needs.changes.outputs.latest_next }} @@ -108,6 +110,10 @@ jobs: username: ${{ vars.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} + - name: Install cosign + if: env.HAS_QUAY_AUTH == 'true' + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Build and push operator-bundle run: | set -euo pipefail @@ -133,6 +139,26 @@ jobs: OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }} + - name: Sign the published bundle image + if: env.HAS_QUAY_AUTH == 'true' + run: | + set -euo pipefail + export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" + + for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do + FULL_REF="${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}-bundle:${tag}" + DIGEST=$(skopeo inspect --format '{{.Digest}}' "docker://${FULL_REF}") + if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then + echo "::error::Could not resolve digest for ${FULL_REF}" + exit 1 + fi + echo "Signing ${FULL_REF}@${DIGEST}" + cosign sign --yes "${FULL_REF}@${DIGEST}" + done + env: + REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} + OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} + build: name: Build (${{ matrix.os }}) needs: @@ -262,6 +288,7 @@ jobs: uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Install cosign + if: env.HAS_QUAY_AUTH == 'true' uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Create manifest lists and push From 51901893fa5046ab73fbde83c91b4af14d572557 Mon Sep 17 00:00:00 2001 From: Fortune-Ndlovu Date: Tue, 4 Aug 2026 13:49:00 +0100 Subject: [PATCH 8/8] chore(ci): move operator-bundle image signing to merge job Sign the operator-bundle image alongside the operator and catalog images in the multi-arch merge job rather than in the bundle-build job. This consolidates image signing in one place and removes the now-redundant cosign setup, id-token permission, and credential gate from the bundle-build job. --- .github/workflows/next-container-build.yaml | 28 +-------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/workflows/next-container-build.yaml b/.github/workflows/next-container-build.yaml index 2b23c4809..270ec43df 100644 --- a/.github/workflows/next-container-build.yaml +++ b/.github/workflows/next-container-build.yaml @@ -86,9 +86,7 @@ jobs: permissions: contents: read packages: write - id-token: write env: - HAS_QUAY_AUTH: ${{ vars.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} SHORT_SHA: ${{ needs.changes.outputs.short_sha }} BASE_VERSION: ${{ needs.changes.outputs.base_version }} LATEST_NEXT: ${{ needs.changes.outputs.latest_next }} @@ -110,10 +108,6 @@ jobs: username: ${{ vars.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - - name: Install cosign - if: env.HAS_QUAY_AUTH == 'true' - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - name: Build and push operator-bundle run: | set -euo pipefail @@ -139,26 +133,6 @@ jobs: OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }} - - name: Sign the published bundle image - if: env.HAS_QUAY_AUTH == 'true' - run: | - set -euo pipefail - export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" - - for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do - FULL_REF="${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}-bundle:${tag}" - DIGEST=$(skopeo inspect --format '{{.Digest}}' "docker://${FULL_REF}") - if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then - echo "::error::Could not resolve digest for ${FULL_REF}" - exit 1 - fi - echo "Signing ${FULL_REF}@${DIGEST}" - cosign sign --yes "${FULL_REF}@${DIGEST}" - done - env: - REGISTRY_ORG: ${{ vars.REGISTRY_ORG }} - OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME || 'operator' }} - build: name: Build (${{ matrix.os }}) needs: @@ -338,7 +312,7 @@ jobs: set -euo pipefail export REGISTRY_WITH_ORG="${REGISTRY}/${REGISTRY_ORG}" - for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-catalog"; do + for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do for tag in "${BASE_VERSION}" "${BASE_VERSION}-${SHORT_SHA}" "${LATEST_NEXT}"; do FULL_REF="${REGISTRY_WITH_ORG}/${image}:${tag}" DIGEST=$(docker buildx imagetools inspect "$FULL_REF" | grep '^Digest:' | awk '{print $2}')