feat(ci): assert a release tag names one exact revision #86

Merged
natsukium merged 4 commits from feat/release-gate-18 into main 2026-09-04 21:02:00 +09:00
Owner

Fixes #18.

A release tag now has to prove it names one exact revision before anything
is published. scripts/release/verify.sh checks the tag's shape and object
(annotated, semver, a recognized prerelease suffix), that the manifests agree
with the tag name, that the working tree is clean, and that the built
artifact reports the same version and the same revision it was cut at; a
hermetic --self-test covers every accept and reject case. .forgejo/workflows/release.yml
publishes from the tag only after that gate passes, and the PR workflow plus
justfile expose the same checks locally.

Doc cascade: docs/reference/workspace.md (the release gate's normative rules),
docs/explanation/testing.md and docs/reference/testing.md (where the self-test
sits in the verification layers), docs/how-to/install.md, docs/backlog.md
(the follow-ups below), docs/explanation/architecture/ipc.md, scripts/README.md,
CLAUDE.md and the implement-feature skill.

This branch also carries a one-line fix to crates/felis-transport/src/driver/tests.rs:
two changes landed on main in the same window (Conn::Welcome grew the typed
identity field, and the phase-ladder refactor added a case building a Welcome
by hand), so main does not compile its test target. The same fix is on the #23
branch; whichever lands first makes the other a no-op.

Verified: just check green (fmt, clippy, nextest, deny); reviewed by plan/correctness/docs lenses and pi sol

Deferred

  • Release-API cross-check of proto-baseline/ against the latest published final release's felis.proto — no acceptance criterion needs it; it is #19's follow-up, needs a release API call plus a token, and first-release is the correct answer while no final release exists.
  • Committing the new proto-baseline/ to main after a final release publishes — a CI push to main needs push-scoped credentials and contradicts the repo's deliberate-human-step stance; left as the human step the backlog now names.
  • Rewriting the README.md and docs/how-to/install.md install commands to git+https://.../felis?ref=refs/tags/v0.1.0 — the tag does not exist until #31 pushes it; documenting an unresolvable ref would break every install command today.
  • A windows-test/windows-smoke/windows-package leg in release.yml — the plan itself recommends leaving it out for v0.1.0, and P-2 keeps the support claim off a run artifact.
  • The rc-tag rehearsal runs on Forgejo (v0.1.0-rc.1 accept, v0.1.0-rc.2 mismatch reject) with run URLs recorded in #31 — needs tag pushes to the remote and a RELEASE_TOKEN Actions secret that does not exist yet; the hermetic self-test covers the same accept/reject cases locally.
  • Creating the RELEASE_TOKEN Actions secret with release-write scope — repo infrastructure outside the tree; the workflow reads it but cannot provision it.
  • Fixing the crates/build-common.rs treefmt drift — inherited from the base branch feat/build-identity-17; fixing it in a stacked branch would conflict with #17.
  • Release-API baseline check from plan amendments 5 and 6 is absent, so first-release stays valid forever — same deferral as above: no acceptance criterion requires it and the API/token dependency doesn't exist yet.
  • Primary install examples still name mutable main, and no CHANGELOG entry records the change — the tag doesn't exist until #31 pushes it, and this is scope prose, not one of the five checkbox acceptance criteria.
  • The identity gate accepts a JSON string "false" as a clean build — speculative hardening against a hypothetical serializer; today's Rust serializer always emits a real boolean.
  • The release gate's CHANGELOG assertion is never exercised as a negative case — a request for extra negative-case coverage, not a criterion violation.
  • A non-40-char FELIS_GIT_HASH makes felis --version and felis version's cli row name different builds — hypothetical packager-supplied short-hash scenario; no acceptance criterion requires validating this env var's shape, and the flake passes the full hash.
Fixes #18. A release tag now has to prove it names one exact revision before anything is published. `scripts/release/verify.sh` checks the tag's shape and object (annotated, semver, a recognized prerelease suffix), that the manifests agree with the tag name, that the working tree is clean, and that the built artifact reports the same version and the same revision it was cut at; a hermetic `--self-test` covers every accept and reject case. `.forgejo/workflows/release.yml` publishes from the tag only after that gate passes, and the PR workflow plus `justfile` expose the same checks locally. Doc cascade: `docs/reference/workspace.md` (the release gate's normative rules), `docs/explanation/testing.md` and `docs/reference/testing.md` (where the self-test sits in the verification layers), `docs/how-to/install.md`, `docs/backlog.md` (the follow-ups below), `docs/explanation/architecture/ipc.md`, `scripts/README.md`, `CLAUDE.md` and the `implement-feature` skill. This branch also carries a one-line fix to `crates/felis-transport/src/driver/tests.rs`: two changes landed on main in the same window (`Conn::Welcome` grew the typed `identity` field, and the phase-ladder refactor added a case building a `Welcome` by hand), so main does not compile its test target. The same fix is on the #23 branch; whichever lands first makes the other a no-op. Verified: just check green (fmt, clippy, nextest, deny); reviewed by plan/correctness/docs lenses and pi sol ## Deferred - Release-API cross-check of `proto-baseline/` against the latest published final release's `felis.proto` — no acceptance criterion needs it; it is #19's follow-up, needs a release API call plus a token, and `first-release` is the correct answer while no final release exists. - Committing the new `proto-baseline/` to main after a final release publishes — a CI push to main needs push-scoped credentials and contradicts the repo's deliberate-human-step stance; left as the human step the backlog now names. - Rewriting the README.md and docs/how-to/install.md install commands to `git+https://.../felis?ref=refs/tags/v0.1.0` — the tag does not exist until #31 pushes it; documenting an unresolvable ref would break every install command today. - A windows-test/windows-smoke/windows-package leg in release.yml — the plan itself recommends leaving it out for v0.1.0, and P-2 keeps the support claim off a run artifact. - The rc-tag rehearsal runs on Forgejo (v0.1.0-rc.1 accept, v0.1.0-rc.2 mismatch reject) with run URLs recorded in #31 — needs tag pushes to the remote and a RELEASE_TOKEN Actions secret that does not exist yet; the hermetic self-test covers the same accept/reject cases locally. - Creating the RELEASE_TOKEN Actions secret with release-write scope — repo infrastructure outside the tree; the workflow reads it but cannot provision it. - Fixing the `crates/build-common.rs` treefmt drift — inherited from the base branch feat/build-identity-17; fixing it in a stacked branch would conflict with #17. - Release-API baseline check from plan amendments 5 and 6 is absent, so `first-release` stays valid forever — same deferral as above: no acceptance criterion requires it and the API/token dependency doesn't exist yet. - Primary install examples still name mutable `main`, and no CHANGELOG entry records the change — the tag doesn't exist until #31 pushes it, and this is scope prose, not one of the five checkbox acceptance criteria. - The identity gate accepts a JSON string `"false"` as a clean build — speculative hardening against a hypothetical serializer; today's Rust serializer always emits a real boolean. - The release gate's CHANGELOG assertion is never exercised as a negative case — a request for extra negative-case coverage, not a criterion violation. - A non-40-char `FELIS_GIT_HASH` makes `felis --version` and `felis version`'s cli row name different builds — hypothetical packager-supplied short-hash scenario; no acceptance criterion requires validating this env var's shape, and the flake passes the full hash.
A tag is the only durable name a build can be installed by, but nothing
checked that a tag meant what it claims: it could be lightweight, point
at a commit other than the one being built, disagree with the version
every crate manifest carries, or be cut from a dirty tree, and the
publication would go out anyway.

scripts/release/verify.sh is the gate for all of that, plus an
--identity mode that compares the built binary's typed identity
(`felis version --format json`: semver, full revision, dirty) with the
tag. The JSON object rather than `felis --version`, whose revision is
abbreviated for a reader and so is not a parse target.

`just release-check <tag>` runs it before a tag is pushed, and its
hermetic self-test joins `just check` and pr.yml's build job, since the
gate itself runs only on tags and a break in it would otherwise surface
at the tag push.

Refs #18
CI validated mutable branch revisions only: every green main push put a
store path in the cache, and nothing tied "which build did I install"
to a name that cannot move. release.yml triggers on `v*` tags and is
the only workflow that creates a release page.

It reruns the whole gate on the tag rather than promoting the main
push's store path. Forgejo's `needs:` cannot reach across workflow
files, so promotion could only trust a cache entry by hash, and a tag
can be pushed at any commit; rebuilding buys a `needs:` edge from the
publication to its evidence, and the derivation is content-addressed,
so an unchanged input set pushes the same path.

What ships is a description of one revision: the store path with its
narHash and flake reference, the config schema, and felis.proto. The
Windows zip stays off the page, because a run artifact is not a
distribution and attaching it would read as one.

Refs #18
The docs described publication as a per-main-revision cache push with
promotion left to a human, which is no longer what the tree does. The
reference twin gains the normative list (trigger, the six verify-tag
assertions, the identity check before the push, the three attachments);
install.md gains what the release page holds and how to install a tag
rather than main; the explanation twin records why a tag rebuilds
instead of promoting, and why the Windows zip is left off.

The wire-gate backlog item narrows: its tag path now runs on every tag,
and what remains is the release-API half.

Refs #18
fix(transport): give the phase test's Welcome its typed identity
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 46s
bench / Criterion regression gate (pull_request) Successful in 1m54s
windows / cargo nextest (Windows) (pull_request) Successful in 5m57s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m4s
pr / nix flake check (pull_request) Successful in 25s
pr / wire schema is compatible with the base (pull_request) Successful in 9s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m35s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 52s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 12s
windows / frontend smoke (Windows) (pull_request) Successful in 1m47s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
81d0e4940a
Two changes landed on main in the same window: `Conn::Welcome` grew the
typed `identity` field, and the phase-ladder refactor added a
classification case that builds a `Welcome` by hand. Neither branch saw
the other, so main does not compile its test target; this branch only
inherits the break.

The case pins where `Conn::Welcome` is legal, not what it carries, so
the field takes `None` — the value a daemon below the identity minor
sends.
natsukium deleted branch feat/release-gate-18 2026-09-04 21:02:01 +09:00
Sign in to join this conversation.
No description provided.