-
0.3.0 Stable
released this
2026-07-21 18:06:51 +00:00 | 125 commits to main since this releaseFixed
- Deleting a shipped release heading from
CHANGELOG.mdis caught on every PR
(#98, heavy-duty/box#122) - The heading-uniqueness check no longer sits behind git conditions it does not
need (#98, heavy-duty/box#143) - An unreadable check rollup no longer reads as "nothing is failing" (#90)
- CI runs
test/labels-reconcile.sh, which it had never run (#90) state:needs-humanno longer appears on PRs a human cannot merge
(#87, heavy-duty/box#136)- A missing
/run/sshdno longer reads as a broken sshd config (#92) - CI's shellcheck sweep reaches
.github/scripts/(#70) - Ctrl-D at the
rig uninstallconfirm aborts out loud (#68) users applytells "revoke everyone" apart from a truncated users file (#65)
Added
- CI refuses a release PR with no drill record at
drills/<version>.md rig platform— what this machine is, computed at run time, stored nowhere
(#64)/etc/rig/manifestrecords which rig converged a machine, and when (#61)
Changed
state:needs-humanis set at handoff, not by the cron (#96)- PR labels split into two axes:
state:*(whose ball) andblocker:*(what
is in the way) (heavy-duty/box#137) - BREAKING:
--class human|serveris now--root-door closed|open; old
markers still resolve (#77) - BREAKING: the box tenant roles carry a
-boxsuffix (#76) - BREAKING: machine roles carry a
-serversuffix, andstaging-serveris
back (#76) - Changelog entries are one line each, and the whole file now follows the rule
(#100)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
6 downloads
- Deleting a shipped release heading from
-
0.2.0 Stable
released this
2026-07-19 23:04:21 +00:00 | 193 commits to main since this releaseAdded
users applygrants the box tier, not just its socket (#49) — role
boxresolved to exactly one action,usermod -aG incus. That is the
socket; it is step 1 of the fivebox grantperforms, so every box-role
user still needed an admin to runbox grant <user>by hand before their
firstbox newwould do anything but refuse ("your project has no box-net
profile"), and until that admin arrived they held anincusmembership
with no converged project — incus-user would lazily hand them a stock
unhardened NAT bridge, which is worse than no grant at all. Onhost=yes
apply now callsbox grantper box-role user, afteruseradd(grant
refuses an unknown account) and with the group ADD deferred to grant, so a
grant that fails partway can take the socket back with it. Failures split
the way thehost=guard beside them already splits: a missingboxCLI
onhost=yesdies (a broken VM host), a per-user grant failure warns and
continues (one box-role user must not stop apply for the fleet).host=no
and marker-less boxes keep their existing skip-with-warning. An
incus-adminmember is warned, not fatal —box grantrefuses them today,
which heavy-duty/box#99 fixes box-side with no rig change needed.
Changed
-
BREAKING:
rig bootstraptakes the users file, and requires it (#51) —
bootstrap already knew everything else about what a box is (class, host,
join, hostname) and wrote/etc/rig/roleto say so; the users file was the
last piece of that answer it did not take, so bring-up was two commands and
the second was the forgettable one.--users <path>now runs theusers applyconvergence as bootstrap's final phase — after the traits, after
the verified tailnet join, after the role marker (apply reads that
marker), and after thehost=yesbox install (so box-role users find the
incusgroup box's ownsetup-hostbuilt). One command, and the box has
its people on it. The file is still passed per invocation and never
persisted;--users -is refused, because bootstrap's stdin belongs to
the pre-auth key prompt.Migration: every existing
rig bootstrapinvocation must add--users <path>or--no-users. Omitting both is now a usage error (exit 2)
naming both flags, and passing both is a usage error too. Scripted
bring-up that already ranrig users applyas a separate step can either
fold it in (--users ./users, and drop the separate call) or keep the old
shape verbatim by adding--no-users. Required onclass=serveras well
asclass=human: a server nobody logs into routinely is exactly where
shared-root access rots, and per-human accounts keep attribution intact
for the times someone does go in — so the complete path is the default
path, and skipping it is deliberate rather than an omission that looks
identical to forgetting. The box TENANT roles (claude|codex|grok| staging) take neither flag: a guest is minted non-interactively by box,
never joins the tailnet, and has no SSH door of its own — entry isbox shell, gated by the host'sincusgrants.A bad users file is caught up front now (the same parser apply uses,
beforeapt, the hostname change, and any spent pre-auth key), and on
host=yeswithRIG_SKIP_BOX_INSTALL=1a box-role user with noincus
group refuses immediately instead of a hundred lines later — the one case
where the outcome is already certain. rig still never installs Incus and
never callsbox setup-hoston its own account; every other way that step
can fail lands inusers apply's existing refusal, unchanged.
Fixed
-
A release no longer disarms the changelog under the PRs still in
flight (#67) — the ceremony stamps## Unreleasedto
## X.Y.Z — YYYY-MM-DDand stops. Every PR authored before that merge
wrote its entry under## Unreleased; with the heading gone, git files
the entry under whatever now occupies the position — the release that
already shipped. There is no conflict, because the stamped heading and
the incoming entry never overlap textually, so the one signal an author
relies on ("git told me to look") is absent exactly when the outcome is
wrong. It happened here: #60's #58 entry landed inside## 0.1.0at
67386b4and was repaired two minutes later by0ff520c; #54 would
have filed a BREAKING entry the same way. The published release body
is never affected —release.ymlextracts it from the tree at the tag,
before the late merges land — so the only file that drifts is the one
only maintainers read, which is why it survived a whole release batch
unnoticed. Fixed in both halves the failure has. The ceremony now
re-arms: it adds a fresh empty## Unreleasedabove the section it
just stamped, so a late merge has somewhere correct to land with no
author action. That belongs to the ceremony step in
CONTRIBUTING.md, not torelease.yml— no workflow
has ever touched the heading; the stamping was always by hand, and the
-devre-arm the workflow does perform was only ever aboutVERSION.
Andtest/release.shnow keys its guard toVERSIONrather than
demanding a literal heading: a stamped top section is legal exactly when
VERSIONis bare, and the moment it carries-dev— main, where
feature PRs merge — the top section must be## Unreleased. That
distinguishes the two states the old check collapsed into one, so it
catches a disarmed main without re-breaking the ceremony's own tree
the way the pre-#44 guard did. The rule is proven against seven
constructedVERSION+CHANGELOG.mdpairs, including a re-armed
ceremony whose top section is legitimately empty — the state the old
non-empty assert would have rejected. box and cast carry the same flow
and the same exposure (heavy-duty/box#96); cast is disarmed onmain
as of this writing and is getting the sibling fix. -
A
host=nobox with anincusgroup no longer hands out the bare
socket (#58) —users applyconsulted thehost=trait only when group
incuswas ABSENT (die onhost=yes, skip onhost=no). When the group
was PRESENT the trait was never asked, so ahost=noor marker-less box
that nonetheless carried the group —box setup-hostran, then the box was
re-bootstrapped with other traits — gave every box-role user a bare
usermod -aG incus: the socket with no tier behind it, whichincus-user
answers by lazily building an UNHARDENED project under whoever opens it
(incusbr-<uid>, NAT on v4 and v6, no ACL, nodns.mode=none, no port
isolation). The marker now decides in BOTH directions, through one new pure
gate (assert_marker_hosts_vms, testable against fixture markers non-root
likeassert_marker_human): the box role applies only where the box CLAIMS
to host VMs, so the verdict is identical whether or not the group exists.
The machine deliberately does not overrule the marker — but the skip is not
silent either: when the group exists and the trait disagrees, the warning
names the contradiction andrig bootstrapas the repair. On such a box
exact-membership convergence now strips box-role users out ofincus, on
the same reasoning: a membership inherited from a previous life is the same
half-grant as a freshly added one. -
Dropping the box role revokes through
box, not behind its back
(#50) —users applyconverged groupincuswith a baregpasswd -d,
the same move it makes forrig-adminandrig. Those two are rig's;
incusis box's, andbox revokedoes strictly more with it: it says
out loud that supplementary groups are read at LOGIN, so a session the
dropped operator already holds keeps the Incus socket until it dies, and
hands overloginctl terminate-user <user>as the remedy. rig logged
removed <user> from incusand moved on, so an operator who dropped
someone from the users file and watched apply succeed believed the VM
access was gone — and was wrong for as long as that user held a session.
Both removal paths (the per-user convergence and the dropped-user sweep)
now callbox revoke, which keeps one owner for the group. Never
--purge: that deletes the user's boxes, images and project, and
destroying someone's running machines is not a convergence step — it
stays an explicit admin act. The exit code is not trusted (#12's lesson):
a revoke that returns 0 with the membership still standing has not closed
the socket, and rig falls back to removing the group itself, as it also
does where box is not installed. Every fallback path carries the session
warning, because the silence was the bug. -
rig bootstraprefuses a users file that names no users (#57) — an
empty, comments-only or whitespace-only file is not a parse error, so it
passed pre-flight, converged nothing, and left the box root-only: the exact
outcome--no-usersexists to make explicit, reached by the flag added to
guarantee the opposite. Bootstrap's pre-flight now catches the zero-user
parse — beforeapt, the hostname change, or a spent pre-auth key — and
refuses, naming--no-usersas the way to ask for a root-only box out loud.
Scoped torig bootstrap's contract only: a standalonerig users apply
against an emptied file is a real de-provisioning operation and is
unchanged.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
0.1.0 Stable
released this
2026-07-19 18:03:35 +00:00 | 211 commits to main since this releaseFixed
-
The release suite accepts the ceremony's own tree (#44) —
test/release.shdemanded a literal## Unreleasedheading in the real
CHANGELOG.md, extracting non-empty and containing#32. All three are
false by construction on therelease: X.Y.Ztree the ceremony's own PR
produces (it stamps that heading into## X.Y.Z — date), so the first
real release PR turned CI red and the flow blocked itself — invisible to
both fork rehearsals, which tag a branch (release.ymlruns;ci.yml
never does). The guard now asserts what it was for: whatever the TOP
##section is —Unreleasedbetween releases, the stamped version on
and right after one — the exactchangelog_sectionthe workflow runs
extracts it non-empty. The rotting issue-number grep is gone. -
The installer survives an environment with no
$HOME(#39) —
cloud-init'sruncmdrunsinstall.shwith no$HOMEset, and under
set -uthe first expansion died with a bash unbound-variable stack
instead of an install — found live by box#88's template seed, which
pinsHOME=/rootas its own scar. The installer now derives the home
fromgetentfor the effective user (root included) before any path
is built from$HOME, and when getent has no answer either it refuses
by name. Driven with a shim getent both ways: the derived-home install
lands, the no-answer refusal is pinned. (#41 — merged without its
entry; restored here at the release gate.) -
Headless credential prompts refuse loudly instead of dying silently
(#42) — the interactive credential prompts (TS_AUTHKEYinbootstrap,
RUNNER_TOKENinrunner install,RUNNER_REMOVE_TOKENin
runner remove, and both tokens inrunner repoint— a site the new
no-bare-read test caught after the issue counted three) were bare
read -rsp: with stdin not a tty (CI,
box exec, any script),readfails,set -eends the run, and the
log just stops — exit 1, no last word, measured live in the
2026-07-19 release drill. Each prompt now checks for a tty first and
dies naming the variable that unblocks an unattended run (runner removealso names--local), and everyreadis|| die-guarded so
EOF at a real prompt gets the same courtesy.db.shalready held the
line here; now all of rig does.
Added
-
Merging a release-labeled PR IS the release — and the release re-arms
main itself (#47) — the rig twin of heavy-duty/box#96, born of the
ceremony retro: the tag was a separate, manual, silent-when-forgotten
step, and a forgotten tag produces no red X.release.ymlnow fires on
pushes to main (fork-sourced ceremony PRs get a read-only token on
pull_requestevents), reading the transition from the push itself:
event.beforeto the pushed head. A decide step answers four states —
release-flow work merged under thereleaselabel (-devendstates,
the post-release window) no-ops green with a NOTICE; the two genuinely
ambiguous bare states refuse loudly; a true transition then requires a
merged,release-labeled PR behind the commit (read via the API — the
label is the operator's declared intent). Then, in the same job, it
API-creates the tag at the merge commit, publishes with the extracted
notes — and bumps main toX.Y.(Z+1)-devitself, direct push with a
loud open-a-PR fallback, so no follow-up bump PR exists on the paved
road. AGITHUB_TOKEN-created tag never fires the tag-push trigger, so
the paths cannot double-publish — and that tag-push path survives intact
as the documented manual fallback and backfill. -
Tagged releases, and an installer that installs them (#32) — the rig
half of the flow designed in heavy-duty/box#83, near-verbatim. A release
is a PR, then a tag: therelease: X.Y.ZPR bumpsVERSIONand stamps
this file's Unreleased section with version + date; the merge commit is
tagged bareX.Y.Z(box's tag scheme — novprefix).release.yml
turns the tag into the GitHub release — after asserting tag ==VERSION
(mismatch fails loudly and creates nothing) — with that version's section
of this file as the body, extracted by the samechangelog_sectionthe
test harness drives. No assets: for a pure-bash tree, GitHub's source
tarball for the tag IS the package.install.shnow defaults to the
latest release: the tag is resolved by following the
releases/latestredirect and reading theLocationheader — no API, no
token — and the download isarchive/refs/tags/<tag>.tar.gz.RIG_REF
picks the other two channels: a tag pins (refs/tagsoutranks a
same-named branch), a branch (RIG_REF=main) tracks the development
tree. Until 0.1.0 is cut the default channel has nothing to resolve and
dies saying exactly that, namingRIG_REF=mainas the way to install
today — it never falls back to main silently, because "I installed the
latest release" must not quietly mean "I installed whatever main was that
second". Step 5 of #32 — pinningBOX_REFin the host-installs-box path
— stays open until box cuts its next tagged release.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-