release cut: bump-kind justfile recipe and a compiled, tested cut tool
Gates / frontend (push) Successful in 1m11s
Gates / test (push) Successful in 1m38s
Gates / test-aarch64 (push) Successful in 6m31s
Gates / container (push) Successful in 9s
CI / gates (push) Successful in 26m52s
Gates / package (push) Successful in 5m27s
Gates / frontend (push) Successful in 1m11s
Gates / test (push) Successful in 1m38s
Gates / test-aarch64 (push) Successful in 6m31s
Gates / container (push) Successful in 9s
CI / gates (push) Successful in 26m52s
Gates / package (push) Successful in 5m27s
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Local release cut: justfile + tools/cut.zig
|
||||
|
||||
Cutting v0.0.7 by hand missed the build.zig.zon bump; verify-dist caught it one CI round late. The cut becomes a compiled, tested tool (the same ruling that moved publication out of workflow shell into tools/release.zig), invoked from a thin justfile. Design reviewed and accepted by Codex (thread 01a020f9); its findings are folded in below.
|
||||
|
||||
## justfile (repo root)
|
||||
|
||||
Recipes only — no variables, no embedded logic:
|
||||
|
||||
- `test` → `zig build test`
|
||||
- `itest` → `zig build test -Dintegration`
|
||||
- `admin-check` → `cd admin && npm run typecheck && npm run test && npm run lint && npm run format:check` (the committed npm scripts, never npx: the repo records that npx can fetch an unpinned package)
|
||||
- `goldens` → the documented `-Dcontract-samples-out` invocation; comment says what `admin/src/lib/contractSamples.gen.ts` is: captured live API bodies the admin tests assert against
|
||||
- `build` → `cd admin && npm run build`, then `zig build -Dadmin-dist=admin/dist` (the default build embeds a placeholder page)
|
||||
- `verify` → `itest` + `admin-check` + `zig fmt --check build.zig src tools` — described honestly as the fast local checks, not the CI gate (it skips admin `npm run build`/`assert-bundled`, cross-targets, verify-dist)
|
||||
- `release kind` → `zig build cut -- {{kind}}` (kind ∈ major, minor, patch)
|
||||
|
||||
`verify` must NOT also depend on `test`: `-Dintegration` already runs the whole ordinary suite.
|
||||
|
||||
## tools/cut.zig
|
||||
|
||||
Wired like the other host tools (`hostTool` + `addRunArtifact`, see build.zig ~230): `zig build cut -- {major|minor|patch}`. NOT installed to zig-out/bin. Its tests join `zig build test`.
|
||||
|
||||
Constants: one repo API base `https://git.mial.net/api/v1/repos/mokhtar/nxdns` (the tool can only ever target this repo — no configurability). The runs API needs a token (verified: anonymous GET is 401); read it from `~/.config/tea/config.yml` (logins entry for git.mial.net); a missing token is a clear error naming the file.
|
||||
|
||||
### Sequence
|
||||
|
||||
1. **Derive the version**: the argument is a bump kind — `major`, `minor` or `patch` — never a free-form number (a number validated as "greater semver" still admits every typo, and a published tag is immutable). Parse `.version` from build.zig.zon (precedent: tools/verify_dist.zig:220, container_check.zig:317). If `v<zon-version>` exists on origin, derive the next version from the bump kind (patch 0.0.7→0.0.8, minor→0.1.0, major→1.0.0; checked u32 arithmetic). If it is absent, the zon version is an in-progress cut: resume it instead of incrementing again, reported explicitly — this preserves the bump-committed-but-untagged rerun and local-tag adoption. The CHANGELOG check in preflight applies to the derived (or resumed) version, so a changelog written for the wrong bump kind fails as a mismatch.
|
||||
2. **Preflight**: working tree clean; branch master; CHANGELOG.md has a `## [<v>] - YYYY-MM-DD` heading (dated, the repo's observed form; do NOT require the date be today) with a non-empty section body (the CI tool refuses a blank section — failing later just burns the tag); the tag-free check follows the plan: a derived version does its own `git ls-remote` and refuses if `v<v>` exists; a resumed version reuses the absence that selected it (asking twice invites two answers). Transport/auth failure is always distinguished from "no match" (exit code + stderr, never "nonzero means absent") and refuses — on the resume path an error read as absent would resume a released version. The local tag namespace is checked too — see resumability.
|
||||
3. **Bump if needed**: rewrite build.zig.zon atomically, reparse it, assert the git diff contains exactly that one file, then `git commit -S -m "build: bump version to <v>"`. git runs with inherited stdio so pinentry can prompt; check the child's actual termination state.
|
||||
4. **Push master**, capture the exact HEAD sha; all later status lookups and the tag use that sha explicitly.
|
||||
5. **Wait for CI**: poll the runs API for the ci.yml run matching that sha and event push. A run-id floor captured before the push scopes the match — applied only when the push actually moved the ref (`git push --porcelain` destination flag), so a no-op push (resume case, or a concurrent identical push) adopts the existing run for that sha. Require the run to appear within a startup deadline; wait for its terminal conclusion. Refuse to tag on anything but success, naming the failing job from the commit-status contexts. Every HTTP attempt individually bounded by a monotonic deadline; transport/JSON errors are reported, never silently treated as pending.
|
||||
6. **Reassert** HEAD and tree unchanged, then `git tag -s v<v> -m v<v> <sha>` and push the tag.
|
||||
7. **Wait for the release run** (matched by workflow path `release.yml@refs/tags/v<v>` — Gitea reports tag pushes as event "push"): startup deadline for the run to appear; the completion clock starts at first sighting, with a ceiling derived from the workflow's sequential jobs — guard 15 + gates 60 (the tool's own CI bound) + publish 120 (release.yml:220) = 195 minutes. Before tagging, reassert HEAD, the tree, and (for an adopted tag) the tag object id; every tag pushed — created or adopted — must carry a signature whose VALIDSIG primary fingerprint equals the one release.yml pins as TAG_SIGNING_FPR. Report the terminal conclusion; on failure name the failing context.
|
||||
8. **On success**: GET the release object, require it published (not draft), print the tag and asset names.
|
||||
|
||||
### Resumability
|
||||
|
||||
A failed run must not strand the operator:
|
||||
- Bump pushed, then failure: rerun continues (preflight sees the version already bumped).
|
||||
- Local tag exists but never reached origin: verify it is an annotated tag by this tool's convention pointing at the current HEAD — adopt it; otherwise refuse with the exact `git tag -d` to run. Never delete a tag that exists on origin.
|
||||
|
||||
### Tests (in-file, join `zig build test`)
|
||||
|
||||
Pure functions unit-tested: semver validation (accept/reject table incl. leading zeroes, `v` prefix), bump-kind parse, derivation table with the minor/major resets and overflow refusals, derive-vs-resume decision for all three kinds, zon `.version` parse + rewrite round-trip, changelog heading + non-empty body check, runs-JSON → decision (running / success / failure / no-run), tea-config token extraction. Process spawning and HTTP live behind thin call sites and are not mocked.
|
||||
|
||||
## Anti-requirements
|
||||
|
||||
- No general release framework; no shared process plumbing extracted unless a third caller appears (container_check.zig:24 rule).
|
||||
- No confirmation prompts — invoking `just release <kind>` is the authorization.
|
||||
- No secrets in argv, no token printed.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] `just --list` shows the recipes; `just verify` passes locally.
|
||||
- [ ] `zig build cut -- patch` derives the next version and refuses in preflight on a dirty tree or a missing changelog section, mutating nothing; `zig build cut -- 0.0.9` and `-- banana` refuse naming the three kinds.
|
||||
- [ ] `zig build test` and `-Dintegration` 0 failed; `zig fmt --check` clean.
|
||||
Reference in New Issue
Block a user