ci(docker): attest the SBOM to each image by its pushed digest — main and tags - #180
Merged
Merged
Conversation
… and tags docker.yml now generates the CycloneDX SBOM in a read-only job (sbom.yml's three SBOM steps, verbatim) and attests it to the slim and the office image with actions/attest v4.2.2: signed through Sigstore keyless OIDC, bound to the digest each build leg reported, stored with the repository's attestations and pushed to GHCR. It runs on every build (tags, main and manual rebuilds), so :latest and :office carry one too. release.yml finds the image digest only by tag, best effort, after a 60-second sleep, so it was the wrong place to attest. Built on #170: no image it attests comes from a restored build cache. The attesting job holds attestations, id-token and packages write, checks nothing out, installs nothing, runs one command (the registry login, with the docker CLI) and otherwise only download-artifact and attest. actions/attest-sbom has been deprecated since v4.0.0 (a wrapper that prints a warning), so actions/attest is pinned directly. create-storage-record is off: storage records exist for organisation-owned repositories only. build-and-push exposes one digest output per variant; the runner drops empty outputs, so each leg sets only its own. The SBOM covers the Python packages from requirements.lock, plus the pip of the venv it is generated from, and not the interpreter, the Debian packages, FFmpeg, Ghostscript or LibreOffice; docs, the workflow comment and the CHANGELOG say so. docs/release-signing.md documents `gh attestation verify` with --predicate-type https://cyclonedx.org/bom (without it gh accepts only SLSA provenance, and the check fails), --signer-workflow and --source-ref, and --source-digest to tie the attestation to the commit a verified tag signature covers. Rejected: attesting in release.yml (digest only by tag, best effort); docker/login-action in the attesting job (third-party code next to the signing token); one job output for both legs (the last leg would win). docker.yml cannot be tried on a branch (a dispatch pushes :latest and :office), so the guards pin what its first run on main needs: SBOM steps equal to sbom.yml's, every variant attested by its own digest output, push-to-registry, no if: or continue-on-error on either job or its steps, matching artifact and file names, the attesting job's actions, permissions and single command, a read-only SBOM job without secrets, cache or kept credentials, and the documented command's flags. Each of 42 simulated regressions fails at least one guard; two harmless edits pass. Security and code review: no blockers; their findings on the docs, the CHANGELOG and the guards are addressed. A pre-existing tag bug they found (the office build also pushes :latest on a release) is left for its own PR. Full suite 1478 green (72 skipped, Linux-only); ruff clean; gitleaks and hook patterns clean; pip-audit not needed (no dependency change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MrChengLen
enabled auto-merge
September 29, 2026 07:45
…back in two commits Resolves the CHANGELOG conflict without a force-push: with the entry out, GitHub can merge main into the branch; the next commit puts it back on top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngelog Second half of the conflict resolution: the entry taken out two commits ago goes back on top of [Unreleased], above the entries main brought in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ommits Every PR adds its CHANGELOG entry at the same place, so each merge to main conflicts with every open PR. Taking this PR's entry out lets GitHub merge main in cleanly; the next commit puts it back on top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MrChengLen
added a commit
that referenced
this pull request
Sep 29, 2026
…ommits main moved on (#178, #179, #180 and #170) and adds entries at the top of CHANGELOG.md, so GitHub's update-branch refuses. A merge commit cannot be signed from this machine, so the entry leaves for the merge of main and comes back in the commit after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every image
docker.ymlpushes — slim and office, on release tags, on every build ofmainand on manual rebuilds — now carries a signed SBOM attestation:sbomjob (contents: read):sbom.yml's three SBOM steps, verbatim, then hands the SBOM over as an artifact.attest-sbomjob (matrixbase/office;attestations,id-token,packages: write): downloads the artifact intosbom/, logs in to GHCR with the docker CLI, and runsactions/attest@1e69f48… # v4.2.2withsubject-name+ the build's exact digest,sbom-pathandpush-to-registry: true. No checkout, no install, no third-party action.build-and-pushexposesdigest-base/digest-office: one output name per matrix leg. The runner does not write empty outputs, so neither leg overwrites the other's digest.Why
release.ymlresolves the image digest only by tag, best effort, aftersleep 60— the wrong place to attest.docker.ymlknows each variant's digest exactly (the index digest, the same one cosign signs and a tag resolves to).Scope label: the SBOM covers the Python packages from
requirements.lock(plus thepipof the venv it is generated from) — not the interpreter, the Debian packages, FFmpeg, Ghostscript or LibreOffice. The docs, the workflow comment and the CHANGELOG say so.Decided before implementation
:latest/:officeare covered and the path runs on every merge rather than first at a release.actions/attestv4.2.2 instead ofactions/attest-sbom, which has been deprecated since v4.0.0 (a wrapper aroundactions/attestv4.1.0 that prints a warning). Same inputs.Builds on #170 (merged): no image this attests comes from a restored build cache.
Verification
docs/release-signing.mddocumentsgh attestation verify oci://… --owner MrChengLen --signer-workflow MrChengLen/FileMorph/.github/workflows/docker.yml --source-ref refs/tags/vX.Y.Z --predicate-type https://cyclonedx.org/bom(--predicate-typeis required,ghdefaults to SLSA provenance), plus--source-digestto tie it to the commit a verified tag signature covers.docker.ymlcannot be tried on a branch (a dispatch pushes:latest/:office), sotests/test_supply_chain_hygiene.pypins what the first run on main needs. Each of 42 simulated regressions fails at least one guard; two harmless edits pass.:latest, via metadata-action'slatest=auto) is left for its own PR.After merge: the first Docker run on main must show both
attest-sbomlegs green, andgh attestation verify oci://ghcr.io/mrchenglen/filemorph:latest --owner MrChengLen --signer-workflow MrChengLen/FileMorph/.github/workflows/docker.yml --source-ref refs/heads/main --predicate-type https://cyclonedx.org/bommust succeed.🤖 Generated with Claude Code