Skip to content

🔒 Attest the five release binaries with GitHub build provenance - #404

Merged
taras merged 1 commit into
mainfrom
agent/issue-100-attest
Aug 9, 2026
Merged

🔒 Attest the five release binaries with GitHub build provenance#404
taras merged 1 commit into
mainfrom
agent/issue-100-attest

Conversation

@taras

@taras taras commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #100.

Why

A downloaded xmd binary can be checked against checksums.txt, but that only
proves the file matches a list published beside it. Nothing binds the bytes to
this repository or to the workflow run that compiled them, so a consumer has no
way to tell a release artifact from a substituted one.

The npm side of #100 turned out to need no work: npm generates provenance
automatically for a public package published from a public repository over
Trusted Publishing/OIDC, which is exactly what publish-one.yml already does.
The issue was narrowed to the binaries before this PR.

What changes

Before:

  • release.yml holds only contents: write. Five matrix jobs compile to
    dist/${{ matrix.artifact }} and upload it. GitHub publishes no attestation
    for any of them, and gh attestation verify on a released binary fails.

After:

  • Each matrix job attests its compiled binary between the compile and the
    upload. A successful tagged build leaves GitHub holding SLSA build provenance
    for all five subjects, and a consumer can verify one:

    gh attestation verify ./xmd-<target> -R taras/executable.md

The release assets themselves are byte-identical to before: same five binaries,
same names, same checksums.txt, no attestation bundle attached.

How it works

deno compile → dist/${{ matrix.artifact }} → actions/attest (signs + publishes to GitHub) → upload-artifact → release

Placement is the whole guarantee. Before the compile there are no bytes to
attest; after the upload an unattested binary is already in the artifact set
release downloads. Because release has needs: build, a failed attestation
fails that matrix job and withholds the entire release rather than publishing a
partial or unattested set — fail-fast: false still lets the other four targets
finish so the failure is diagnosable, but it does not make build successful.

One shared matrix step covers all five platforms, so the five-target list is not
duplicated anywhere; the existing exact-five matrix assertion is what proves the
coverage.

Review guide

Start with: .github/workflows/release.yml

Then review:

  1. The workflow-scope permissions block — contents: write preserved,
    id-token: write and attestations: write added.
  2. The Attest ${{ matrix.artifact }} step and its position in build.
  3. scripts/tests/release-targets.test.ts — the gray-matter parse and the six
    new assertions.
  4. specs/release-process-spec.md §1, §3, §4, §7.

Look carefully at:

  • The action pin. actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 is
    the commit the v4.2.2 tag resolves to (verified against the GitHub API, not
    copied from documentation). A mutable major tag here would put an unreviewed
    action inside a job holding id-token: write.

What must stay true

  • Every released binary is attested. Enforced by one shared matrix step, and
    checked by the exact-five matrix assertion plus the ordering assertion.
  • Attestation is a gate, not a notification. Enforced by release.needs: build with no continue-on-error and no if on the step; checked by
    cannot be softened into a warning and publishes no binary once an attestation has failed.
  • The action stays pinned to a reviewed commit. Checked by attests once for the whole matrix, at the reviewed pin.
  • Only subject-path is configured. A registry push, a renamed subject, or a
    custom predicate would quietly replace the default sign-and-publish behavior.
    Checked by exact equality on the step's with.

How to verify it

The regressions parse release.yml as YAML through gray-matter rather than
matching text, so a comment naming the action cannot satisfy them. Each was run
against a deliberate mutation of the workflow to confirm it is not vacuous:

Mutation Test that fails
drop attestations: write grants the scopes an attestation is minted and published with
unpin to actions/attest@v4 attests once for the whole matrix, at the reviewed pin
add a second attest step attests once for the whole matrix, at the reviewed pin
hardcode subject-path: dist/xmd-x86_64-apple-darwin attests the file the matrix job compiled, and configures nothing else
add push-to-registry: true attests the file the matrix job compiled, and configures nothing else
add continue-on-error: true cannot be softened into a warning
add if: always() cannot be softened into a warning
move the step above the compile attests after the compile and before the upload
move the step below the upload attests after the compile and before the upload
change release.needs to preflight publishes no binary once an attestation has failed

Run locally:

deno task test scripts/tests/release-targets.test.ts scripts/tests/publish-workflow-membership.test.ts

Also run on the rebased head: deno task lint, deno task check,
deno task check:jsr, git diff --check — all clean.

Scope

Included

  • Workflow permissions, the pinned attestation step, its placement.
  • Structural workflow regressions in the suite that already owns release.yml.
  • Release-spec updates for the binary attestation, npm's automatic provenance,
    the failure gate, reruns, and consumer verification.

Intentionally unchanged

  • publish-one.yml and the npm command. --provenance is deliberately
    absent: Trusted Publishing already produces provenance for these public
    packages, and the flag would add nothing. The spec now says so, so its absence
    does not read as an oversight.
  • checksums.txt and the release assets. Checksums stay; attestation is
    published to GitHub's attestation API, not attached as a release file.
  • The five targets, artifact names, and compile flags.
  • JSR publishing, which has no equivalent provenance path.
  • Code signing and notarization, owned by macOS Developer-ID signing + notarization for xmd #68.

Risks and limitations

  • The first real tagged release is the integration exercise. No tag or release
    was created to test OIDC, so this PR's proof is structural: the workflow
    parses to the intended shape, and the ordering and gating assertions hold.
    What CI cannot show here is that GitHub's attestation API accepts the token —
    that requires a v* tag push.
  • Verification of the five downloaded binaries with gh attestation verify is
    the acceptance check to run after the next release.
  • Rollback is removing the step and the two permissions; nothing persists in the
    repository, and existing attestations do not affect later releases.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified. (None.)
  • The description matches the final diff and test results.

Each matrix job attests its compiled dist/${{ matrix.artifact }} between the
compile and the upload, so GitHub publishes build provenance for the exact
bytes it produced and an unattested binary never enters the artifact set the
release job downloads. `release` needs the whole build matrix, so a failed
attestation withholds the release.

npm keeps publishing as it does: trusted publishing already generates
provenance for these public packages, so `--provenance` stays off the command
line.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

PR #404: 🔒 Attest the five release binaries with GitHub build provenance

3 files, +217 / -5

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 9, 2026 03:50
@taras
taras enabled auto-merge (squash) August 9, 2026 03:50
@taras
taras merged commit 8b7a3c2 into main Aug 9, 2026
11 checks passed
@taras
taras deleted the agent/issue-100-attest branch August 9, 2026 03:58
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.

Attest the five release binaries with GitHub build provenance

1 participant