Skip to content

fix(release): attach the binaries before the release is published - #16

Merged
bdelanghe merged 2 commits into
mainfrom
claude/next-task-gtdhdi
Aug 13, 2026
Merged

fix(release): attach the binaries before the release is published#16
bdelanghe merged 2 commits into
mainfrom
claude/next-task-gtdhdi

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

Fixes #5 — the forward half. The backfill half is not in this PR and needs a decision from you; see the last section.

Downstream of bounded-systems/mint#30, which landed the assets-artifact path an hour before this.

The bug

A published GitHub release is immutable, so whichever workflow publishes it first locks every other one out. binaries.yml and release.yml were both triggered by the v* tag, and two tag-triggered workflows are unordered by construction — so mint's release job published the release before binaries.yml could attach, and the upload died with

HTTP 422: Cannot upload assets to an immutable release

Both workflows were written create-or-upload to be "order-independent", which is correct for mutable releases and precisely wrong for immutable ones — order-independence is what puts the publish first half the time.

Worth noting: this hit v0.3.0 too, not just v0.2.0. The issue predates that release, so it only names v0.2.0, but both are published with immutable: true and carry only the two provenance assets:

release assets binaries
v0.2.0 mint-release.intoto.json, …sigstore.json
v0.3.0 mint-release.intoto.json, …sigstore.json

The fix — one writer

binaries.yml is folded into release.yml as a needs: job that uploads a workflow artifact; the artifact name goes to release-provenance via mint's new assets-artifact input. It downloads it from the same run, attaches it next to the provenance, and publishes once. Nothing races for the release.

Least privilege: the binaries job keeps the workflow's default contents: read. Only the release job gets contents: write + id-token: write + actions: read — the last one has to be declared by the caller, because a called workflow only ever gets the intersection with its caller's grants.

The pin pair looks mismatched on purpose

uses: bounded-systems/mint/.github/workflows/release-provenance.yml@576e904…  # mint main @ #30
with:
  ref: v0.3.1
  • The workflow pin moves to mint main at #30 (576e904), the commit that carries the draft-until-attached fix. No mint tag contains it yet — v0.5.0 predates it — so a commit SHA is the only way to consume it, which is what the org convention asks for regardless.
  • ref stays at v0.3.1. It selects the mint code that runs (mint attest), not the workflow. I checked: release-provenance.yml was byte-identical from v0.3.1 until #30 (v0.3.1's blob 2c510dc is exactly #30's pre-image), so bumping only the workflow pin carries the ordering fix and nothing else. Moving ref would put drift-gate's signed provenance onto three minors of unrelated mint changes, unreviewed here. version.yml is on v0.3.1 too — bump both together when mint tags.

Dependabot would revert this next Monday

Not hypothetical, and worth a look even if you skim the rest. .github/dependabot.yml documents that Dependabot converges uses: pins onto the referenced repo's latest tag, mint's latest tag is v0.5.0, and v0.5.0 predates #30 and does not define assets-artifact at all. So the weekly grouped actions bump is a live path straight back to this bug.

release_workflow_test.ts guards the predicate that matters: a 40-hex commit SHA passes, and a tag passes only from v0.6.0 up. That blocks the revert without standing in the way of the real re-pin later. If you'd rather belt-and-braces it, a Dependabot ignore entry for the mint pin until v0.6.0 would do it too.

Tests

release_workflow_test.ts reads the workflows as fixtures rather than asserting on a run, so a regression fails on the PR instead of six weeks later on a tag that ships an empty and now-immutable release. It asserts the needs:, that the uploaded artifact name matches the one requested, that actions: read is granted, that no other workflow writes to the release at all, and the pin floor above.

Each was mutation-checked — every one of these fails the suite, and the legitimate future re-pin does not:

mutation result
drop needs: binaries ❌ fails
rename the uploaded artifact (mismatch) ❌ fails
drop actions: read ❌ fails
drop assets-artifact ❌ fails (2 tests)
reintroduce a release-writing binaries.yml ❌ fails
bump the mint pin to v0.5.0 (Dependabot's move) ❌ fails
re-pin to v0.6.0 (the legitimate future bump) ✅ passes

Full local run, matching ci.yml: deno task check, deno lint, deno fmt --check, deno task test 12/12, and the dogfood drift check still 21/21 symbols.

The new test sits at the repo root rather than in src/ (it tests CI, not the published surface), so it is added to deno.json's fmt/lint includes to stay covered. publish.exclude already drops **/*_test.ts.

Incidental

The v* trigger now also builds binaries for prerelease tags, which binaries.yml's stricter v[0-9]+.[0-9]+.[0-9]+ filter excluded. release.yml already ran provenance on v*, so this aligns them rather than widening anything new.

The backfill needs your call — I did not do it

The issue also asks to backfill the v0.2.0 binaries. That cannot be done as asked: both v0.2.0 and v0.3.0 are already published and immutable: true, which is the very condition that caused the 422. There is no upload that attaches to them. The options, and none of them is mine to pick:

  1. Let it ride (recommended). Merge this; the next tag ships binaries. The patch intent in .release/ resolves 0.3.0 → 0.3.1, so cutting that gets users binaries promptly without touching immutable history.
  2. Delete and recreate the v0.2.0/v0.3.0 releases. Destructive and outward-facing: it discards the published releases and their existing signed provenance association, and any binaries would be compiled now rather than at release time, so they would not be covered by the original attestation. I'd want that decision to be explicit.
  3. Leave v0.2.0/v0.3.0 as they are and note in the README that binaries start at 0.3.1.

Happy to implement whichever you pick.

Manual, not attempted here

Nothing in [settings]/[org] is required for this. If you want option 2, that is a maintainer-run release deletion.


Generated by Claude Code

A published GitHub release is immutable, so whichever workflow publishes it
first locks every other one out. binaries.yml and release.yml were both
triggered by the v* tag, and two tag-triggered workflows are unordered by
construction — so mint's release job published the release before binaries.yml
could attach, and the upload died with

    HTTP 422: Cannot upload assets to an immutable release.

That is how v0.2.0 AND v0.3.0 both shipped with no binaries. Both workflows
were written create-or-upload to be "order-independent", which is correct for
mutable releases and precisely wrong for immutable ones — order-independence is
what puts the publish first half the time.

Fix it with one writer, the path mint's #30 added for exactly this: fold
binaries.yml into release.yml as a `needs:` job that uploads a workflow
artifact, and hand the artifact name to release-provenance via the new
`assets-artifact` input. It downloads it from the same run, attaches it next to
the provenance, and publishes once. Nothing races for the release.

Pin notes, since the pair looks mismatched on purpose:

  - the workflow pin moves to mint main @ #30 (576e904), the commit carrying
    the draft-until-attached fix. No mint tag contains it yet — v0.5.0 predates
    it — so a SHA is the only way to consume it, which is what the org
    convention asks for anyway.
  - `ref` stays at v0.3.1. It selects the mint code that RUNS (`mint attest`),
    and release-provenance.yml was byte-identical from v0.3.1 until #30, so
    this change carries the ordering fix and nothing else. Moving `ref` would
    put drift-gate's signed provenance onto three minors of unrelated mint
    changes. version.yml is on v0.3.1 too; bump both when mint tags.

The binaries job keeps the workflow's default contents: read; only the release
job is granted contents/id-token write plus `actions: read`, which a caller has
to declare itself because a called workflow only gets the intersection.

release_workflow_test.ts guards the shape rather than a run, so a regression
fails on the PR instead of six weeks later on a tag that ships an empty and
now-immutable release. It asserts the `needs:`, that the uploaded artifact name
matches the one requested, that `actions: read` is granted, that no other
workflow writes to the release at all, and that the mint pin has not regressed
below the fix.

That last one is not hypothetical. Dependabot converges `uses:` pins onto the
referenced repo's LATEST TAG (.github/dependabot.yml spells this out), mint's
latest tag is v0.5.0, and v0.5.0 predates #30 and does not define
`assets-artifact` — so next Monday's grouped "actions" bump is a live path back
to this bug. The guard accepts a 40-hex SHA or a tag from v0.6.0 up, so it
blocks the revert without standing in the way of the real re-pin later.

Incidental: the v* trigger now also builds binaries for prerelease tags, which
binaries.yml's stricter v[0-9]+.[0-9]+.[0-9]+ filter excluded. release.yml
already ran provenance on v*, so this aligns them.

Does NOT backfill the v0.2.0/v0.3.0 binaries the issue also asks for: both
releases are already published and immutable, so nothing can be attached to
them. See the PR for the options — it needs a maintainer decision.

Refs bounded-systems/mint#30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P74tnmfWfpq5PaZ67BKsh

Copy link
Copy Markdown
Contributor Author

CI status: deno, version / version-check, add and CodeQL all green. osv / osv-scan is red, and it is not from this change — it fails identically on main at 6e3ccba, the commit this branch was cut from:

Total 1 package affected by 1 known vulnerability (0 Critical, 1 High, 0 Medium, 0 Low)
GHSA-rgw5-rvv9-x895  CVSS 7.5  npm  brace-expansion  5.0.8 → fixed in 5.0.9   (deno-lock.cdx.json)

brace-expansion is transitive (ts-morphtinyglobbyminimatch), and this PR touches no dependency: the only non-workflow edits are deno.json's fmt/lint include lists. So it needs a lockfile bump, which is its own change rather than something to fold in here — worth its own ticket, especially since .github/dependabot.yml only runs the github-actions ecosystem, so nothing is bumping the npm/JSR side automatically.

Flagging rather than fixing, since the lane is report-only: false here and will hold the merge gate.

One scope note on this PR: the guard tests read the workflows as fixtures, so CI proves the shape on every PR, but the end-to-end attach can only really be exercised by a real v* tag push. The first tag after merge is the actual proof.


Generated by Claude Code

@bdelanghe
bdelanghe marked this pull request as ready for review August 13, 2026 12:48
@bdelanghe
bdelanghe merged commit 10b8501 into main Aug 13, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Front Desk Aug 13, 2026
bdelanghe pushed a commit that referenced this pull request Aug 13, 2026
Dependabot group update for the github-actions ecosystem: re-pins the
actions used across ci.yml, deps.yml, front-desk-add.yml, publish-jsr.yml,
release.yml and version.yml to their new commit SHAs.

Pin bumps only — no workflow behavior changes. Supersedes #15, which was
closed after #16 removed binaries.yml out from under it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

binaries job fails on release: 422 Cannot upload assets to an immutable release

2 participants