From 2b351fbd776c6bdb4fa373701cbbab14b86b9dc8 Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 8 Oct 2026 17:22:44 -0500 Subject: [PATCH] docs(release): describe a formula that lands without a bottle The tap installs and tests the `agentnative` formula from the archives a release publishes and lands the bump as one commit, with no bottle. The release documents described a bottle build, a bottle upload back to this repository's release, and a bottle attestation check. - `RELEASES.md` and `RELEASES-RATIONALE.md` state what the tap does with a release and why the release stays not-latest until the formula lands. The Homebrew rollback reverts one commit. - `RELEASES-POSTFLIGHT.md` names the tap workflow `Publish formula`, checks an archive install where it checked a bottle, and drops the `brew verify` gate, which reads bottles. - `README.md` says the formula installs the archive by checksum and points at the tap's "Verifying an archive". - `scripts/SYNCS.md` shows the tap downloading and verifying the release's archives, with nothing uploaded back. - The header comments of `release.yml` and `finalize-release.yml` give the pipeline as it runs, and say the `build` job is the one place a release compiles. --- .github/workflows/finalize-release.yml | 2 +- .github/workflows/release.yml | 10 +++++++--- README.md | 8 +++++--- RELEASES-POSTFLIGHT.md | 23 ++++++++++------------- RELEASES-RATIONALE.md | 11 ++++++----- RELEASES.md | 13 +++++++------ scripts/SYNCS.md | 16 ++++++++-------- 7 files changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml index a150a4f..35bb27f 100644 --- a/.github/workflows/finalize-release.yml +++ b/.github/workflows/finalize-release.yml @@ -2,7 +2,7 @@ # Copy to .github/workflows/finalize-release.yml in the tool repo. # # Triggered by repository_dispatch from homebrew-tap's publish workflow -# after bottles are uploaded. Publishes the draft GitHub Release. +# after the formula bump lands on the tap. Marks the GitHub Release latest. name: Finalize Release on: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad8f5b1..7b1dad4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,13 @@ # crate: agentnative (crates.io name) | bin: anc (installed executable) # # Pipeline: check-version -> build (7 targets) -> attest (build provenance, and a -# CycloneDX SBOM of the binary) -> crates.io -> release (non-draft) -> verify the -# published files against their attestations -> homebrew bottle build -> bottles -# upload back to release assets -> finalize-release marks the release final. +# CycloneDX SBOM of the binary) -> crates.io -> release (visible, not latest) -> +# verify the published files against their attestations -> homebrew-tap tests the +# formula bump from the archives and lands it -> finalize-release marks the +# release latest. +# +# The `build` job is the only place a release compiles its binaries. The tap +# installs the published archives and builds no bottle. name: Release on: diff --git a/README.md b/README.md index e70ec9a..cdd0138 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,11 @@ cargo binstall agentnative # https://github.com/brettdavies/agentnative-cli/releases ``` -The Homebrew formula installs the pre-built archive the release publishes for your platform and compiles nothing. The -tap signs the bottles it builds from those archives, and `brew verify brettdavies/tap/agentnative` checks one against -that attestation. +The Homebrew formula installs the pre-built archive the release publishes for your platform, checked against the +checksum the formula pins, and compiles nothing. The tap pins a checksum only after it has verified the archive against +the release's build-provenance attestation; [Verifying an +archive](https://github.com/brettdavies/homebrew-tap#verifying-an-archive) in the tap's README has the command that +repeats the check. Each release archive carries a build-provenance attestation and an attested SBOM, signed by the release workflow. To check that an archive you downloaded was built by it, from this repository: diff --git a/RELEASES-POSTFLIGHT.md b/RELEASES-POSTFLIGHT.md index ad10533..0b741cf 100644 --- a/RELEASES-POSTFLIGHT.md +++ b/RELEASES-POSTFLIGHT.md @@ -30,7 +30,7 @@ Sub-commands let you re-run one verification in isolation: | Sub-command | What it checks | Source of truth | | ------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | | `release` | `release.yml` on the tag push: `gh run view ... --json conclusion` is `"success"` | `gh run view` | -| `tap` | `brettdavies/homebrew-tap` `update-formula` (repository_dispatch) + `Publish bottles` (workflow_run) SUCCESS | `gh run list -R brettdavies/homebrew-tap` | +| `tap` | `brettdavies/homebrew-tap` `update-formula` (repository_dispatch) + `Publish formula` (workflow_run) SUCCESS | `gh run list -R brettdavies/homebrew-tap` | | `finalize` | `finalize-release.yml` callback ran in this repo (cross-repo dispatch loop closed) | `gh run list -e repository_dispatch` | | `make-latest` | GitHub Release `vX.Y.Z` is non-draft, non-prerelease, and `releases/latest` resolves to it | `gh api /releases/latest` | | `crates` | `crates.io` shows `agentnative vX.Y.Z` published | `crates.io` index API | @@ -56,10 +56,10 @@ Run immediately after the tag push triggers `release.yml`. Trusted Publishing, and dispatches `update-formula` into the homebrew-tap. Run `scripts/release/postflight.sh release` for the automated check. - [ ] **Homebrew-tap dispatch landed.** `gh run list -R brettdavies/homebrew-tap --limit 5` should show a recent - `update-formula` (event=repository_dispatch) and a `Publish bottles` (event=workflow_run) both SUCCESS. The bottles - workflow auto-merges the formula bump PR and pushes an `agentnative: add bottle.` commit to tap `main`. Run - `scripts/release/postflight.sh tap` for the automated check. -- [ ] **`finalize-release.yml` callback ran.** After the bottles publish, the tap dispatches back to this repo and the + `update-formula` (event=repository_dispatch) and a `Publish formula` (event=workflow_run) both SUCCESS. The publish + workflow pushes the formula bump to tap `main` as one `chore(agentnative): bump to v` commit, with no bottle: + the formula installs this release's archives. Run `scripts/release/postflight.sh tap` for the automated check. +- [ ] **`finalize-release.yml` callback ran.** After the formula lands, the tap dispatches back to this repo and the callback flips the GitHub Release `make_latest: true`. Check `gh run list -e repository_dispatch --limit 3`; expect a `finalize-release` SUCCESS. Run `scripts/release/postflight.sh finalize` for the automated check. - [ ] **GitHub Release marked latest.** `gh api repos/brettdavies/agentnative-cli/releases/latest --jq .tag_name` @@ -70,14 +70,11 @@ Run immediately after the tag push triggers `release.yml`. - [ ] **`cargo install agentnative --version ` on a clean environment** resolves and runs. Drive on a fresh container or a sibling machine so the local `~/.cargo/bin` isn't polluted. Confirms the publish landed all package data and the installer can reconstruct `anc` from source. -- [ ] **`brew update && brew install brettdavies/tap/agentnative`** on a fresh prefix resolves the new bottle and - `anc --version` reports the new tag. Drive on a throwaway prefix (`HOMEBREW_PREFIX=/tmp/brew-postflight-X brew ...`). - Confirms the homebrew-tap end of the cross-repo dispatch chain landed cleanly and the published bottle SHA matches the - formula. -- [ ] **Every Homebrew bottle verifies against its attestation.** - `brew verify --os=all --arch=all brettdavies/tap/agentnative` reports `has a valid attestation` for each bottle. The - tap signs the bottles in its `publish.yml`; this is the check Homebrew runs for a user who sets - `HOMEBREW_VERIFY_ATTESTATIONS`, and a failure means that user cannot install the formula. +- [ ] **`brew update && brew install brettdavies/tap/agentnative`** on a fresh prefix downloads this release's archive + for the platform and `anc --version` reports the new tag. Drive on a throwaway prefix + (`HOMEBREW_PREFIX=/tmp/brew-postflight-X brew ...`). Confirms the homebrew-tap end of the cross-repo dispatch chain + landed cleanly and the archive matches the checksum the formula pins. The install compiles nothing and pours no + bottle; the tap verified each archive against this release's attestation before it pinned the checksum. - [ ] **`cargo binstall agentnative`** (without `--version`) resolves to the new tag and installs the matching prebuilt binary. Confirms the GitHub Release asset layout (binary + completions + licenses, expected archive naming) matches binstall's asset-resolution rules and the `[package.metadata.binstall]` overrides in `Cargo.toml`. Drive on a clean diff --git a/RELEASES-RATIONALE.md b/RELEASES-RATIONALE.md index f1e610d..e005df7 100644 --- a/RELEASES-RATIONALE.md +++ b/RELEASES-RATIONALE.md @@ -233,11 +233,12 @@ publish (`v0.1.0`) requires a regular crates.io API token because Trusted Publis ### Why `make_latest: false` then `finalize-release` -The GitHub Release is created visible-but-not-latest (`make_latest: false`) so `cargo-binstall` and `/releases/latest` -don't 404 during the bottle-build window, but the release isn't yet promoted to "Latest" while bottles upload. After the -homebrew-tap workflow uploads bottles to this repo's release assets, it dispatches `finalize-release` back to this repo, -which idempotently flips `make_latest: true`. End result: crate on crates.io, GitHub Release marked latest, Homebrew -formula updated with bottles, all atomically advertised. +The GitHub Release is created visible-but-not-latest (`make_latest: false`) so its archives resolve by tag at once, +which the tap's bump needs, while `cargo-binstall` and `/releases/latest` keep resolving the previous version until +Homebrew can install the new one. The tap verifies the archives, tests the formula bump on four platforms, and lands +it; it builds no bottle, so nothing is uploaded back to this repo's release. It then dispatches `finalize-release` back +to this repo, which idempotently flips `make_latest: true`. End result: crate on crates.io, GitHub Release marked +latest, Homebrew formula updated, all atomically advertised. ### Why backport `main` → `dev` after publish diff --git a/RELEASES.md b/RELEASES.md index f9960e5..8d53715 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -266,11 +266,11 @@ Always use annotated tags (`-a -m`). The tag push triggers `.github/workflows/re The tap's formula installs this release's archives. Its `update-formula` workflow downloads the four it names (the two `apple-darwin` and the two `linux-musl` archives), verifies each against the attestation `attest` made, and pins its checksum; an archive with no attestation stops the bump, so `attest: true` in `release.yml` is what lets a release reach -Homebrew. The tap then builds bottles from those archives, signs them in its own `publish.yml`, and uploads them to this -repo's release assets. +Homebrew. The tap then installs and tests the formula from those archives on four platforms and lands the bump on its +`main`. It builds no bottle: the binaries are compiled once, by `release.yml`, and `brew install` downloads the archive. -After the homebrew-tap workflow uploads bottles to this repo's release assets, it dispatches `finalize-release` back to -this repo, which idempotently flips `make_latest: true`. +After the homebrew-tap workflow lands the formula bump, it dispatches `finalize-release` back to this repo, which +idempotently flips `make_latest: true`. → Rationale (`make_latest` flow, musl hard-block, annotated-tag gotcha): [`RELEASES-RATIONALE.md` § Release pipeline](./RELEASES-RATIONALE.md#release-pipeline). @@ -504,9 +504,10 @@ cargo yank --version "${BAD#v}" agentnative gh release edit "$PREV" --latest gh release edit "$BAD" --prerelease -# Homebrew: revert the formula bump on the tap so `brew install` resolves the last-good bottle. +# Homebrew: revert the formula bump on the tap so `brew install` resolves the last-good version. gh api repos/brettdavies/homebrew-tap/commits --jq '.[0:5][] | .sha[0:7] + " " + .commit.message' -# then revert the `agentnative: add bottle.` and formula-bump commits via a PR to the tap's main. +# then revert the formula-bump commit via a PR to the tap's main. A release the tap published with a +# bottle (0.6.0 and earlier) also has an `agentnative: add bottle.` commit to revert first. ``` Un-yank with `cargo yank --undo --version agentnative` if the yank was wrong. A yanked crate version cannot be diff --git a/scripts/SYNCS.md b/scripts/SYNCS.md index e7cbd0a..7757bc5 100644 --- a/scripts/SYNCS.md +++ b/scripts/SYNCS.md @@ -22,7 +22,7 @@ flowchart LR subgraph Outbound["Outbound — data OUT of this repo"] SITE_COV["agentnative-site
coverage-matrix.json"] SITE_SCORE["agentnative-site
per-tool scorecards"] - TAP["brettdavies/homebrew-tap
(formula + bottles)"] + TAP["brettdavies/homebrew-tap
(formula)"] CRATES["crates.io
(agentnative crate)"] end @@ -57,8 +57,8 @@ flowchart LR | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `brettdavies/agentnative-site` (`/coverage` page) | site's `scripts/sync-coverage-matrix.sh` `cp`s from `$ANC_ROOT/coverage/matrix.json` (default `$HOME/dev/agentnative-cli`) → `src/data/coverage-matrix.json` | `coverage/matrix.json` (`schema_version: "1.0"`), generated here by `anc emit coverage-matrix`, committed as a tracked artifact (not gitignored) | Run on the site after this repo bumps the matrix (new audit, registry change, or `Audit::covers()` change). | This repo's CI (via `cargo test`) runs `test_generate_coverage_matrix_drift_check_passes_on_committed_artifacts`, which invokes `anc emit coverage-matrix --check` and exits non-zero when `docs/coverage-matrix.md` or `coverage/matrix.json` disagree with the registry. Site has no automated drift check; the cli-side gate is authoritative. | | `brettdavies/agentnative-site` (per-tool scorecards) | site's `scripts/regen-scorecards.sh` runs `anc audit --command [--audit-profile ] --output json` against each registry entry; writes `scorecards/-v.json` in the site repo | Per-tool scorecard JSONs (`schema_version: "0.5"`); the `anc` binary embeds `spec_version` at compile time (sourced from the vendored `src/principles/spec/VERSION`) | Run on the site after `anc` is upgraded on the box (`brew upgrade brettdavies/tap/agentnative`); also run on registry changes. Script enforces `MIN_ANC_VERSION` (currently `0.1.3`) unless `--allow-dev-build` is passed. | Site validates schema 0.5 invariants at build time (`bun test` + `bun run build`). Filename owns the canonical version anchor: the actually-installed `anc --version` determines the output filename, so a filename can never lie about which release was scored. | -| `brettdavies/homebrew-tap` (formula bump) | `.github/workflows/release.yml` → reusable `brettdavies/.github/.github/workflows/rust-release.yml@main` → `homebrew` job fires `repository_dispatch` (`event_type=update-formula`, payload: formula=`agentnative`, version=`X.Y.Z`, repo) | Triggers homebrew-tap to bump the `agentnative` formula and build bottles | On every `git tag v*.*.*` push to this repo. Authenticated via `CI_RELEASE_TOKEN` (fine-grained PAT with Contents R+W). | n/a at this boundary. Bottle-build success is observable via the homebrew-tap workflow run; bottle-upload back to this repo's Release assets is what triggers the inverse `finalize-release` dispatch (next row). | -| `brettdavies/agentnative-cli` (this repo's own `finalize-release.yml`) | Inverse `repository_dispatch` from homebrew-tap's publish workflow (`event_type=finalize-release`) | Bottle SHAs uploaded to this Release's assets; `make_latest` flips from `false` → `true` on the GitHub Release | Fired by homebrew-tap after bottles upload. Idempotent; re-dispatch is safe. | n/a (the flip is observable on the Release page). | +| `brettdavies/homebrew-tap` (formula bump) | `.github/workflows/release.yml` → reusable `brettdavies/.github/.github/workflows/rust-release.yml@main` → `homebrew` job fires `repository_dispatch` (`event_type=update-formula`, payload: formula=`agentnative`, version=`X.Y.Z`, repo) | Triggers homebrew-tap to bump the `agentnative` formula and test it | On every `git tag v*.*.*` push to this repo. Authenticated via `CI_RELEASE_TOKEN` (fine-grained PAT with Contents R+W). | n/a at this boundary. The bump's success is observable via the homebrew-tap workflow run; bottle-upload back to this repo's Release assets is what triggers the inverse `finalize-release` dispatch (next row). | +| `brettdavies/agentnative-cli` (this repo's own `finalize-release.yml`) | Inverse `repository_dispatch` from homebrew-tap's publish workflow (`event_type=finalize-release`) | Formula bump landed on the tap's `main`; `make_latest` flips from `false` → `true` on the GitHub Release | Fired by homebrew-tap after the formula bump lands. Idempotent; re-dispatch is safe. | n/a (the flip is observable on the Release page). | | `crates.io` (`agentnative` crate) | Same `release.yml` → `publish-crate` job, `cargo publish` via OIDC Trusted Publishing (no static token after first publish) | The compiled crate at the tag's version | On every `git tag v*.*.*` push. First publish requires `CARGO_REGISTRY_TOKEN` one-time; subsequent publishes are token-less. | `check-version` job gates the pipeline: tag must match `Cargo.toml` `[package].version` exactly, else release aborts before any publish. | ## Release / sync orchestration @@ -83,8 +83,8 @@ The full sync graph clusters around two events: **"new spec tag upstream"** and 2. **Automatic on tag push.** `release.yml` runs `check-version` → `audit` → `build` (5 targets) → `publish-crate` (crates.io OIDC) → `release` (draft GH Release, `make_latest: false`) → `homebrew` (`repository_dispatch` to homebrew-tap). -3. **Automatic, inverse.** homebrew-tap builds bottles, uploads them as assets on this repo's Release, then dispatches - `finalize-release` back to this repo. `finalize-release.yml` flips `make_latest: true` idempotently. +3. **Automatic, inverse.** homebrew-tap tests the formula bump from this Release's archives, lands it with no bottle, + then dispatches `finalize-release` back to this repo. `finalize-release.yml` flips `make_latest: true` idempotently. 4. **Manual post-release.** Run `scripts/sync-dev-after-release.sh vX.Y.Z` and merge the PR it opens against `dev`. Backports the release bookkeeping (Cargo.toml version, Cargo.lock's workspace entries, CHANGELOG.md from main) and every release-prep edit `main` made since the previous release tag, so future builds from `dev` report the released @@ -113,8 +113,8 @@ sequenceDiagram CRATES-->>REL: published REL->>CLI: create draft GH Release (make_latest: false) REL->>TAP: repository_dispatch (event_type=update-formula) - TAP->>TAP: bump formula + build bottles - TAP->>CLI: upload bottle assets to Release + TAP->>CLI: download + verify the Release's archives + TAP->>TAP: bump formula, test it, land it (no bottle) TAP->>FIN: repository_dispatch (event_type=finalize-release) FIN->>CLI: flip make_latest: true (idempotent) FIN-->>Maintainer: Release is now "latest" @@ -131,7 +131,7 @@ sequenceDiagram | cli → site (scorecards) | manual on the site side | | cli → crates.io | automatic on `v*` tag push | | cli → homebrew-tap (formula) | automatic on `v*` tag push | -| homebrew-tap → cli (`finalize-release`) | automatic on bottle upload | +| homebrew-tap → cli (`finalize-release`) | automatic once the formula bump lands | | cli main → cli dev (`sync-dev-after-release.sh`) | manual after `finalize-release` publishes | ## Reference