Repository navigation
feat(ci): attest through release_ruleset, so BCR can verify the archive - #89
Merged
Merged
Conversation
## Provenance attestation The Bazel Central Registry verifies a source archive attestation with slsa-verifier against a hard-coded builder id, set in tools/slsa.py of bazelbuild/bazel-central-registry. That id names release_ruleset.yaml, so the release half of tag-and-release.yml is replaced rather than extended. * `.github/workflows/release_prep.sh` is new. release_ruleset.yaml requires this exact path, builds the archive there, and reads the release notes from its stdout. * A new `source_release` job runs the tests, builds the archive, attests it and creates the release, through release_ruleset.yaml@v7.7.0. * The binary matrix job waits for `source_release` and uploads to the tag the tag job produced. It used to find the tag with a dry run of the tag action and upload to `previous_tag`, which is the release before this one. * The registry publish waits for `source_release`, so publish-to-bcr reads the archive from the release run's artifacts. ## Publication `publish.yml`, which writes the personal registry, moves to publish-to-bcr v1.5.0 and stops attesting. `publish-bcr.yml` moves to v1.5.0, starts attesting, and opens its pull request with `draft: false`. Only one publish may attest. Every publish-to-bcr run with `attest: true` uploads MODULE.bazel.intoto.jsonl and source.json.intoto.jsonl to the release under those fixed names and records the hash of the file it uploaded in its own registry entry, so two attesting publishes overwrite each other and the entry written first is left naming hashes the release no longer serves. BCR verifies attestations at presubmit and the personal registry does not, so BCR is the one that keeps them. A new `publish-bcr` job runs it at the end of the release, after the personal registry publish. Until now `publish-bcr.yml` existed but nothing called it: it is not referenced anywhere in tag-and-release.yml, so it only ever ran when dispatched by hand, which happened three times in April 2026 and not since. Those dispatches opened two pull requests against the registry, and both were opened as drafts, because publish-to-bcr defaults `draft` to true and this repository never set it. Neither landed. bazelbuild/bazel-central-registry#8468 sat as a draft nobody reviewed and the stale bot closed it after 44 days. bazelbuild/bazel-central-registry#8530 failed presubmit and was closed three minutes after it opened. `draft: false` is what stops the first of those happening again. ## The attestations this repository produced were unusable `tag-and-release.yml` called `actions/attest-build-provenance` itself. That produces a valid attestation which BCR rejects, and this was measured on the v45.0.0 release rather than assumed. slsa-verifier fails it twice over: --builder-id .../bazel-contrib/.github/.../release_ruleset.yaml FAILED: untrusted reusable workflow: builderID does not match provenance: got 'https://github.com/bazel-contrib/.github/.github/workflows/release_ruleset.yaml', expected 'https://github.com/filmil/upspin/.github/workflows/tag-and-release.yml' and, even given its own builder id, FAILED: invalid ref: "refs/heads/main": unexpected ref type: "heads" because the workflow runs from a branch, not from the tag it releases. ## Two BCR template fixes `.bcr/attestations.template.json` is deleted. It listed two attestations, `source.json` and `MODULE.bazel`. BCR requires exactly three, the source archive included, and rejects an entry that names two. publish-to-bcr writes a correct three entry default when the file is absent, so there is no reason to carry one by hand. `.bcr/source.template.json` had `"integrity": "{INTEGRITY}"`. publish-to-bcr substitutes only `OWNER`, `REPO`, `TAG` and `VERSION`, and only into `url`, `docs_url` and `strip_prefix`, so that placeholder was never substituted; it computes the integrity itself. It now reads `""`, which is the convention the other modules use. ## Verification The archive this script builds is byte identical to the one v45.0.0 shipped: recorded in the registry entry : sha256-ykJiOH8saqPi9ezKrANU9VVoFtUHtsPbolJhlxcVPWo= the released v45.0.0.tar.gz : sha256-ykJiOH8saqPi9ezKrANU9VVoFtUHtsPbolJhlxcVPWo= built here by release_prep.sh : sha256-ykJiOH8saqPi9ezKrANU9VVoFtUHtsPbolJhlxcVPWo= so `strip_prefix` stays correct and the entry keeps describing the same tree. `git archive` is used for the tarball because that is what built it before. The convenience zip keeps the exclusions thedoctor0/zip-release used, plus `--symlinks`, without which `zip` follows the bazel convenience symlinks and walks the whole output base, because it walks the tree before applying `-x`. The tag job is left alone deliberately. Adding `fetch-depth: 0` there would change which commits the tag action analyses, and so possibly the size of the version bump, on a repository whose version tracks upstream. This commit has been created by an automated coding assistant, with human supervision. Prompt used to generate this change, in full: > now, refresh ai-coding-sop from origin/main, re-read to learn how I code, > and prepare BCR publication, my private registry publication, and > attestation support for https://github.com/filmil/upspin and > https://github.com/filmil/upspin-gdrive Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6wR5DN3FNpTDAsqj9K4uD
filmil
force-pushed
the
ai-dev-20260909-bwLkww-provenance
branch
from
September 9, 2026 03:32
1f6f002 to
c369b71
Compare
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.
Provenance attestation
The Bazel Central Registry verifies a source archive attestation with
slsa-verifier against a hard-coded builder id, set in tools/slsa.py of
bazelbuild/bazel-central-registry. That id names release_ruleset.yaml, so the
release half of tag-and-release.yml is replaced rather than extended.
.github/workflows/release_prep.shis new. release_ruleset.yaml requiresthis exact path, builds the archive there, and reads the release notes from
its stdout.
source_releasejob runs the tests, builds the archive, attests itand creates the release, through release_ruleset.yaml@v7.7.0.
source_releaseand uploads to the tag thetag job produced. It used to find the tag with a dry run of the tag action
and upload to
previous_tag, which is the release before this one.source_release, so publish-to-bcr reads thearchive from the release run's artifacts.
Publication
publish.yml, which writes the personal registry, moves to publish-to-bcrv1.5.0 and stops attesting.
publish-bcr.ymlmoves to v1.5.0, startsattesting, and opens its pull request with
draft: false.Only one publish may attest. Every publish-to-bcr run with
attest: trueuploads MODULE.bazel.intoto.jsonl and source.json.intoto.jsonl to the release
under those fixed names and records the hash of the file it uploaded in its
own registry entry, so two attesting publishes overwrite each other and the
entry written first is left naming hashes the release no longer serves. BCR
verifies attestations at presubmit and the personal registry does not, so BCR
is the one that keeps them.
A new
publish-bcrjob runs it at the end of the release, after the personalregistry publish. Until now
publish-bcr.ymlexisted but nothing called it:it is not referenced anywhere in tag-and-release.yml, so it only ever ran when
dispatched by hand, which happened three times in April 2026 and not since.
Those dispatches opened two pull requests against the registry, and both were
opened as drafts, because publish-to-bcr defaults
draftto true and thisrepository never set it. Neither landed. bazelbuild/bazel-central-registry#8468
sat as a draft nobody reviewed and the stale bot closed it after 44 days.
bazelbuild/bazel-central-registry#8530 failed presubmit and was closed three
minutes after it opened.
draft: falseis what stops the first of thosehappening again.
The attestations this repository produced were unusable
tag-and-release.ymlcalledactions/attest-build-provenanceitself. Thatproduces a valid attestation which BCR rejects, and this was measured on the
v45.0.0 release rather than assumed. slsa-verifier fails it twice over:
and, even given its own builder id,
because the workflow runs from a branch, not from the tag it releases.
Two BCR template fixes
.bcr/attestations.template.jsonis deleted. It listed two attestations,source.jsonandMODULE.bazel. BCR requires exactly three, the sourcearchive included, and rejects an entry that names two. publish-to-bcr writes
a correct three entry default when the file is absent, so there is no reason
to carry one by hand.
.bcr/source.template.jsonhad"integrity": "{INTEGRITY}". publish-to-bcrsubstitutes only
OWNER,REPO,TAGandVERSION, and only intourl,docs_urlandstrip_prefix, so that placeholder was neversubstituted; it computes the integrity itself. It now reads
"", which isthe convention the other modules use.
Verification
The archive this script builds is byte identical to the one v45.0.0 shipped:
so
strip_prefixstays correct and the entry keeps describing the same tree.git archiveis used for the tarball because that is what built it before.The convenience zip keeps the exclusions thedoctor0/zip-release used, plus
--symlinks, without whichzipfollows the bazel convenience symlinks andwalks the whole output base, because it walks the tree before applying
-x.The tag job is left alone deliberately. Adding
fetch-depth: 0there wouldchange which commits the tag action analyses, and so possibly the size of the
version bump, on a repository whose version tracks upstream.
This commit has been created by an automated coding assistant,
with human supervision.
Prompt used to generate this change, in full:
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01R6wR5DN3FNpTDAsqj9K4uD