Skip to content

feat(ci): attest through release_ruleset, so BCR can verify the archive - #89

Merged
filmil merged 1 commit into
mainfrom
ai-dev-20260909-bwLkww-provenance
Sep 9, 2026
Merged

filmil merged 1 commit into
mainfrom
ai-dev-20260909-bwLkww-provenance

Conversation

@filmil

@filmil filmil commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

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

## 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
filmil force-pushed the ai-dev-20260909-bwLkww-provenance branch from 1f6f002 to c369b71 Compare September 9, 2026 03:32
@filmil
filmil merged commit e93b7d4 into main Sep 9, 2026
9 checks passed
@filmil
filmil deleted the ai-dev-20260909-bwLkww-provenance branch September 9, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant