Add tag-triggered release workflow with static musl binary #11

Merged
schaefera merged 5 commits from ci/release into master 2026-10-16 13:06:23 +00:00
Collaborator

Closes #7

What

Pushing a SemVer tag vX.Y.Z or vX.Y.Z-<pre> builds a fully static x86_64 musl binary, smoke-tests it, and publishes a Forgejo release with sporah-X.Y.Z-x86_64-linux.tar.gz (binary, README, LICENSE, completions) plus SHA256SUMS. A release only runs when the tag equals Cargo.toml's version and the commit is on master. Tags with a - are marked as pre-releases.
reqwest switches from native-tls to rustls (no OpenSSL) and trusts both the system CA store and bundled webpki roots. The README gets a download/verify/install snippet.

Blast radius

Tier: trunk (the repo has no CLAUDE.md "Blast radius" list, so everything is trunk)
Trunk paths touched: .forgejo/workflows/release.yml, Cargo.toml, Cargo.lock, README.md
Gate / rollback: the workflow only runs on SemVer tag pushes, so nothing happens until a tag is pushed. To undo, revert this merge, and delete a bad release and its tag in Forgejo. Re-running on the same tag replaces that release's assets (override: true).

Proof

  • CI commands (local): cargo fmt --check ok; cargo clippy --locked --all-targets -- -D warnings ok; cargo test --locked 9 passed; cargo audit exit 0 (one allowed "unsound" warning for anyhow, via wasm-only deps).
  • Every release step except publish, run in rust:1-bookworm:
    • the tag check passes and the 9 tests pass;
    • the musl build gives ldd: "statically linked" and --version: sporah-cli 0.1.0;
    • the live city Berlin call prints bars;
    • the tarball lists sporah, README.md, LICENSE, completions/_sporah and completions/sporah.bash, and SHA256SUMS is written.
  • Packaged binary in busybox:1.36 (no /etc/ssl): a live HTTPS fetch exits 0, so the bundled roots work without a system store.
  • Tag trigger, checked with nektos/act's workflowpattern (the matcher Forgejo uses):
    • triggers: v0.1.0, v10.20.300, v0.2.0-rc1, v1.0.0-beta.2;
    • ignored: viktor, v1, v0.1, v1.2.3.4, va.b.c, v1.2.3rc1, 1.2.3.
  • Tag/version guard: v0.1.0 ok; v0.2.0 and v0.1 fail.
  • Smoke step against the packaged binary (with a timeout):
    • the real API passes;
    • an API 404 passes, because TLS worked;
    • an unreachable host and an unroutable IP fail on the timeout.
  • Not proven:
    • the publish step, including prerelease and override (needs a real tag and RELEASE_TOKEN);
    • the master-ancestry gate on a real runner;
    • native roots against a real private CA.

Review

Reviewers (all fresh-context, using the code-review method):

  • Review 1, high: 6 findings and 2 notes.
    • Fixed: master gate and tests in the job; system CA roots; live TLS smoke test; README; github.repository; sccache off.
    • Declined: early token check; release notes.
  • Review 2, medium:
    • Fixed: the smoke test fails only on transport errors; a comment says the ancestry gate doesn't check CI status.
    • Verified: RUSTC_WRAPPER="" overrides the image's ENV RUSTC_WRAPPER=sccache.
  • Maintainer comment ("viktor" tags): fixed with SemVer-only tag filters.
  • Review 3, medium:
    • Fixed: pre-releases are marked prerelease, so they never become "latest"; a 30 s timeout on the live call.
    • Accepted: override: true (makes re-runs safe); the ancestry gate doesn't check CI status (documented); the grep depends on the "request failed" message text.
  • Review 4, medium: no blocking findings. Accepted: a slow API (>30 s) fails the release, since a hang may be the TLS/DNS problem the check exists for.

Review-Confidence: 45/100. "The workflow logic was reviewed well at high and then medium, with local simulations covering most steps. But the publish/prerelease step and the ancestry gate have never run, and the CI-passed gate is only a comment. Do a dry-run -rc1 tag release on master before relying on this."

Confidence by part

  • Build, static check, packaging: high. Run end to end in a container; the binary was exercised in busybox.
  • Tag trigger and tag/version guard: high. Tested with Forgejo's matcher and with a script.
  • Smoke test ("Check binary is static and can reach the API"): medium. Reviewed and tested locally. Known gap, found by the auditor and not fixed: a crash by signal (exit > 128) currently passes.
  • Master-ancestry gate: medium. Not run on a runner.
  • Publish (forgejo-release, prerelease, RELEASE_TOKEN): low. Never exercised. It needs the repo secret RELEASE_TOKEN with write:repository scope. A first v0.1.0-rc1 tag (with Cargo.toml set to match) would exercise it as a pre-release.
  • System CA roots: medium. Relies on reqwest's documented merge of both root sets.

Merge with "Create merge commit" so the Review-Confidence trailers survive.

🤖 Generated with Claude Code

Closes #7 ## What Pushing a SemVer tag `vX.Y.Z` or `vX.Y.Z-<pre>` builds a fully static x86_64 musl binary, smoke-tests it, and publishes a Forgejo release with `sporah-X.Y.Z-x86_64-linux.tar.gz` (binary, README, LICENSE, completions) plus `SHA256SUMS`. A release only runs when the tag equals `Cargo.toml`'s version and the commit is on master. Tags with a `-` are marked as pre-releases. reqwest switches from native-tls to rustls (no OpenSSL) and trusts both the system CA store and bundled webpki roots. The README gets a download/verify/install snippet. ## Blast radius **Tier:** trunk (the repo has no CLAUDE.md "Blast radius" list, so everything is trunk) **Trunk paths touched:** `.forgejo/workflows/release.yml`, `Cargo.toml`, `Cargo.lock`, `README.md` **Gate / rollback:** the workflow only runs on SemVer tag pushes, so nothing happens until a tag is pushed. To undo, revert this merge, and delete a bad release and its tag in Forgejo. Re-running on the same tag replaces that release's assets (`override: true`). ## Proof - CI commands (local): `cargo fmt --check` ok; `cargo clippy --locked --all-targets -- -D warnings` ok; `cargo test --locked` 9 passed; `cargo audit` exit 0 (one allowed "unsound" warning for `anyhow`, via wasm-only deps). - Every release step except publish, run in `rust:1-bookworm`: - the tag check passes and the 9 tests pass; - the musl build gives `ldd`: "statically linked" and `--version`: `sporah-cli 0.1.0`; - the live `city Berlin` call prints bars; - the tarball lists `sporah`, `README.md`, `LICENSE`, `completions/_sporah` and `completions/sporah.bash`, and `SHA256SUMS` is written. - Packaged binary in `busybox:1.36` (no `/etc/ssl`): a live HTTPS fetch exits 0, so the bundled roots work without a system store. - Tag trigger, checked with nektos/act's `workflowpattern` (the matcher Forgejo uses): - triggers: `v0.1.0`, `v10.20.300`, `v0.2.0-rc1`, `v1.0.0-beta.2`; - ignored: `viktor`, `v1`, `v0.1`, `v1.2.3.4`, `va.b.c`, `v1.2.3rc1`, `1.2.3`. - Tag/version guard: `v0.1.0` ok; `v0.2.0` and `v0.1` fail. - Smoke step against the packaged binary (with a timeout): - the real API passes; - an API 404 passes, because TLS worked; - an unreachable host and an unroutable IP fail on the timeout. - **Not proven:** - the publish step, including `prerelease` and `override` (needs a real tag and `RELEASE_TOKEN`); - the master-ancestry gate on a real runner; - native roots against a real private CA. ## Review **Reviewers** (all fresh-context, using the `code-review` method): - **Review 1, high:** 6 findings and 2 notes. - Fixed: master gate and tests in the job; system CA roots; live TLS smoke test; README; `github.repository`; sccache off. - Declined: early token check; release notes. - **Review 2, medium:** - Fixed: the smoke test fails only on transport errors; a comment says the ancestry gate doesn't check CI status. - Verified: `RUSTC_WRAPPER=""` overrides the image's `ENV RUSTC_WRAPPER=sccache`. - **Maintainer comment ("viktor" tags):** fixed with SemVer-only tag filters. - **Review 3, medium:** - Fixed: pre-releases are marked `prerelease`, so they never become "latest"; a 30 s timeout on the live call. - Accepted: `override: true` (makes re-runs safe); the ancestry gate doesn't check CI status (documented); the grep depends on the "request failed" message text. - **Review 4, medium:** no blocking findings. Accepted: a slow API (>30 s) fails the release, since a hang may be the TLS/DNS problem the check exists for. **Review-Confidence:** 45/100. "The workflow logic was reviewed well at high and then medium, with local simulations covering most steps. But the publish/prerelease step and the ancestry gate have never run, and the CI-passed gate is only a comment. Do a dry-run `-rc1` tag release on master before relying on this." ## Confidence by part - Build, static check, packaging: **high**. Run end to end in a container; the binary was exercised in busybox. - Tag trigger and tag/version guard: **high**. Tested with Forgejo's matcher and with a script. - Smoke test ("Check binary is static and can reach the API"): **medium**. Reviewed and tested locally. Known gap, found by the auditor and not fixed: a crash by signal (exit > 128) currently passes. - Master-ancestry gate: **medium**. Not run on a runner. - Publish (`forgejo-release`, `prerelease`, `RELEASE_TOKEN`): **low**. Never exercised. It needs the repo secret `RELEASE_TOKEN` with `write:repository` scope. A first `v0.1.0-rc1` tag (with `Cargo.toml` set to match) would exercise it as a pre-release. - System CA roots: **medium**. Relies on reqwest's documented merge of both root sets. Merge with "Create merge commit" so the `Review-Confidence` trailers survive. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Drops the OpenSSL dependency so the musl release binary is fully
static and needs no system TLS library or CA bundle at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add tag-triggered release workflow with static musl binary
All checks were successful
CI / audit (pull_request) Successful in 15s
CI / ci (pull_request) Successful in 1m12s
4265f43c70
On a v* tag: check the tag matches Cargo.toml, build
x86_64-unknown-linux-musl, verify it is static, package
sporah-<version>-x86_64-linux.tar.gz (binary, completions, README,
LICENSE) with SHA256SUMS, and publish a Forgejo release using the
RELEASE_TOKEN secret. README documents installing from a release.

Closes #7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gate release on master, smoke-test TLS, trust system CAs
All checks were successful
CI / audit (pull_request) Successful in 12s
CI / ci (pull_request) Successful in 52s
20e328bc7e
Review follow-ups for the release workflow:
- Fail unless the tagged commit is on master (where ci.yml ran), and run
  cargo test before building.
- Live request to the API after the static check, so a broken TLS/DNS
  setup in the static binary fails the release. Only "request failed"
  fails it; any HTTP answer (no data, 5xx) shows TLS worked.
- Enable rustls native roots alongside webpki roots, so a system CA
  store (corporate proxies, private CAs) is honoured while hosts without
  one still work.
- Disable sccache so the release build is actually clean.
- Use github.repository instead of a hardcoded repo name.
- README: mkdir -p ~/.local/bin, note that version is the latest release.

Review-Tier: trunk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review-Confidence: 25
claude-bot changed title from WIP: Add tag-triggered release workflow (static musl binary) to Add tag-triggered release workflow with static musl binary 2026-10-16 11:51:44 +00:00
@ -0,0 +6,4 @@
on:
push:
tags:
- "v*"
Owner

Would this be triggered if one pushed a tag like "viktor" or similar as well? If so, would be better to specify more specifically if possible which tags trigger the build. We want to follow Semver, so the format should be relatively predictable.

Would this be triggered if one pushed a tag like "viktor" or similar as well? If so, would be better to specify more specifically if possible which tags trigger the build. We want to follow Semver, so the format should be relatively predictable.
"v*" also started the workflow for tags like "viktor" (which then failed
the Cargo.toml version check). Match vMAJOR.MINOR.PATCH and
vMAJOR.MINOR.PATCH-<pre-release> instead.

Review-Tier: trunk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark pre-release tags as pre-releases, time out the smoke test
All checks were successful
CI / audit (pull_request) Successful in 12s
CI / ci (pull_request) Successful in 40s
5771b81892
- forgejo-release gets prerelease: true for tags with a "-" (v0.2.0-rc1),
  so a release candidate never becomes the "latest" release.
- The live API call runs under `timeout 30`; sporah sets no request
  timeout, so a stalled connection would otherwise hang the job.

Review-Tier: trunk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review-Confidence: 45
Author
Collaborator

Re the v* trigger ("viktor"): yes, it would have started a run, which then failed at the Cargo.toml version check. Fixed in bfd98a6. The trigger is now SemVer only:

tags:
  - "v[0-9]+.[0-9]+.[0-9]+"
  - "v[0-9]+.[0-9]+.[0-9]+-*"

I checked this with nektos/act's workflowpattern, the matcher Forgejo uses:

  • Triggers: v0.1.0, v10.20.300, v0.2.0-rc1, v1.0.0-beta.2.
  • Ignored: viktor, v1, v0.1, v1.2.3.4, va.b.c, v1.2.3rc1, 1.2.3.

The follow-up review found that pre-release tags would have been published as full releases and become "latest". a923ccd now marks tags that contain - as prerelease.

Re the `v*` trigger ("viktor"): yes, it would have started a run, which then failed at the Cargo.toml version check. Fixed in bfd98a6. The trigger is now SemVer only: ```yaml tags: - "v[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+-*" ``` I checked this with nektos/act's `workflowpattern`, the matcher Forgejo uses: - **Triggers:** `v0.1.0`, `v10.20.300`, `v0.2.0-rc1`, `v1.0.0-beta.2`. - **Ignored:** `viktor`, `v1`, `v0.1`, `v1.2.3.4`, `va.b.c`, `v1.2.3rc1`, `1.2.3`. The follow-up review found that pre-release tags would have been published as full releases and become "latest". a923ccd now marks tags that contain `-` as `prerelease`.
schaefera approved these changes 2026-10-16 13:06:19 +00:00
schaefera merged commit d7fe4217d0 into master 2026-10-16 13:06:23 +00:00
schaefera deleted branch ci/release 2026-10-16 13:06:23 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/sporah-cli!11
No description provided.