diff --git a/.github/workflows/ci-docker-image.yml b/.github/workflows/ci-docker-image.yml index 7d07688feb..6d36a94e80 100644 --- a/.github/workflows/ci-docker-image.yml +++ b/.github/workflows/ci-docker-image.yml @@ -202,9 +202,33 @@ jobs: password: ${{ secrets.HARBOR_PASSWORD }} - name: Sign manifest + env: + IMAGE: ${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }} run: | - cosign sign --yes --recursive --new-bundle-format=false --use-signing-config=false \ - "${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }}" + # cosign gives up after two internal attempts when a Sigstore transparency + # log write fails, so a brief rekor.sigstore.dev blip is enough to fail a + # release. Retry the whole command instead; five attempts 60s apart cover a + # ~4-minute outage. The sbom job carries a verbatim copy of this function: + # the jobs run on separate runners, so sharing it would take a checkout or a + # third-party action in the signing path. Keep the two copies identical. + retry() { + local attempt + for attempt in 1 2 3 4 5; do + if "$@"; then + return 0 + fi + if [ "${attempt}" -lt 5 ]; then + echo "::warning::${1} ${2} failed (attempt ${attempt}/5), retrying in 60s" + sleep 60 + fi + done + echo "::error::${1} ${2} failed after 5 attempts" + return 1 + } + + retry cosign sign --yes --recursive \ + --new-bundle-format=false --use-signing-config=false \ + "${IMAGE}" sbom: needs: merge @@ -236,20 +260,16 @@ jobs: "${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }}" \ --output cyclonedx-json=infrahub-sbom.cdx.json - - name: Attest SBOM (SPDX) - run: | - cosign attest --yes \ - --type spdxjson \ - --predicate infrahub-sbom.spdx.json \ - "${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }}" - - - name: Attest SBOM (CycloneDX) - run: | - cosign attest --yes \ - --type cyclonedx \ - --predicate infrahub-sbom.cdx.json \ - "${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }}" - + # Uploaded before the attestations so that a transparency-log outage cannot cost + # us the SBOMs themselves: v1.10.7 shipped without any because the attest step + # failed first and skipped this one. + # + # overwrite is required precisely because this now runs before a step that can + # fail. Artifacts are scoped to the run rather than the attempt, so on a re-run + # this name already exists from the earlier attempt and the default overwrite: + # false would fail the upload, breaking the recovery path this ordering exists to + # protect. Every caller passes a version unique to its invocation, so the only + # artifact this can replace is the same SBOM from a previous attempt. - name: Upload SBOM artifacts uses: actions/upload-artifact@v7 with: @@ -258,3 +278,37 @@ jobs: infrahub-sbom.spdx.json infrahub-sbom.cdx.json retention-days: 90 + overwrite: true + + - name: Attest SBOMs + env: + IMAGE: ${{ vars.HARBOR_HOST }}/${{ github.repository }}@${{ needs.merge.outputs.digest }} + run: | + # Same transparency-log flakiness the sign job guards against; this is a + # verbatim copy of that job's retry function (separate runners, so it cannot + # be shared without a checkout or a third-party action in the signing path). + # Keep the two copies identical. + retry() { + local attempt + for attempt in 1 2 3 4 5; do + if "$@"; then + return 0 + fi + if [ "${attempt}" -lt 5 ]; then + echo "::warning::${1} ${2} failed (attempt ${attempt}/5), retrying in 60s" + sleep 60 + fi + done + echo "::error::${1} ${2} failed after 5 attempts" + return 1 + } + + retry cosign attest --yes \ + --type spdxjson \ + --predicate infrahub-sbom.spdx.json \ + "${IMAGE}" + + retry cosign attest --yes \ + --type cyclonedx \ + --predicate infrahub-sbom.cdx.json \ + "${IMAGE}"