Spaces:
Running
Running
| name: Container build + GHCR push | |
| # Builds the root Dockerfile, generates an image SBOM, and pushes to GHCR: | |
| # * Pull-request: build + smoke-test only (no push, no registry login). | |
| # * Push to main: build + push SHA-tagged image to ghcr.io/szl-holdings/a11oy. | |
| # * Release published: push semver + latest tags, sign with cosign keyless. | |
| # | |
| # L1 fix (2026-05-31): REVISION build-arg is now passed as github.sha so that | |
| # the runtime ENV A11OY_GIT_SHA is populated in the container and /healthz | |
| # returns the real deployed SHA. Reference: red-team finding L1. | |
| # | |
| # Cosign keyless verification (no stored key; GitHub OIDC + Fulcio + Rekor): | |
| # cosign verify \ | |
| # --certificate-identity-regexp \ | |
| # "https://github.com/szl-holdings/a11oy/.github/workflows/docker-build.yml.*" \ | |
| # --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | |
| # ghcr.io/szl-holdings/a11oy:<tag> | |
| # | |
| # References: | |
| # docker/build-push-action: https://github.com/docker/build-push-action | |
| # anchore/sbom-action: https://github.com/anchore/sbom-action | |
| # cosign keyless: https://docs.sigstore.dev/cosign/signing/overview/ | |
| # | |
| # Authored for SZL Holdings. Signed-off per repository DCO. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| permissions: | |
| contents: read | |
| env: | |
| IMAGE: ghcr.io/szl-holdings/a11oy | |
| jobs: | |
| build: | |
| name: Build image + SBOM (push on main + release) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write # push to GHCR on main and release | |
| id-token: write # cosign keyless OIDC token | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 | |
| - name: Derive image version and tags | |
| id: ver | |
| run: | | |
| SHA7="${GITHUB_SHA::7}" | |
| BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| VERSION="${{ github.event.release.tag_name }}" | |
| TAGS="${{ env.IMAGE }}:${VERSION} | |
| ${{ env.IMAGE }}:latest | |
| ${{ env.IMAGE }}:sha-${SHA7}" | |
| elif [ "${{ github.event_name }}" = "push" ]; then | |
| VERSION="0.0.0-dev-${SHA7}" | |
| TAGS="${{ env.IMAGE }}:sha-${SHA7}" | |
| else | |
| VERSION="0.0.0-pr-${SHA7}" | |
| TAGS="${{ env.IMAGE }}:pr-${SHA7}" | |
| fi | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "sha7=${SHA7}" >> "$GITHUB_OUTPUT" | |
| echo "build_date=${BUILD_DATE}" >> "$GITHUB_OUTPUT" | |
| # Multi-line value — use heredoc to avoid quoting issues. | |
| { | |
| echo "tags<<EOF" | |
| echo "${TAGS}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| # Log in on push-to-main and on release; skip for PRs. | |
| - name: Log in to GHCR | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build image (push on main + release; load on PR) | |
| id: build | |
| uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 | |
| with: | |
| context: . | |
| file: Dockerfile | |
| build-args: | | |
| VERSION=${{ steps.ver.outputs.version }} | |
| REVISION=${{ github.sha }} | |
| BUILD_DATE=${{ steps.ver.outputs.build_date }} | |
| push: ${{ github.event_name != 'pull_request' }} | |
| load: ${{ github.event_name == 'pull_request' }} | |
| tags: ${{ steps.ver.outputs.tags }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # FIX (Yachay, empire-reliability 2026-06-01): docker/build-push-action@v6 | |
| # defaults to provenance:true, which exports an OCI attestation manifest. | |
| # Pushing that referrers index to ghcr.io/szl-holdings/* returns 403 Forbidden | |
| # on the attestation blob HEAD (org GHCR rejects the auto-created attestation | |
| # index). Image SBOM is already produced by the dedicated anchore/syft step, | |
| # so disabling buildx attestations is the root-cause fix, not a workaround. | |
| provenance: false | |
| sbom: false | |
| - name: Smoke test image (PR builds — loaded into local daemon) | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| TAG="${{ env.IMAGE }}:pr-${{ steps.ver.outputs.sha7 }}" | |
| echo "=== --version ===" | |
| docker run --rm "${TAG}" --version | |
| echo "=== --help ===" | |
| docker run --rm "${TAG}" --help | |
| # FIX (Yachay, empire-reliability 2026-06-01): the SBOM step previously always | |
| # referenced the pushed main tag `:sha-<sha7>`. On pull_request builds the image | |
| # is NOT pushed to GHCR (push:false) — it is `load`ed into the local Docker daemon | |
| # under tag `:pr-<sha7>`. Syft therefore tried to pull `ghcr.io/.../a11oy:sha-<sha7>` | |
| # which does not exist for PRs and returned `unauthorized` (registry has no such | |
| # manifest + no PR login), failing every PR run. Root-cause fix: scan the | |
| # locally-loaded PR image on PRs and the pushed SHA tag on push-to-main. | |
| - name: Generate image SBOM (CycloneDX) via Syft — push to main | |
| if: github.event_name == 'push' | |
| uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # v0.17.9 | |
| with: | |
| image: ${{ env.IMAGE }}:sha-${{ steps.ver.outputs.sha7 }} | |
| format: cyclonedx-json | |
| output-file: a11oy-image-sbom.cyclonedx.json | |
| upload-artifact: true | |
| - name: Generate image SBOM (CycloneDX) via Syft — PR (local image) | |
| if: github.event_name == 'pull_request' | |
| uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # v0.17.9 | |
| with: | |
| image: ${{ env.IMAGE }}:pr-${{ steps.ver.outputs.sha7 }} | |
| format: cyclonedx-json | |
| output-file: a11oy-image-sbom.cyclonedx.json | |
| upload-artifact: true | |
| - name: Install cosign (release only) | |
| if: github.event_name == 'release' | |
| uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 | |
| - name: Sign image with cosign keyless (release only) | |
| if: github.event_name == 'release' | |
| env: | |
| COSIGN_EXPERIMENTAL: "1" | |
| run: | | |
| DIGEST="${{ steps.build.outputs.digest }}" | |
| cosign sign --yes "${{ env.IMAGE }}@${DIGEST}" | |
| echo "Signed ${{ env.IMAGE }}@${DIGEST} (keyless OIDC)." | |
| - name: Generate + attach image SBOM on release (signed) | |
| if: github.event_name == 'release' | |
| uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # v0.17.9 | |
| with: | |
| image: ${{ env.IMAGE }}:${{ steps.ver.outputs.version }} | |
| format: cyclonedx-json | |
| output-file: a11oy-image-sbom.cyclonedx.json | |
| upload-artifact: true | |