RigForge follows Semantic Versioning. The current version is
tracked in VERSION and the history in CHANGELOG.md.
- MAJOR: incompatible
config.json/ CLI / behavior changes. - MINOR: new, backwards-compatible functionality.
- PATCH: backwards-compatible fixes.
From 1.0.0 on, the config.json and CLI surface is stable, so a breaking change bumps MAJOR. (Pre-1.0
0.x releases could break the interface between minor versions while it settled.)
Work lands on develop (the integration branch); a release is the point where develop is
promoted to main and tagged. The steps below build the release commit on develop, merge it to
main, and tag from main.
-
Ensure
developis green:make test(andmake test-e2eif Docker is available). -
Full real-hardware e2e (the release gate). CI exercises everything it can (lint, the dependency-free suite, the Docker
/etce2e, the coverage gate), but it can't compile XMRig, reserve HugePages, write MSRs, set the governor, or actually hash. So on a real Linux rig, run the genuine deploy end to end and assert each step:sudo bash tests/e2e-real.sh provision # real deps + XMRig build + tuning + kernel tuning + service sudo reboot # HugePages (1G + GRUB cmdline) take effect on boot; reconnect sudo bash tests/e2e-real.sh verify # doctor (HugePages/MSR/governor/service) + bench (real H/s) + a short tune + a live auto-tune pass sudo bash tests/e2e-real.sh control # the writable control path (#236) against real systemd: enable, POST a change, poll to applied, revert sudo bash tests/e2e-real.sh upgrade # the remote-upgrade chain (#308/#322) with REAL git: noop + refused-tag rollback legs, plus a mandatory forward leg that auto-derives the previous real release tag -> current and proves it, then reverts (skip with a reason: E2E_UPGRADE_SKIP_REASON="...") sudo bash tests/e2e-real.sh watchdog # the thermal-hold path (#349) against the real sensor and real systemd: lowers max_temp_c below the live reading, runs the verb once, asserts the stop + hold marker + journal evidence, restores on every exit path (skips explicitly when no temperature reading is available) sudo bash tests/e2e-real.sh perf # offline bench vs the committed per-host baseline + best-ever history (the release perf gate) sudo bash tests/e2e-real.sh teardown # uninstall + assert a clean revert
When a live Pithead stack is reachable, also run the worker↔stack contract gate (stack on its latest release tag — record
pithead versionin the run log). It asserts the mining round-trip, the:8080API contract, stratum auth (passE2E_STRATUM_PASSif the stack uses one), dashboard visibility (E2E_DASH_URL), and that the sister API does not shave hashrate under polling load:PITHEAD_URL=<stack-host>:3333 sudo -E make e2e-pithead
Both gates carry the standardized performance checks (see
tests/README.md› Performance testing):e2e-real'sperfphase compares the offline bench against the committed per-host baseline intests/perf-baselines/, ande2e-pithead'sapi-impactphase proves the sister API doesn't shave live hashrate. A perf regression fails the gate — investigate or consciously re-record the baseline before tagging.Each phase must report
E2E-REAL (<phase>): PASS. This proves a release bundle actually builds, tunes, and hashes on real hardware, which the suites can't since they all stub XMRig.- Put a real, reachable pool in
config.jsonfirst. Without one,setupwrites an unroutable placeholder andverifyfails the connect + share-submission round-trip. That round-trip is mandatory, since proving the rig really mines is the whole point of the gate. Pointpools[0].urlat a real low-difficulty pool you control (e.g. the stack's test pool). For a deliberate offline smoke run with no pool on hand, setE2E_ALLOW_OFFLINE_POOL=1to downgrade it to an explicit skip. - Quick subset:
make smoke(bench-only) is the fast version when you just need to confirm a built worker still hashes; the fulle2e-realflow above supersedes it for a real release. - Kept out of CI on purpose (a real build + HugePages + mining are flaky by nature and against Actions' ToS); it's a manual pre-tag gate the releaser runs.
- Put a real, reachable pool in
-
In
CHANGELOG.md, move the## [Unreleased]entries under a new## [X.Y.Z] - YYYY-MM-DDheading, then leave a fresh empty## [Unreleased]above it. -
Bump
VERSIONtoX.Y.Z. -
Commit the two together on
develop:git commit -am "release: vX.Y.Z" git push origin develop -
Promote
developtomain. Open a pull request first — it carries the review, the CI run and the audit trail for the promotion, andmain's ruleset requires one:gh pr create --base main --head develop --title "release: vX.Y.Z" \ --body "Promote develop to main for the vX.Y.Z release."
Review it, then land it with a fast-forward push rather than the merge button, so
mainends up ondevelop'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 frommain: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 Branchruleset targetsrefs/heads/mainonly (developcarries no rules at all) and haspull_request,non_fast_forwardanddeletion, withOrganizationAdminbypass atbypass_mode: always. The push satisfiesnon_fast_forward— that rule blocks force-pushes, and this is a genuine fast-forward — and needs the bypass forpull_request. Untested: no push has yet relied on that bypass, so confirm it on the next promotion. If it is refused, fall back togh pr merge --merge --adminand then back-merge (git merge origin/mainondevelop) to restore the invariant before the next release.The invariant is the point:
mainmust stay an ancestor ofdevelop. Bothgh pr mergemodes break it, in different ways, and the repo has been broken by each in turn.--mergewrites a merge commit ontomainthatdevelopnever 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 parent3220f57is the last commit the two branches shared. Nothing back-merged it, so they never re-converged.--rebaseis worse: it rebases develop's commits ontomain, minting new shas, somainends up carrying twins of commitsdevelopstill holds under their original shas. Later promotion PRs then come backCONFLICTINGand need a hand-built reconcile commit —cfd92fa(v1.15.0) and60aa883(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
mainthatdeveloplacked, untilde4e781healed 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/developbefore promoting. If a hotfix ever lands directly onmain, back-merge it (git merge origin/mainondevelop) to restore the invariant before the next release. -
Tag and push from
main(annotated tag, matchingVERSION) once the PR is merged:git checkout main && git pull --ff-only origin main git tag -a vX.Y.Z -m "RigForge vX.Y.Z" git push origin main --follow-tags
Pushing the tag triggers the release pipeline
(.github/workflows/release.yml), which:
- verifies the tag matches
VERSION(the build fails otherwise), - packages the deploy bundle (
rigforge.sh,util/,systemd/,config.minimal.json,config.reference.json,README.md,docs/,images/,LICENSE,VERSION) asrigforge-vX.Y.Z.zipand.tar.gz(tests/,.github/, and other dev files are excluded), - generates
SHA256SUMSfor the artifacts, - pulls that version's section from
CHANGELOG.mdas the release notes, - creates the GitHub Release as a draft. Review the generated notes and bundles, then click
Publish (pre-1.0
0.xtags are marked pre-release;1.0.0+ are full releases).
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). It is whichever rig ran the
gate, which is not always miner-0 despite tests/README.md
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
SECURITY.md › Release integrity).
The release is created as a draft so a human reviews it before it goes public, a deliberate gate for a tool that installs a root miner. Drop
--draftfromrelease.ymlto auto-publish on tag instead.
- Keep
VERSIONand the latestCHANGELOG.mdheading in lock-step; the test suite checksVERSIONis valid SemVer. VERSIONis also surfaced at runtime:rigforge.sh version(or--version/-v) reads it, so a release tag, the changelog heading, and what the script reports all stay in agreement.