Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 53 additions & 16 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,55 @@ promoted to `main` and tagged. The steps below build the release commit on `deve
git push origin develop
```

6. Promote `develop` to `main` **through a pull request** — `main` is a protected release branch, so the
promotion goes through a reviewable PR (its own gate + audit trail), not a direct push:
6. Promote `develop` to `main`. Open a pull request first — it carries the review, the CI run and the
audit trail for the promotion, and `main`'s ruleset requires one:

```bash
gh pr create --base main --head develop --title "release: vX.Y.Z" \
--body "Promote develop to main for the vX.Y.Z release."
```

Review and merge it. Keep `main` linear with a **fast-forward (rebase) merge** so the tag sits on the
same commit as `develop`'s release commit:
Review it, then land it with a **fast-forward push** rather than the merge button, so `main` ends up
on `develop`'s release commit *exactly* — same sha, not merely the same tree — and stays linear.
GitHub closes the PR as merged once its commits are reachable from `main`:

```bash
gh pr merge --rebase --admin # fast-forward main to develop; --admin lets the releaser merge
git fetch origin
git merge-base --is-ancestor origin/main origin/develop \
|| { echo "NOT a fast-forward — main has commits develop lacks; back-merge first (see below)"; exit 1; }
git push origin develop:main
```

The `Main Branch` ruleset targets `refs/heads/main` only (`develop` carries no rules at all) and has
`pull_request`, `non_fast_forward` and `deletion`, with `OrganizationAdmin` bypass at
`bypass_mode: always`. The push satisfies `non_fast_forward` — that rule blocks force-pushes, and
this is a genuine fast-forward — and needs the bypass for `pull_request`.
**Untested: no push has yet relied on that bypass, so confirm it on the next promotion. If it is
refused, fall back to `gh pr merge --merge --admin` and then back-merge (`git merge origin/main` on
`develop`) to restore the invariant before the next release.**

> **The invariant is the point: `main` must stay an ancestor of `develop`.** Both `gh pr merge`
> modes break it, in different ways, and the repo has been broken by each in turn.
>
> `--merge` writes a merge commit onto `main` that `develop` never receives. That is how the last
> divergence started: `23fcd27` ("release: v1.12.0 (promote develop to main via merge)",
> 2026-07-19) has two parents, and its second parent `3220f57` is the last commit the two branches
> shared. Nothing back-merged it, so they never re-converged.
>
> `--rebase` is worse: it *rebases* develop's commits onto `main`, minting new shas, so `main` ends
> up carrying **twins** of commits `develop` still holds under their original shas. Later promotion
> PRs then come back `CONFLICTING` and need a hand-built reconcile commit — `cfd92fa` (v1.15.0) and
> `60aa883` (v1.15.1) are two of those, and PR #368 is a promotion that could not be merged at all.
>
> Five releases were cut while diverged (v1.13.0, v1.13.1, v1.14.0, v1.15.0, v1.15.1), drifting to
> 37 commits on `main` that `develop` lacked, until `de4e781` healed it. In that whole window **no
> tag ever sat on develop's release commit** — every one of v1.12.0…v1.15.1 is unreachable from
> develop as it stood before the heal. A fast-forward is what puts them back on the same commit.
>
> Verify with `git merge-base --is-ancestor origin/main origin/develop` before promoting. If a
> hotfix ever lands directly on `main`, back-merge it (`git merge origin/main` on `develop`) to
> restore the invariant before the next release.

7. Tag and push from `main` (annotated tag, matching `VERSION`) once the PR is merged:

```bash
Expand All @@ -108,17 +142,20 @@ Pushing the tag triggers the release pipeline
After a rig is re-tagged, record its benchmark for the release
(`E2E_PERF_TAG=vX.Y.Z E2E_PERF_RECORD=1 sudo bash tests/e2e-real.sh perf` on the rig) and commit
the updated `tests/perf-baselines/` files — the per-release history is what lets the perf gate
catch slow drift across releases (see `tests/perf-baselines/README.md`). In practice that means
miner-0 every time, since the release gate itself always runs there (see
[`tests/README.md`](./tests/README.md#the-shared-rig-miner-0)); the rest of the fleet isn't re-tagged
on every release, so its baselines are only as fresh as the last time each rig was actually
touched. `tests/perf-baselines/` legitimately carries gaps between releases for rigs that went
untouched — it is not a promise that every rig has an entry for every tag. The recording is also
the per-rig perf gate (#214): it judges against the committed baseline and best-ever history
before writing, refuses to record a regressed number (fix it, or consciously override with
`E2E_PERF_FORCE=1`), so a failed rig means investigate before calling it healthy. Once a rig's
baseline is merged, reset its copy (`sudo git checkout -- tests/perf-baselines/` in
`/opt/rigforge`): the recording dirties the rig's checkout, and the *next* release's
catch slow drift across releases (see `tests/perf-baselines/README.md`). It is whichever rig ran the
gate, which is **not** always miner-0 despite [`tests/README.md`](./tests/README.md#the-shared-rig-miner-0)
calling it the shared rig: v1.15.0 was gated on miner-2 and v1.15.1 on miner-3, and miner-0 currently
cannot pass the gate at all — it dual-boots Windows, so Secure Boot is enabled, kernel lockdown
(`integrity`) denies every MSR write, and `doctor` counts that as an issue and exits non-zero. Pick a
rig with Secure Boot off. The rest of the fleet isn't re-tagged on every release, so its baselines are
only as fresh as the last time each rig was actually touched. `tests/perf-baselines/` legitimately
carries gaps between releases for rigs that went untouched — it is not a promise that every rig has
an entry for every tag. The recording is also the per-rig perf gate (#214): it judges against the
committed baseline and best-ever history before writing, refuses to record a regressed number (fix
it, or consciously override with `E2E_PERF_FORCE=1`), so a failed rig means investigate before
calling it healthy. Once a rig's baseline is merged, reset its copy
(`sudo git checkout -- tests/perf-baselines/` in `/opt/rigforge`): the recording dirties the rig's
checkout, and the *next* release's
`git checkout <tag>` aborts on exactly those files (this bit both the v1.4.0 and v1.5.0 deploys).

To verify a downloaded bundle: `sha256sum -c SHA256SUMS` (see
Expand Down