[v0.1/P0] Add a tag-driven immutable release artifact gate #18
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#18
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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-linuxartifact. A first release needs an immutable source-to-artifact identity.Scope
v0.1.0tag.main.Acceptance criteria
x86_64-linuxis advertised as supported for v0.1.0.Triage plan (2026-09-03)
Source-grounded triage against
mainat69076d42, reviewed through seven rounds of an independent reviewer (pisol/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.
git tagis empty.CHANGELOG.md:14has a single## [Unreleased]header andCHANGELOG.md:11-12says everything accrues there until a first tag..forgejo/workflows/pr.yml:11-15triggers onpush: branches: [main]andpull_request; there is notags:trigger in any ofpr.yml,build.yml,bench.yml,fuzz.yml,kani.yml.linux-publish(pr.yml:415-477) pushesnix build .#felisto the niks3 cache on every greenmainpush (if: github.event_name == 'push',:452);windows-package(pr.yml:301-364) uploads a zip as a 14-day run artifact.build.yml:8-32anddocs/reference/workspace.md:271-300state the per-revision rule explicitly and say promotion "is a deliberate human step" (pr.yml:312-317,docs/reference/testing.md:1140-1146).x86_64-linuxshould be advertised: already the documented stance —docs/reference/workspace.md:276-281anddocs/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.main:README.md:20,23,docs/how-to/install.md:16,19,36,49all usegit+https://git.natsukium.com/natsukium/feliswith no?ref=/?rev=;docs/how-to/update-felis.md:26isnix profile upgrade felis(follows whatever ref was installed).install.md:143and:154describe the cache as holding "eachmainrevision that passes".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:59single-sources the package version fromcrates/felis-cli/Cargo.toml, which is where the tag ↔ version comparison should anchor.uses:(pr.yml:28,:360); the artifact upload already uses Forgejo'supload-artifactfork. There is no Forgejo release-creation action in the tree, so publishing to a release page needs the Forgejo API (fjskill /curlwith a token) or theforgejo-releaseaction — a newuses:that perdocs/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.ymland verify identity, rather than trying to promote the earliermainpush's store path: Forgejo cannotneeds:across workflow runs (build.yml:21-24,pr.yml:424-427already 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 aneeds:edge. The output store path is content-addressed by the same derivation, so the bytes match themainpush'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 asnix 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 referencegit+https://…/felis?ref=refs/tags/v0.1.0is 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
maindid I install").Approach
.forgejo/workflows/release.yml(new)on: push: tags: ['v*']. Job chain:verify-tag→build(same steps aspr.yml:31-43) →linux-smoke→linux-publish→release. Reuse by copying the job bodies; Forgejo has no reusable-workflowuses:for local workflows that is worth depending on, and the comment block inpr.ymlalready documents why each step exists — reference it rather than repeat it.verify-tag(runs onnix,fetch-depth: 0—pr.ymlcheckouts are depth 1, onlybench.yml:68fetches full history):git cat-file -t "$GITHUB_REF_NAME"istag(annotated), andgit rev-parse "$GITHUB_REF_NAME^{}"equalsgithub.sha.v<semver>equals(lib.importTOML crates/felis-cli/Cargo.toml).package.version(the sourcenix/package.nix:59already reads) and equals every workspace crate'sversion(they are set per crate,crates/*/Cargo.toml:3, not inherited — see #31 report).CHANGELOG.mdhas a## [<semver>] - <date>header, not only Unreleased.nix eval --raw .#felis.versionmatches;nix flake metadata --json | jq .dirtyRevisionis 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 topr.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 themainbuild 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/releaseswithsecrets.RELEASE_TOKEN, or the commit-pinnedhttps://code.forgejo.org/actions/forgejo-release), attachingcrates/felis-client-core/felis-config.schema.json,crates/felis-protocol/proto/felis.proto, afelis-x86_64-linux.txtcontaining the store path, narHash, and thefelis --versionline, and the Windows zip only if awindowsleg is included (see below). Body: the CHANGELOG section for that version (extract withawkbetween headers) plus the support matrix line "supported: x86_64-linux only".windows-test/windows-smoke/windows-packagechain torelease.ymland attach the zip labelled "CI evidence, unsupported", or leave it out of the release entirely and let the docs point at thepr.ymlrun 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.ymlwindows-package's upload for PR-less pushes? No — keep; it is the evidence the docs cite.justfilejust release-check <tag>: the same local assertions asverify-tag(annotated tag, semver match across manifests, CHANGELOG header, clean tree,nix buildthenfelis --versionmatch), so the human cutting the tag (#31) runs the gate before pushing it. Add under areleasegroup heading; keepCI mirrors 1:1header claim true by namingrelease.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 thex86_64-linux"Published artifact" cell to say tag builds are the supported artifact andmainpushes are pre-release.docs/reference/testing.md:1073-1160"CI shape": addrelease.ymlto the live-pipeline list.docs/how-to/install.md:16,19,36,49andREADME.md:20,23:git+https://git.natsukium.com/natsukium/felis?ref=refs/tags/v0.1.0(andinputs.felis.urllikewise); keep one "track main (unsupported)" example clearly labelled.install.md:143,154"eachmainrevision" → tag builds are supported,mainbuilds are previews.docs/how-to/update-felis.md:26,99—nix profile upgradeand the "first tagged release" sentence.docs/explanation/architecture/overview.mdor wherever the per-revision rule (P-2) is argued — record why rebuild-in-workflow beat promote-by-hash (Forgejoneeds: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 perCHANGELOG.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
v0.0.0-rc1-style on a branch... Forgejo tag pushes trigger regardless of branch, so use av0.1.0-rc.1prerelease tag on the release commit candidate:verify-tagmust accept prerelease semver and the release step must mark itprerelease: true. A deliberate mismatch test: pushv0.1.0-rc.2pointing at a commit whoseCargo.tomlstill says0.1.0with the wrong tag name → job fails at step 2. Record both run URLs in #31.awkCHANGELOG extraction and semver comparison can live in a smallscripts/release/shell file with a bats-freeshself-test invoked byjust release-check, or just be simple enough to read.Dependencies
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.pr.yml:438-446).Risk/effort
M. Mostly YAML and docs, but the first tag run is unrehearsed on Forgejo: release-API auth,
fetch-depthfor 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 "eachmainrevision"; run the doc-cascade grep formainrevision /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 insiderelease.yml" so the implementer does not attempt cross-workflow promotion, and adding the rc-tag rehearsal as an acceptance item.Review amendments (round 1)
verify-tagcompares 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 withprerelease: trueand skips the## [<semver>]CHANGELOG-header check (which applies to final tags only). The mismatch rehearsal (v0.1.0-rc.2against a manifest saying a different core version) still fails at step 2. The exact-equality rule applies to final tags.Review amendments (round 2)
felis --versioncomparison inverify-tagstep 4, thefelis-x86_64-linux.txtattachment, andjust release-checkwithfelis version --format json(offline for thecliobject) and compare.cli.version,.cli.revision(full 40 hex),.cli.dirty == falseagainst the tag's commit.felis --versionstays as an offline human smoke line only; no gate parses it.Review amendments (round 3)
release.ymlreruns every exact-revision gate itself. The job chain becomesverify-tag→check(just check: fmt-check + clippy + nextest + cargo-deny) →proto-sync(just protothengit diff --exit-codeonsrc/generated/, plusjust schemaand a clean diff on the config schema) →proto-compat(#19's job, with the base = the latest earlierv*tag, or, for the first tag, an explicitfirst-releaseacknowledgment file that the workflow requires to exist and that #31 deletes afterward) →build→linux-smoke→linux-publish→release. A prior greenmainrun 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-workflowneeds:, so trusting themainrun would be a human procedure, not a gate.Review amendments (round 4)
proto-compaton a tag build compares against the committed release baseline (proto/baseline/, see #19 round 4), never against the latest Git tag; thereleasejob updates the baseline after a successful final publication only.first-releaseis required exactly while no baseline exists.Review amendments (round 5)
verify-taggains the release-API baseline check (#19 round 5): the committed baseline must match the latest published final release'sfelis.protoasset, andfirst-releaseis valid only when the API reports no final release.Review amendments (round 6)
release.ymldeclaresconcurrency: { 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: inverify-tagand again as the first step of thereleasejob 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.