release · diff

git:20260526.d4be150 to git:20261019.7a74bbf

199 added, 86 removed. Audit A to A.

---
name: release
- description: Orchestrate a new Freenet release. Determines next version, shows changelog, confirms with user, and triggers the release pipeline via GitHub Actions. Use when the user says "do a release", "new release", "release", or "/release".
+ description: Orchestrate a new freenet-core release. Runs the MANDATORY pre-cut gates (no concurrent cut, auto-update updater diff + canary suites, a 30-minute real-NAT soak of the exact ship SHA, a telemetry baseline), then determines the version, confirms with the user, triggers the release pipeline via GitHub Actions, and monitors telemetry through the 0-4h auto-update stagger. Use when the user says "do a release", "new release", "cut a release", "release", or "/release".
user_invocable: true
license: LGPL-3.0
---
# Freenet Release Skill
- ## Overview
+ This is the **single entry point** for cutting a freenet-core release. The order is fixed:
- This skill orchestrates a complete Freenet release. The release pipeline runs entirely in GitHub Actions — your job is to determine the version, confirm with the user, trigger the workflow, watch it complete, then verify the cascade (gateway updates, announcements, network health).
+ 1. **MANDATORY PRECONDITIONS** P0-P4 (below). Every one must pass, and you must report each result.
+ 2. Steps 1-5: version, trigger, watch, verify artifacts, River smoke test.
+ 3. **Step 6 (precondition P5): post-cut telemetry monitoring** through the 0-4h auto-update stagger.
- The workflow handles all of: version bump → release PR → CI gate → crates.io publish → GitHub release → cross-compile binaries. The `release.published` event then auto-triggers `gateway-update.yml` (HMAC-POSTs each gateway's release-agent) and `release-announce.yml` (Matrix + River posts via release-agent).
+ **Do not dispatch `release.yml` or run `scripts/release.sh` until P0-P4 have passed.** No "quick release", "routine patch" or "CI is green" path skips them. The only thing you may run beforehand is `scripts/release.sh --dry-run`.
+ **A `dry_run=true` dispatch of `release.yml` is NOT side-effect free.** Its version-bump job has no dry-run guard, so it pushes a release branch, opens a real `build: release X.Y.Z` PR and auto-merges it into `main`. That makes it a trigger: it needs the preconditions too, and it moves `main` past the SHA you soaked.
+
+ The release pipeline runs entirely in GitHub Actions: version bump, then release PR, CI gate, crates.io publish, GitHub release, and cross-compiled binaries. The `release.published` event then auto-triggers `gateway-update.yml` (which HMAC-POSTs each gateway's release-agent) and `release-announce.yml` (Matrix and River posts). CI does **not** exercise a real NATed peer, and it does not exercise the shipping binary's own updater. That is why the preconditions exist.
+
+ ## MANDATORY PRECONDITIONS: run all of these before ANY trigger
+
+ > **Maintainers:** the environment-specific parts of these gates live in your local
+ > `freenet-release` skill, and **you must run it**. That covers which real-NAT peer you
+ > soak on, how to reach it and deploy the binary there, gateway hosts, and the traps
+ > specific to your setup. This skill defines **what** must pass; the local skill says
+ > **how** in your environment. If you have no local companion, you still have to meet
+ > every gate by equivalent means. Write down what you did; do not skip a gate.
+ > Some maintainer environments enforce this with a hook that refuses the trigger unless
+ > the local soak procedure has recorded a passing result for the current `origin/main` SHA.
+
+ ### P0: No concurrent cut
+
+ Two cuts racing each other produce conflicting release PRs and version bumps. Check:
+
+ ```bash
+ gh run list --workflow release.yml --repo freenet/freenet-core --limit 5 \
+ --json databaseId,status,conclusion,createdAt,displayTitle
+ gh run list --workflow cross-compile.yml --repo freenet/freenet-core --limit 5 \
+ --json databaseId,status,headBranch,event,createdAt # the previous tag's publish/un-draft
+ gh pr list --repo freenet/freenet-core --state open --search 'build: release in:title'
+ ```
+
+ **Stop** in any of these cases:
+
+ - a `release.yml` run is `queued` or `in_progress`
+ - a tag's `cross-compile.yml` run is still running (that is where the previous release publishes and un-drafts)
+ - a `build: release` PR is open
+ - another maintainer is mid-soak or mid-cut (ask in the dev channel if unsure)
+
+ Coordinate first. **Re-run these checks immediately before Step 2**: the soak takes 30+ minutes, and the state can change during it.
+
+ ### P1: Auto-update is not broken (updater diff + BOTH canary suites)
+
+ Auto-update is the highest-consequence subsystem in a release because a peer's update path has only one leg. Every stage of it is the binary being replaced. A broken updater therefore **cannot be recovered in the field**: the affected cohort never self-heals and has to be fixed by hand. v0.2.120/v0.2.121 (#5221) did exactly this. It was broken fleet-wide, went unnoticed for two releases, and about 1,100 nodes were updated manually. Answer "is auto-update safe?" with commands, not from memory, and not from the fact that CI is green.
+
+ **Step 1: does the payload touch the updater?**
+
+ ```bash
+ R=<your freenet-core checkout>
+ git -C "$R" fetch -q origin
+ LAST_TAG=$(git -C "$R" describe --tags --abbrev=0 origin/main)
+ git -C "$R" diff --stat "$LAST_TAG"..origin/main -- \
+ crates/core/src/bin/commands/auto_update.rs \
+ crates/core/src/bin/commands/update.rs crates/core/src/bin/commands/update/ \
+ crates/core/src/bin/freenet.rs \
+ crates/core/src/bin/commands/service.rs crates/core/src/bin/commands/service/ \
+ crates/core/src/bin/commands/rollback.rs \
+ scripts/auto-update-canary.sh scripts/auto-update-canary_test.sh \
+ scripts/auto-update-canary_lifecycle_test.sh scripts/release_canary_wiring_test.sh \
+ .github/workflows/cross-compile.yml .github/workflows/release.yml
+ ```
+
+ If any file changed, read the full diff. A diff that is entirely comments is not a behaviour change, but **prove** that rather than eyeballing it. For a shell script, strip whole-line comments only. Stripping everything after a `#` would also delete executable `${x##*/}` and `${1#v}` expansions, and could falsely report "identical".
+
+ ```bash
+ f=scripts/auto-update-canary.sh
+ diff <(git -C "$R" show "$LAST_TAG:$f" | sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d') \
+ <(git -C "$R" show "origin/main:$f" | sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d') \
+ && echo "IDENTICAL apart from whole-line comments"
+ ```
+
+ **Step 2: run BOTH canary self-test suites.** They are fast and hermetic, and need no node. A failure blocks the cut.
+
+ ```bash
+ cd "$R"
+ bash scripts/auto-update-canary_test.sh # wiring pins: markers, trigger sites, frozen constants
+ bash scripts/auto-update-canary_lifecycle_test.sh # Gate B retries INDETERMINATE, never a real failure
+ ```
+
+ **Step 3: know what CI's gates do not cover.** Gate A (pre-flight, which blocks the crates.io publish) and Gate B (self-update canary from the previous release to this one, post-publish and non-blocking) run automatically in the **`cross-compile.yml` run for the release tag**, not in `release.yml`. They have two structural gaps that no green tick reveals:
+
+ - **Gate B runs the PREVIOUS release's updater.** The shipping binary's own installer half (checksum, signature, extract, swap, restart) is never exercised in its own release.
+ - **The canary is Linux musl x86_64 only.** macOS has partial cover through the dmg-swap E2E. **Windows has no auto-update gate at all** (#5341).
+
+ The P2 soak peer runs with auto-update **enabled**. It is the only real-NAT observer of the released updater, so never disable auto-update on it. If you need to freeze its version, stop the service instead.
+
+ ### P2: Real-NAT pre-release soak of the exact ship SHA (30 minutes)
+
+ Unit, simulation and CI tests miss regressions that only real subscription and UPDATE traffic over a NAT link surface. Examples are the #4345 GET-stream-assembly failures, connection-teardown bugs and log spam. All of the following are required:
+
+ - **Exact ship SHA.** Build the `origin/main` HEAD that the release will tag. If `main` moves before you trigger, the soak no longer covers what ships, so re-soak the new SHA.
+ - **Clean, non-dirty build.** `git status --short` must be empty, **including untracked files**. `build.rs` stamps the build from `git status --porcelain`, and **a `-dirty` build has auto-update DISABLED**, so a soak of a dirty binary proves nothing about the updater (freenet-core#5613). Check the **artifact**: `freenet --version` on the soak peer must show the ship SHA with no `-dirty` suffix.
+ - **A real NATed peer, upgraded to that binary.** It must not be a gateway, a loopback simulation or a datacenter host with a public IP. Auto-update must stay enabled on it (see P1).
+ - **River room check.** On the soak peer's own node, the Freenet Official room must load: members render, the composer is present, it is not stuck on "Subscribing…", and there is no WASM panic. River renders inside an **iframe**, so assertions against the top-level document see an empty page and report a healthy room as dead. Select the content frame.
+ - **Subscribe/UPDATE test message.** Send one innocuous message to the Freenet Official room as owner (maintainers: the local `river-official-room` skill; otherwise ask the room owner). Confirm it appears in the soak peer's UI. This proves the new binary applies a contract UPDATE over its live subscription.
+ - **A 30-minute log and behaviour watch on the new binary.** This is the standard duration. Shorter does not count. Watch for panics or service restarts, the #4345 stream-assembly signature, connection failures and timeouts, and log spam (one message repeating hundreds of times). Save the log excerpt; it becomes the pre-release baseline that Step 6 compares gateways against.
+
+ **If no real-NAT peer is available**, the default is to **wait**. A weaker fallback exists for one narrow case only, and it needs the user's **explicit approval**:
+
+ - **Eligibility:** config, telemetry or CI-only releases, with no transport, subscribe, UPDATE or contract-runtime change. Show the user the changed files and why each one qualifies.
+ - **The fallback:**
+ - the full simulation suite green
+ - a 30-minute smoke test of the ship binary on a **non-service test instance** next to a gateway (never overwrite a gateway's service binary), exercising River
+ - heightened Step 6 monitoring
+ - the real-NAT gap stated in the release report
+ - **Anything touching those runtime paths:** do not ship without real-NAT validation. Wait for a NATed peer, or stand one up: a real NATed host, such as a cloud VM behind a NAT gateway. A loopback, netns or container "NAT" simulation does not count.
+
+ **Gate:** if anything regressed, **do not release**. Roll the soak peer back to its previous binary and investigate.
+
+ ### P3: Pre-cut telemetry baseline
+
+ Step 6 is only meaningful against a baseline taken **before** the cut. Using the dashboard at telemetry.freenet.org (or the maintainer-local `freenet-telemetry-monitor` skill), record and save the following:
+
+ - GET, PUT, SUBSCRIBE and UPDATE success rates and latency
+ - peer and connection counts
+ - the current version distribution
+ - gateway error rates and log growth (about 1 MB/h is normal)
+
+ Note the time window you measured. Save it somewhere you can diff against later, not just in chat.
+
+ ### P4: Report the gates and confirm
+
+ Before Step 2, tell the user which gates ran and what each returned: P0-P3, plus the soak SHA, `--version` output, soak duration and room/message result. "CI is green" is not an answer to "did we break auto-update". The canary suites and the soak answer that, and they are different signals. Then confirm the version and changelog (Step 1).
+
## Arguments
- If an argument is provided (e.g., `/release 0.2.65`), use it as the target version.
- - If no argument is provided, auto-detect: increment the patch of the version currently in `crates/core/Cargo.toml`. If unreleased PRs have already bumped it past the last tag, **use the current Cargo.toml version as-is** rather than incrementing again — that's the version those PRs were intended for.
+ - If no argument is provided, auto-detect: increment the patch of the version currently in `crates/core/Cargo.toml`. If unreleased PRs have already bumped it past the last tag, **use the current Cargo.toml version as-is** rather than incrementing again. That is the version those PRs were intended for.
## Step 1: Determine Version and Show Changelog
```bash
- cd ~/code/freenet/freenet-core/main
+ cd <your freenet-core checkout>
git pull origin main # MUST pull first
LAST_TAG=$(git describe --tags --abbrev=0)
CARGO_VERSION=$(grep "^version" crates/core/Cargo.toml | cut -d'"' -f2)
echo "Last release tag: $LAST_TAG"
echo "Cargo.toml says: $CARGO_VERSION"
- echo
- echo "Commits since $LAST_TAG:"
git log --oneline "$LAST_TAG"..HEAD
+ git rev-parse HEAD # must equal the SHA you soaked in P2
```
- Pick the target version (see Arguments above). Present the user with current version, target version, and a categorized changelog. **Confirm before triggering.**
+ Pick the target version (see Arguments above). Present the user with the current version, target version and a categorised changelog. **Confirm before triggering.**
## Step 2: Trigger the Release Workflow
+ **Only after P0-P4 have passed for the current `origin/main` SHA.**
+
```bash
gh workflow run release.yml --repo freenet/freenet-core --field version=X.Y.Z
```
- Available inputs (see `gh workflow view release.yml --yaml`):
- - `version` — required for explicit version; leave blank to auto-patch-bump from the latest crates.io release.
- - `skip_tests` — skip pre-release local tests (CI still runs on the PR).
- - `dry_run` — show what would happen without publishing.
+ Inputs (see `gh workflow view release.yml --yaml`):
+ - `version`: leave blank to auto-patch-bump from the latest crates.io release.
+ - `skip_tests`: skips the pre-release local tests (CI still runs on the PR).
+ - `dry_run`: skips the tag and the GitHub release, **but still opens and auto-merges the version-bump PR**. Treat it as a real trigger.
+
Confirm the run started:
```bash
- sleep 5
gh run list --workflow release.yml --repo freenet/freenet-core --limit 1 \
--json databaseId,status,event,createdAt
```
## Step 3: Watch the Release Run
- The workflow takes ~20–30 minutes (version-bump → release PR → merge_group full-suite CI → crates.io → GitHub release → cross-compile binaries). Use `gh run watch` in a backgrounded `Bash` call so the harness notifies on completion — never manually poll.
+ The workflow takes about 20-30 minutes. Use `gh run watch` in a backgrounded call so you are notified on completion; never poll by hand.
```bash
+ VER=X.Y.Z # the version being released; used below
RUN_ID=$(gh run list --workflow release.yml --repo freenet/freenet-core --limit 1 --json databaseId -q '.[0].databaseId')
gh run watch "$RUN_ID" --repo freenet/freenet-core --interval 30 --exit-status
+ gh run view "$RUN_ID" --repo freenet/freenet-core --log-failed | tail -100 # on failure
```
- If the run fails, inspect the failing job:
+ `release.yml` going green is **not** the end. The tag push starts a `cross-compile.yml` run, and that run builds the binaries, runs **Gate A** (which blocks publish), publishes to crates.io, un-drafts the release and runs **Gate B**. Watch that run too:
```bash
- gh run view "$RUN_ID" --repo freenet/freenet-core --log-failed | tail -100
+ gh run list --workflow cross-compile.yml --repo freenet/freenet-core --branch "v$VER" --limit 1 \
+ --json databaseId,status,conclusion
+ gh run watch <that id> --repo freenet/freenet-core --interval 30 --exit-status
```
- Common failure modes are below (Step 5).
+ **Check that what shipped is what you soaked.** The tag is the commit the version-bump PR merged as, and that PR waits roughly 20-75 minutes in the merge queue. Anything that landed on `main` in that window ships too.
- ## Step 4: Verify the Release Artifacts
+ ```bash
+ git fetch --tags origin
+ git diff --stat <soaked-sha> "v$VER" # must touch only version files (Cargo.toml/Cargo.lock/changelog)
+ ```
- After the release workflow succeeds, the `release.published` event fires and the downstream cascade runs automatically. Verify each piece landed:
+ If the diff touches anything else, the soak did not cover the release. Treat that like a soak failure: tell the user, and decide between soaking the tag and watching closely, or rolling back.
+ ## Step 4: Verify the Release Artifacts
+
```bash
VER=X.Y.Z
-
- # crates.io
- cargo search freenet --limit 1 # should show new version
- cargo search fdev --limit 1 # should show bumped fdev
-
- # GitHub release + binaries
- gh release view "v$VER" --repo freenet/freenet-core
+ cargo search freenet --limit 1 # new version
+ cargo search fdev --limit 1 # bumped fdev
gh release view "v$VER" --repo freenet/freenet-core --json assets --jq '.assets[].name'
+ gh run list --workflow gateway-update.yml --repo freenet/freenet-core --limit 1
+ gh run list --workflow release-announce.yml --repo freenet/freenet-core --limit 1
```
- **Required platform binaries** (all must be attached before the cascade is healthy):
+ **Required binaries:**
- `freenet-x86_64-unknown-linux-musl.tar.gz`
- `freenet-aarch64-unknown-linux-musl.tar.gz`
- `freenet-x86_64-apple-darwin.tar.gz`
- `freenet-aarch64-apple-darwin.tar.gz`
- `freenet-x86_64-pc-windows-msvc.zip`
- `freenet.exe`
- - corresponding `fdev-*` archives
+ - the corresponding `fdev-*` archives
- `SHA256SUMS.txt`
- Check the downstream cascade fired (these auto-trigger on `release.published`):
-
- ```bash
- gh run list --workflow gateway-update.yml --repo freenet/freenet-core --limit 1
- gh run list --workflow release-announce.yml --repo freenet/freenet-core --limit 1
- ```
-
- Both should show recent runs initiated after the release. If either is missing, the `RELEASE_PAT` secret may have expired (see freenet-core's AGENTS.md "Release Workflow & RELEASE_PAT" section).
+ Both cascade runs should show recent runs started after the release. If either is missing, `RELEASE_PAT` may have expired (see freenet-core's AGENTS.md).
## Step 5: River Compatibility Smoke Test
- Before relying on the auto-announcements, verify River clients can still talk to the gateway. Protocol changes (new wire variants, streaming defaults, serialization changes) can silently break River even when all freenet-core tests pass.
-
- The current Freenet Official room owner VK lives in the `river-official-room` skill — do NOT hardcode it here; it rotates when the room is recreated.
+ Protocol changes can silently break River clients even when every freenet-core test passes. Examples are new wire variants, streaming defaults and serialization changes. Get the current Freenet Official room owner VK from the `river-official-room` skill; it rotates, so do not hardcode it.
```bash
- ROOM_VK=$(...) # from river-official-room skill
- cd ~/code/freenet/river/main
+ cd <your river checkout>
cargo run -p riverctl -- member list "$ROOM_VK"
```
- If this fails with a bincode deserialization error: **STOP.** A protocol or serialization change broke River compatibility. Roll back the release (see "Rollback" below), file an issue, and rebuild River against the new stdlib before retrying.
-
- (v0.2.11 incident: enabling WebSocket streaming by default broke riverctl pinned to stdlib 0.1.40 — `StreamHeader`/`StreamChunk` variants weren't deserializable. This smoke test catches that class of regression.)
-
- ## Step 6: Post-Release Network Verification
-
- Wait 10–15 min for gateways to auto-update via the release-agent HMAC POST, then verify:
+ A bincode deserialization error means **STOP**: roll back (see "Rollback"), file an issue, and rebuild River against the new stdlib. In v0.2.11, enabling WebSocket streaming by default broke riverctl, which was pinned to stdlib 0.1.40.
- 1. **Gateway versions** — each gateway is running the new version. Use the `freenet-gateway-ops` skill for SSH'd version + log checks on nova / vega / technic.
- 2. **Gateway logs clean** — no new error patterns, no rapid log growth (normal is ~1 MB/h; faster signals a problem).
- 3. **Network health** — telemetry shows peers connecting, contracts propagating, subscriptions working. Use `freenet-telemetry-monitor` for raw event queries or `freenet-telemetry-dashboard` for the live view.
+ ## Step 6 (precondition P5): Post-cut telemetry monitoring through the 0-4h stagger
- Watch for:
+ Peers adopt a release over a **0-4h decentralised-discovery stagger**, so a regression in the shipped binary keeps spreading for hours after the gateways look fine. Keep monitoring until the stagger has fully elapsed. The release is not done when the workflow goes green.
- - **Log spam** — same message repeating hundreds of times (can fill disks within hours).
- - **New error patterns** — errors not present before the release.
- - **Connection failures** — `connection refused`, `timeout`, `handshake failed`.
- - **Resource issues** — `out of memory`, `no space left`, `too many open files`.
+ 1. **Gateways (+10-15 min):** each gateway runs the new version, and its logs are clean against the P2 baseline. Maintainers: host access is in the local `freenet-release` and `freenet-gateway-ops` skills. Grep for signatures specific to the subsystems **this** release touched, not just generic errors.
+ 2. **Telemetry at roughly +30 min, +1 h, +2 h and +4 h**, compared with the P3 baseline:
+ - version adoption is climbing
+ - GET/SUBSCRIBE/UPDATE success rates and latency hold
+ - connection counts hold
+ - no new error classes appear
+ - log growth is normal
+ 3. **Roll back immediately** on crashes, log spam, data loss or a clear success-rate regression. Open an issue with the evidence, and post a rollback notice on Matrix.
- If a critical issue surfaces: **roll back immediately**, create a GitHub issue with the symptom + logs, post a rollback notice via the release-announce path or Matrix manually.
+ Watch for log spam (it can fill disks within hours), new error patterns, connection failures (`connection refused`, `timeout`, `handshake failed`) and resource exhaustion (`out of memory`, `no space left`, `too many open files`).
## Rollback
- The rollback script is still the right tool for un-doing a release:
-
```bash
- cd ~/code/freenet/freenet-core/main
- ./scripts/release-rollback.sh --version <VERSION> # keeps crates.io
- ./scripts/release-rollback.sh --version <VERSION> --yank-crates # irreversible
+ ./scripts/release-rollback.sh --version <VERSION> # keeps crates.io
+ ./scripts/release-rollback.sh --version <VERSION> --yank-crates # disruptive; undo with cargo yank --undo
./scripts/release-rollback.sh --version <VERSION> --dry-run
```
## Common Workflow Failures
- - **PR title fails Conventional Commits.** The release workflow opens its own PR with title `build: release X.Y.Z`. If the commit-msg hook is enforced differently in CI, patch via `gh api repos/freenet/freenet-core/pulls/<PR> --method PATCH -f title="build: release X.Y.Z"`.
- - **Merge queue stuck on the release PR.** Release `merge_group` entries run the full suite (Unit & Integration, Simulation, NAT Validation) as the pre-publish gate (#3973). Expect 20–30 min. Non-release merge_group entries skip Simulation and NAT Validation, so this is the only place those run against the rebased commit — don't manually merge to skip.
- - **Cascade didn't fire after `release.published`.** Suggests `RELEASE_PAT` is missing or expired. `release.yml` emits a `::warning::` when the secret is absent; check the workflow logs and rotate the PAT per AGENTS.md. As a manual fallback: `gh workflow run gateway-update.yml --field version=X.Y.Z` and `gh workflow run release-announce.yml --field version=X.Y.Z`.
- - **Cross-compile attached partial binaries.** Some platforms can fail independently. `gh run view <cross-compile run-id> --log-failed` shows which target failed. Re-run that single platform via `gh workflow run cross-compile.yml`.
+ - **PR title fails Conventional Commits.** The release PR title must be `build: release X.Y.Z`. Patch it with `gh api repos/freenet/freenet-core/pulls/<PR> --method PATCH -f title="build: release X.Y.Z"`.
+ - **Merge queue is slow on the release PR.** Release `merge_group` entries run the full suite (Unit & Integration, Simulation, NAT Validation) as the pre-publish gate (#3973). Expect 20-30 min. Do not merge by hand to skip it.
+ - **The cascade didn't fire after `release.published`.** `RELEASE_PAT` is missing or expired. The manual fallback is `gh workflow run gateway-update.yml --field version=X.Y.Z` and `gh workflow run release-announce.yml --field version=X.Y.Z`.
+ - **Cross-compile attached partial binaries.** Find the failed target with `gh run view <run-id> --log-failed`, then re-run it with `gh run rerun <run-id> --failed`, or with `gh workflow run cross-compile.yml`.
+ - **Re-announcing by hand.** Check that every required binary is attached first. The automatic cascade waits for `release.published`, which fires only after the binaries exist; a manual re-trigger does not wait.
+ - **A draft release has NO Windows assets.** Windows Authenticode signing is fail-closed by design. The `Verify signatures` step throws before upload, so an empty Windows slot is the expected shape of a signing failure. Read that step's output first. **Never strip the signing steps** to force a release through.
## Version Scheme
- - **freenet:** `0.2.X` — patch incremented each release.
- - **fdev:** `0.3.X` — patch auto-incremented by the workflow (independent versioning).
+ - **freenet:** `0.2.X`, with the patch incremented each release.
+ - **fdev:** `0.3.X`, with the patch auto-incremented by the workflow (independent versioning).
## Incident Learnings
- Recurring issues the pipeline has been hardened against:
-
- - **Don't manually run individual release steps** (`cargo publish`, `gh release create`, `gh release edit`). The workflow handles draft releases and binary waits so users never see a version before its binaries exist. Doing steps by hand caused a user-facing 404 during v0.1.177.
- - **Don't announce before binaries exist.** Cross-compile takes 15–20 min; gateway auto-update (especially aarch64) depends on the binaries being attached. The `release-announce.yml` and `gateway-update.yml` cascade triggers on `release.published`, which fires only after binaries are attached, so this is automatic — but if you're manually re-triggering announcements, check binary presence first.
- - **Log spam can fill disks within hours.** Always do the post-release health check in Step 6. v0.2.62 shipped with River-saturation log spam that filled gateways at ~10 GB/day (#4251); v0.2.63's release wave addresses both root-cause (#4253) and symptom (#4252).
- - **Streaming default broke riverctl** (v0.2.11). Always run the Step 5 River smoke test before considering the release fully shipped.
- - **PR title must use `build:` prefix**, not `chore:` — the commit-msg hook only allows feat/fix/docs/style/refactor/perf/test/build/ci.
- - **PATH shadowing.** Old `cargo install freenet` may leave a stale binary at `~/.cargo/bin/freenet` shadowing `/usr/local/bin/freenet`; always use absolute paths when verifying running versions.
- - **Binary on disk ≠ running process.** Verify via `systemctl show -p MainPID` + `/proc/PID/exe`, not just the file on disk.
- - **Release `merge_group` runs the full suite as the pre-publish gate** (#3973). Don't try to skip it.
+ - **Releasing without the real-NAT soak.** The soak and auto-update gates once lived only in one maintainer's local skill, so a session using this skill nearly cut a release without them. That is why they are MANDATORY PRECONDITIONS here.
+ - **A broken updater is unrecoverable in the field.** v0.2.120/v0.2.121 (#5221): about 1,100 nodes were updated by hand. The soak peer's auto-update had been disabled for a measurement and was never re-enabled.
+ - **A `-dirty` soak binary has auto-update off** (#5613), so the soak passes while proving nothing. Check `--version` on the artifact.
+ - **Don't run release steps by hand** (`cargo publish`, `gh release create`, `gh release edit`). Doing so caused a user-facing 404 during v0.1.177.
+ - **Log spam can fill disks within hours.** v0.2.62 hit about 10 GB/day on gateways (#4251).
+ - **The streaming default broke riverctl** (v0.2.11). Always run the Step 5 smoke test.
+ - **PATH shadowing.** A stale `~/.cargo/bin/freenet` can shadow the installed binary, so use absolute paths when verifying versions.
+ - **A binary on disk is not the running process.** Verify with `/proc/<MainPID>/exe`.
## Success Criteria
- Release is complete when:
+ The release is complete when all of these hold:
- - ✓ `release.yml` workflow succeeded
+ - ✓ P0-P4 passed and were reported **before** the trigger, and the soak covered the exact ship SHA for 30 minutes
+ - ✓ the `release.yml` workflow succeeded, AND the tag's `cross-compile.yml` run succeeded, including Gate A and Gate B
+ - ✓ `git diff <soaked-sha> vX.Y.Z` touches only version files
- ✓ crates.io shows the new version for both `freenet` and `fdev`
- - ✓ GitHub release tagged with all required binaries attached
- - ✓ `gateway-update.yml` and `release-announce.yml` cascade runs succeeded
- - ✓ River smoke test passes
- - ✓ Gateways running the new version, logs clean
- - ✓ Network telemetry healthy
+ - ✓ the GitHub release is tagged, with all required binaries attached
+ - ✓ the `gateway-update.yml` and `release-announce.yml` cascade runs succeeded
+ - ✓ the River smoke test passes
+ - ✓ gateways run the new version and their logs are clean
+ - ✓ telemetry stayed healthy against the P3 baseline through the full 0-4h stagger