[v0.1/P0] Add a tag-driven immutable release artifact gate #18

Closed
opened 2026-09-03 16:18:01 +09:00 by natsukium · 1 comment
Owner

Parent: #12 (P0.3). Related to #9.

Why

CI currently validates mutable branch revisions, no release tag exists, and the only durable support claim should be the gated x86_64-linux artifact. A first release needs an immutable source-to-artifact identity.

Scope

  • Trigger release publication from an annotated v0.1.0 tag.
  • Verify the tag points to the intended clean release commit.
  • Rebuild or promote only that exact revision.
  • Publish immutable source and schema references plus the gated Linux artifact.
  • Keep the Windows zip as CI evidence rather than a distribution claim.
  • Make release installation examples name a tag or revision, never mutable main.

Acceptance criteria

  • A tag/revision mismatch or dirty release identity fails publication.
  • The published artifact can be traced to the tag and build identity.
  • Only x86_64-linux is advertised as supported for v0.1.0.
  • Release docs identify all exact-revision gates and artifact contents.
  • The workflow is implemented in Forgejo Actions, not GitHub Actions.
Parent: #12 (P0.3). Related to #9. ## Why CI currently validates mutable branch revisions, no release tag exists, and the only durable support claim should be the gated `x86_64-linux` artifact. A first release needs an immutable source-to-artifact identity. ## Scope - Trigger release publication from an annotated `v0.1.0` tag. - Verify the tag points to the intended clean release commit. - Rebuild or promote only that exact revision. - Publish immutable source and schema references plus the gated Linux artifact. - Keep the Windows zip as CI evidence rather than a distribution claim. - Make release installation examples name a tag or revision, never mutable `main`. ## Acceptance criteria - [ ] A tag/revision mismatch or dirty release identity fails publication. - [ ] The published artifact can be traced to the tag and build identity. - [ ] Only `x86_64-linux` is advertised as supported for v0.1.0. - [ ] Release docs identify all exact-revision gates and artifact contents. - [ ] The workflow is implemented in Forgejo Actions, not GitHub Actions.
Author
Owner

Triage plan (2026-09-03)

Source-grounded triage against main at 69076d42, reviewed through seven rounds of an independent reviewer (pi sol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12. Where a "Review amendments" section below conflicts with an earlier section, the amendment is the decision.

Claim check

Accurate at HEAD.

  • No release tag exists: git tag is empty. CHANGELOG.md:14 has a single ## [Unreleased] header and CHANGELOG.md:11-12 says everything accrues there until a first tag.
  • CI validates mutable branch revisions only: .forgejo/workflows/pr.yml:11-15 triggers on push: branches: [main] and pull_request; there is no tags: trigger in any of pr.yml, build.yml, bench.yml, fuzz.yml, kani.yml. linux-publish (pr.yml:415-477) pushes nix build .#felis to the niks3 cache on every green main push (if: github.event_name == 'push', :452); windows-package (pr.yml:301-364) uploads a zip as a 14-day run artifact. build.yml:8-32 and docs/reference/workspace.md:271-300 state the per-revision rule explicitly and say promotion "is a deliberate human step" (pr.yml:312-317, docs/reference/testing.md:1140-1146).
  • Only x86_64-linux should be advertised: already the documented stance — docs/reference/workspace.md:276-281 and docs/how-to/install.md:130-146 (Windows: "the support claim stays off until there is somewhere durable to download it from"). The issue's acceptance item is therefore already true in prose; the work is to keep it true in the release page/notes.
  • Install examples name mutable main: README.md:20,23, docs/how-to/install.md:16,19,36,49 all use git+https://git.natsukium.com/natsukium/felis with no ?ref=/?rev=; docs/how-to/update-felis.md:26 is nix profile upgrade felis (follows whatever ref was installed). install.md:143 and :154 describe the cache as holding "each main revision that passes".
  • Identity available to the gate: Nix builds already stamp shortRev/dirtyShortRev (flake.nix:251, nix/package.nix:32,78), so a dirty tag checkout is detectable today, but nothing checks it, and the binary reports no semver/dirty in a typed way (see #17 report). nix/package.nix:59 single-sources the package version from crates/felis-cli/Cargo.toml, which is where the tag ↔ version comparison should anchor.
  • Forgejo, not GitHub: all existing workflows are Forgejo Actions with commit-pinned uses: (pr.yml:28, :360); the artifact upload already uses Forgejo's upload-artifact fork. There is no Forgejo release-creation action in the tree, so publishing to a release page needs the Forgejo API (fj skill / curl with a token) or the forgejo-release action — a new uses: that per docs/reference/testing.md "CI shape" must be case-by-case pinned.

Nothing in the three post-snapshot commits touches this.

Verdict

accept-with-changes. Keep the scope, but narrow "rebuild or promote only that exact revision" to rebuild from the tag in a separate release.yml and verify identity, rather than trying to promote the earlier main push's store path: Forgejo cannot needs: across workflow runs (build.yml:21-24, pr.yml:424-427 already record that constraint), so "promote" would have to trust an external cache entry by hash. A rebuild from the tag inside the release workflow, gated on the same suite + smoke jobs, is the only shape that carries the per-revision rule as a needs: edge. The output store path is content-addressed by the same derivation, so the bytes match the main push's if the inputs did.

Also narrow "publish immutable source and schema references" to what Forgejo can host durably: a Forgejo release on the tag with (a) the felis-config.schema.json, (b) felis.proto, (c) the Linux closure as nix copy --to file:// NAR or simply the store path + narHash recorded in the release notes, and (d) the Windows zip attached labelled as CI evidence, not a supported download, per the tracker's decision. The flake reference git+https://…/felis?ref=refs/tags/v0.1.0 is itself the immutable source reference; no tarball is needed.

Principles: nothing user-facing beyond the release page; principle 1/4 pass (the change removes the ambiguity of "which main did I install").

Approach

.forgejo/workflows/release.yml (new)

  • on: push: tags: ['v*']. Job chain: verify-tagbuild (same steps as pr.yml:31-43) → linux-smokelinux-publishrelease. Reuse by copying the job bodies; Forgejo has no reusable-workflow uses: for local workflows that is worth depending on, and the comment block in pr.yml already documents why each step exists — reference it rather than repeat it.
  • verify-tag (runs on nix, fetch-depth: 0pr.yml checkouts are depth 1, only bench.yml:68 fetches full history):
    1. git cat-file -t "$GITHUB_REF_NAME" is tag (annotated), and git rev-parse "$GITHUB_REF_NAME^{}" equals github.sha.
    2. Tag name v<semver> equals (lib.importTOML crates/felis-cli/Cargo.toml).package.version (the source nix/package.nix:59 already reads) and equals every workspace crate's version (they are set per crate, crates/*/Cargo.toml:3, not inherited — see #31 report).
    3. CHANGELOG.md has a ## [<semver>] - <date> header, not only Unreleased.
    4. nix eval --raw .#felis.version matches; nix flake metadata --json | jq .dirtyRevision is null; after build, ./result/bin/felis --version (post-#17: semver + full revision + dirty=false) matches the tag's commit. Any mismatch fails the job before anything is built for publication.
  • linux-publish: identical to pr.yml:415-477 (niks3 push); it publishes the tag build to the same cache — the cache is keyed by store path, so a tag build identical to the main build is a no-op push and a different one is simply the new truth.
  • release: creates the Forgejo release on the tag via API (curl -X POST $FORGEJO_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases with secrets.RELEASE_TOKEN, or the commit-pinned https://code.forgejo.org/actions/forgejo-release), attaching crates/felis-client-core/felis-config.schema.json, crates/felis-protocol/proto/felis.proto, a felis-x86_64-linux.txt containing the store path, narHash, and the felis --version line, and the Windows zip only if a windows leg is included (see below). Body: the CHANGELOG section for that version (extract with awk between headers) plus the support matrix line "supported: x86_64-linux only".
  • Windows: either add the windows-test/windows-smoke/windows-package chain to release.yml and attach the zip labelled "CI evidence, unsupported", or leave it out of the release entirely and let the docs point at the pr.yml run artifact. Recommend leave it out for v0.1.0 — fewer runner dependencies for the first tag, and the tracker already says the zip is not a distribution claim.

pr.yml

  • No change to triggers. Consider dropping windows-package's upload for PR-less pushes? No — keep; it is the evidence the docs cite.

justfile

  • just release-check <tag>: the same local assertions as verify-tag (annotated tag, semver match across manifests, CHANGELOG header, clean tree, nix build then felis --version match), so the human cutting the tag (#31) runs the gate before pushing it. Add under a release group heading; keep CI mirrors 1:1 header claim true by naming release.yml.

Docs cascade

  • docs/reference/workspace.md "Build and platform matrix" (:271-300): add a "Release gate" subsection (tag trigger, the four verify assertions, what is attached, what the cache holds) and change the x86_64-linux "Published artifact" cell to say tag builds are the supported artifact and main pushes are pre-release.
  • docs/reference/testing.md:1073-1160 "CI shape": add release.yml to the live-pipeline list.
  • docs/how-to/install.md:16,19,36,49 and README.md:20,23: git+https://git.natsukium.com/natsukium/felis?ref=refs/tags/v0.1.0 (and inputs.felis.url likewise); keep one "track main (unsupported)" example clearly labelled. install.md:143,154 "each main revision" → tag builds are supported, main builds are previews. docs/how-to/update-felis.md:26,99nix profile upgrade and the "first tagged release" sentence.
  • Explanation: docs/explanation/architecture/overview.md or wherever the per-revision rule (P-2) is argued — record why rebuild-in-workflow beat promote-by-hash (Forgejo needs: cannot cross workflows) and why the Windows zip is attached as evidence or omitted; "Revisit if" a Windows durable download exists or Forgejo gains cross-workflow gating.
  • CHANGELOG.md: process change is not user-affecting per CHANGELOG.md:3-8; the install-URL change is (documented commands change) — one "Changed" line.
  • skills/felis: no CLI surface change; nothing to cascade.

Tests that prove it

  • CI itself is the test; do a dry run on a throwaway annotated tag v0.0.0-rc1-style on a branch... Forgejo tag pushes trigger regardless of branch, so use a v0.1.0-rc.1 prerelease tag on the release commit candidate: verify-tag must accept prerelease semver and the release step must mark it prerelease: true. A deliberate mismatch test: push v0.1.0-rc.2 pointing at a commit whose Cargo.toml still says 0.1.0 with the wrong tag name → job fails at step 2. Record both run URLs in #31.
  • Unit-level: the awk CHANGELOG extraction and semver comparison can live in a small scripts/release/ shell file with a bats-free sh self-test invoked by just release-check, or just be simple enough to read.

Dependencies

  • #17 first (identity: semver + full revision + dirty in felis --version), otherwise assertion 4 has nothing typed to compare and "dirty release identity fails publication" cannot be tested end to end. Tracker order (#17 then #18) holds.
  • #31 is the consumer; #18 must be green on an rc tag before #31 pushes the real tag.
  • Secrets/infra outside the repo: a Forgejo token with release write scope stored as an Actions secret, or confirmation that the runner's OIDC identity can call the release API (it cannot today; OIDC is only used for niks3 at pr.yml:438-446).

Risk/effort

M. Mostly YAML and docs, but the first tag run is unrehearsed on Forgejo: release-API auth, fetch-depth for annotated-tag inspection, and prerelease handling are each a likely first-run failure. Mitigate with the rc-tag rehearsal above. Secondary risk: doc drift — five files currently say "each main revision"; run the doc-cascade grep for main revision / git+https:// before closing.

Labels

Keep priority/P0, release/v0.1.0. Suggest editing the scope line "Rebuild or promote only that exact revision" to "Rebuild from the tag inside release.yml" so the implementer does not attempt cross-workflow promotion, and adding the rc-tag rehearsal as an acceptance item.

Review amendments (round 1)

  • Prerelease tags. verify-tag compares the tag's core semver (prerelease suffix stripped) with the manifest version and requires the tag's prerelease suffix, when present, to be well-formed (-rc.N). A prerelease tag publishes with prerelease: true and skips the ## [<semver>] CHANGELOG-header check (which applies to final tags only). The mismatch rehearsal (v0.1.0-rc.2 against a manifest saying a different core version) still fails at step 2. The exact-equality rule applies to final tags.

Review amendments (round 2)

  • Every gate compares the canonical JSON form. Replace each felis --version comparison in verify-tag step 4, the felis-x86_64-linux.txt attachment, and just release-check with felis version --format json (offline for the cli object) and compare .cli.version, .cli.revision (full 40 hex), .cli.dirty == false against the tag's commit. felis --version stays as an offline human smoke line only; no gate parses it.

Review amendments (round 3)

  • release.yml reruns every exact-revision gate itself. The job chain becomes verify-tagcheck (just check: fmt-check + clippy + nextest + cargo-deny) → proto-sync (just proto then git diff --exit-code on src/generated/, plus just schema and a clean diff on the config schema) → proto-compat (#19's job, with the base = the latest earlier v* tag, or, for the first tag, an explicit first-release acknowledgment file that the workflow requires to exist and that #31 deletes afterward) → buildlinux-smokelinux-publishrelease. A prior green main run is not a publication dependency; the tag run is. Rationale to record: a tag can be pushed at any commit, and Forgejo has no cross-workflow needs:, so trusting the main run would be a human procedure, not a gate.

Review amendments (round 4)

  • proto-compat on a tag build compares against the committed release baseline (proto/baseline/, see #19 round 4), never against the latest Git tag; the release job updates the baseline after a successful final publication only. first-release is required exactly while no baseline exists.

Review amendments (round 5)

  • verify-tag gains the release-API baseline check (#19 round 5): the committed baseline must match the latest published final release's felis.proto asset, and first-release is valid only when the API reports no final release.

Review amendments (round 6)

  • Release runs are serialized. release.yml declares concurrency: { group: felis-release, cancel-in-progress: false } so tag runs queue instead of overlapping, covering the whole chain through the post-publication baseline commit. The release-API/baseline check runs twice: in verify-tag and again as the first step of the release job immediately before the publish call, so a release created outside the workflow, or by a queued earlier run, invalidates the result before anything is published.
## Triage plan (2026-09-03) Source-grounded triage against `main` at `69076d42`, reviewed through seven rounds of an independent reviewer (`pi` sol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12. Where a "Review amendments" section below conflicts with an earlier section, the amendment is the decision. ## Claim check Accurate at HEAD. - **No release tag exists**: `git tag` is empty. `CHANGELOG.md:14` has a single `## [Unreleased]` header and `CHANGELOG.md:11-12` says everything accrues there until a first tag. - **CI validates mutable branch revisions only**: `.forgejo/workflows/pr.yml:11-15` triggers on `push: branches: [main]` and `pull_request`; there is no `tags:` trigger in any of `pr.yml`, `build.yml`, `bench.yml`, `fuzz.yml`, `kani.yml`. `linux-publish` (`pr.yml:415-477`) pushes `nix build .#felis` to the niks3 cache on every green `main` push (`if: github.event_name == 'push'`, `:452`); `windows-package` (`pr.yml:301-364`) uploads a zip as a 14-day run artifact. `build.yml:8-32` and `docs/reference/workspace.md:271-300` state the per-revision rule explicitly and say promotion "is a deliberate human step" (`pr.yml:312-317`, `docs/reference/testing.md:1140-1146`). - **Only `x86_64-linux` should be advertised**: already the documented stance — `docs/reference/workspace.md:276-281` and `docs/how-to/install.md:130-146` (Windows: "the support claim stays off until there is somewhere durable to download it from"). The issue's acceptance item is therefore already true in prose; the work is to keep it true in the release page/notes. - **Install examples name mutable `main`**: `README.md:20,23`, `docs/how-to/install.md:16,19,36,49` all use `git+https://git.natsukium.com/natsukium/felis` with no `?ref=`/`?rev=`; `docs/how-to/update-felis.md:26` is `nix profile upgrade felis` (follows whatever ref was installed). `install.md:143` and `:154` describe the cache as holding "each `main` revision that passes". - **Identity available to the gate**: Nix builds already stamp `shortRev`/`dirtyShortRev` (`flake.nix:251`, `nix/package.nix:32,78`), so a dirty tag checkout is *detectable* today, but nothing checks it, and the binary reports no semver/dirty in a typed way (see #17 report). `nix/package.nix:59` single-sources the package version from `crates/felis-cli/Cargo.toml`, which is where the tag ↔ version comparison should anchor. - **Forgejo, not GitHub**: all existing workflows are Forgejo Actions with commit-pinned `uses:` (`pr.yml:28`, `:360`); the artifact upload already uses Forgejo's `upload-artifact` fork. There is no Forgejo release-creation action in the tree, so publishing to a release page needs the Forgejo API (`fj` skill / `curl` with a token) or the `forgejo-release` action — a new `uses:` that per `docs/reference/testing.md` "CI shape" must be case-by-case pinned. Nothing in the three post-snapshot commits touches this. ## Verdict **accept-with-changes.** Keep the scope, but narrow "rebuild or promote only that exact revision" to **rebuild from the tag in a separate `release.yml` and verify identity**, rather than trying to promote the earlier `main` push's store path: Forgejo cannot `needs:` across workflow runs (`build.yml:21-24`, `pr.yml:424-427` already record that constraint), so "promote" would have to trust an external cache entry by hash. A rebuild from the tag inside the release workflow, gated on the same suite + smoke jobs, is the only shape that carries the per-revision rule as a `needs:` edge. The output store path is content-addressed by the same derivation, so the bytes match the `main` push's if the inputs did. Also narrow "publish immutable source and schema references" to what Forgejo can host durably: a Forgejo release on the tag with (a) the `felis-config.schema.json`, (b) `felis.proto`, (c) the Linux closure as `nix copy --to file://` NAR or simply the store path + narHash recorded in the release notes, and (d) the Windows zip attached **labelled as CI evidence, not a supported download**, per the tracker's decision. The flake reference `git+https://…/felis?ref=refs/tags/v0.1.0` is itself the immutable source reference; no tarball is needed. Principles: nothing user-facing beyond the release page; principle 1/4 pass (the change removes the ambiguity of "which `main` did I install"). ## Approach ### `.forgejo/workflows/release.yml` (new) - `on: push: tags: ['v*']`. Job chain: `verify-tag` → `build` (same steps as `pr.yml:31-43`) → `linux-smoke` → `linux-publish` → `release`. Reuse by copying the job bodies; Forgejo has no reusable-workflow `uses:` for local workflows that is worth depending on, and the comment block in `pr.yml` already documents why each step exists — reference it rather than repeat it. - `verify-tag` (runs on `nix`, `fetch-depth: 0` — `pr.yml` checkouts are depth 1, only `bench.yml:68` fetches full history): 1. `git cat-file -t "$GITHUB_REF_NAME"` is `tag` (annotated), and `git rev-parse "$GITHUB_REF_NAME^{}"` equals `github.sha`. 2. Tag name `v<semver>` equals `(lib.importTOML crates/felis-cli/Cargo.toml).package.version` (the source `nix/package.nix:59` already reads) and equals every workspace crate's `version` (they are set per crate, `crates/*/Cargo.toml:3`, not inherited — see #31 report). 3. `CHANGELOG.md` has a `## [<semver>] - <date>` header, not only Unreleased. 4. `nix eval --raw .#felis.version` matches; `nix flake metadata --json | jq .dirtyRevision` is null; after build, `./result/bin/felis --version` (post-#17: semver + full revision + `dirty=false`) matches the tag's commit. Any mismatch fails the job before anything is built for publication. - `linux-publish`: identical to `pr.yml:415-477` (niks3 push); it publishes the tag build to the same cache — the cache is keyed by store path, so a tag build identical to the `main` build is a no-op push and a different one is simply the new truth. - `release`: creates the Forgejo release on the tag via API (`curl -X POST $FORGEJO_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases` with `secrets.RELEASE_TOKEN`, or the commit-pinned `https://code.forgejo.org/actions/forgejo-release`), attaching `crates/felis-client-core/felis-config.schema.json`, `crates/felis-protocol/proto/felis.proto`, a `felis-x86_64-linux.txt` containing the store path, narHash, and the `felis --version` line, and the Windows zip only if a `windows` leg is included (see below). Body: the CHANGELOG section for that version (extract with `awk` between headers) plus the support matrix line "supported: x86_64-linux only". - Windows: either add the `windows-test`/`windows-smoke`/`windows-package` chain to `release.yml` and attach the zip labelled "CI evidence, unsupported", or leave it out of the release entirely and let the docs point at the `pr.yml` run artifact. Recommend **leave it out** for v0.1.0 — fewer runner dependencies for the first tag, and the tracker already says the zip is not a distribution claim. ### `pr.yml` - No change to triggers. Consider dropping `windows-package`'s upload for PR-less pushes? No — keep; it is the evidence the docs cite. ### `justfile` - `just release-check <tag>`: the same local assertions as `verify-tag` (annotated tag, semver match across manifests, CHANGELOG header, clean tree, `nix build` then `felis --version` match), so the human cutting the tag (#31) runs the gate before pushing it. Add under a `release` group heading; keep `CI mirrors 1:1` header claim true by naming `release.yml`. ### Docs cascade - `docs/reference/workspace.md` "Build and platform matrix" (`:271-300`): add a "Release gate" subsection (tag trigger, the four verify assertions, what is attached, what the cache holds) and change the `x86_64-linux` "Published artifact" cell to say tag builds are the supported artifact and `main` pushes are pre-release. - `docs/reference/testing.md:1073-1160` "CI shape": add `release.yml` to the live-pipeline list. - `docs/how-to/install.md:16,19,36,49` and `README.md:20,23`: `git+https://git.natsukium.com/natsukium/felis?ref=refs/tags/v0.1.0` (and `inputs.felis.url` likewise); keep one "track main (unsupported)" example clearly labelled. `install.md:143,154` "each `main` revision" → tag builds are supported, `main` builds are previews. `docs/how-to/update-felis.md:26,99` — `nix profile upgrade` and the "first tagged release" sentence. - Explanation: `docs/explanation/architecture/overview.md` or wherever the per-revision rule (P-2) is argued — record why rebuild-in-workflow beat promote-by-hash (Forgejo `needs:` cannot cross workflows) and why the Windows zip is attached as evidence or omitted; "Revisit if" a Windows durable download exists or Forgejo gains cross-workflow gating. - `CHANGELOG.md`: process change is not user-affecting per `CHANGELOG.md:3-8`; the install-URL change *is* (documented commands change) — one "Changed" line. - `skills/felis`: no CLI surface change; nothing to cascade. ### Tests that prove it - CI itself is the test; do a dry run on a throwaway annotated tag `v0.0.0-rc1`-style on a branch... Forgejo tag pushes trigger regardless of branch, so use a `v0.1.0-rc.1` prerelease tag on the release commit candidate: `verify-tag` must accept prerelease semver and the release step must mark it `prerelease: true`. A deliberate mismatch test: push `v0.1.0-rc.2` pointing at a commit whose `Cargo.toml` still says `0.1.0` with the wrong tag name → job fails at step 2. Record both run URLs in #31. - Unit-level: the `awk` CHANGELOG extraction and semver comparison can live in a small `scripts/release/` shell file with a bats-free `sh` self-test invoked by `just release-check`, or just be simple enough to read. ## Dependencies - **#17 first** (identity: semver + full revision + dirty in `felis --version`), otherwise assertion 4 has nothing typed to compare and "dirty release identity fails publication" cannot be tested end to end. Tracker order (#17 then #18) holds. - **#31** is the consumer; #18 must be green on an rc tag before #31 pushes the real tag. - Secrets/infra outside the repo: a Forgejo token with release write scope stored as an Actions secret, or confirmation that the runner's OIDC identity can call the release API (it cannot today; OIDC is only used for niks3 at `pr.yml:438-446`). ## Risk/effort **M.** Mostly YAML and docs, but the first tag run is unrehearsed on Forgejo: release-API auth, `fetch-depth` for annotated-tag inspection, and prerelease handling are each a likely first-run failure. Mitigate with the rc-tag rehearsal above. Secondary risk: doc drift — five files currently say "each `main` revision"; run the doc-cascade grep for `main` revision / `git+https://` before closing. ## Labels Keep `priority/P0`, `release/v0.1.0`. Suggest editing the scope line "Rebuild or promote only that exact revision" to "Rebuild from the tag inside `release.yml`" so the implementer does not attempt cross-workflow promotion, and adding the rc-tag rehearsal as an acceptance item. ## Review amendments (round 1) - **Prerelease tags.** `verify-tag` compares the tag's *core* semver (prerelease suffix stripped) with the manifest version and requires the tag's prerelease suffix, when present, to be well-formed (`-rc.N`). A prerelease tag publishes with `prerelease: true` and skips the `## [<semver>]` CHANGELOG-header check (which applies to final tags only). The mismatch rehearsal (`v0.1.0-rc.2` against a manifest saying a different core version) still fails at step 2. The exact-equality rule applies to final tags. ## Review amendments (round 2) - **Every gate compares the canonical JSON form.** Replace each `felis --version` comparison in `verify-tag` step 4, the `felis-x86_64-linux.txt` attachment, and `just release-check` with `felis version --format json` (offline for the `cli` object) and compare `.cli.version`, `.cli.revision` (full 40 hex), `.cli.dirty == false` against the tag's commit. `felis --version` stays as an offline human smoke line only; no gate parses it. ## Review amendments (round 3) - **`release.yml` reruns every exact-revision gate itself.** The job chain becomes `verify-tag` → `check` (`just check`: fmt-check + clippy + nextest + cargo-deny) → `proto-sync` (`just proto` then `git diff --exit-code` on `src/generated/`, plus `just schema` and a clean diff on the config schema) → `proto-compat` (#19's job, with the base = the latest earlier `v*` tag, or, for the first tag, an explicit `first-release` acknowledgment file that the workflow requires to exist and that #31 deletes afterward) → `build` → `linux-smoke` → `linux-publish` → `release`. A prior green `main` run is not a publication dependency; the tag run is. Rationale to record: a tag can be pushed at any commit, and Forgejo has no cross-workflow `needs:`, so trusting the `main` run would be a human procedure, not a gate. ## Review amendments (round 4) - `proto-compat` on a tag build compares against the committed release baseline (`proto/baseline/`, see #19 round 4), never against the latest Git tag; the `release` job updates the baseline after a successful final publication only. `first-release` is required exactly while no baseline exists. ## Review amendments (round 5) - `verify-tag` gains the release-API baseline check (#19 round 5): the committed baseline must match the latest published final release's `felis.proto` asset, and `first-release` is valid only when the API reports no final release. ## Review amendments (round 6) - **Release runs are serialized.** `release.yml` declares `concurrency: { group: felis-release, cancel-in-progress: false }` so tag runs queue instead of overlapping, covering the whole chain through the post-publication baseline commit. The release-API/baseline check runs twice: in `verify-tag` and again as the first step of the `release` job immediately before the publish call, so a release created outside the workflow, or by a queued earlier run, invalidates the result before anything is published.
Sign in to join this conversation.
No description provided.