rig/CHANGELOG.md

75 lines
2.6 KiB
Markdown
Raw Normal View History

# Changelog
History before 0.1.0 lives in git — rig grew its version surface (`VERSION`,
`rig --version`, the side-by-side `versions/<v>` install layout; #35/#36)
on the way to cutting its first release, and this file starts there.
2026-07-21 14:50:19 +00:00
## 0.3.0 — 2026-07-21
fix(labels): state:needs-human means a human could merge it right now Ported from heavy-duty/box#137 (heavy-duty/box#136) so the three repos' reconcilers stay byte-identical. The state machine here was byte-identical to box's before this change, and remains so after -- only the scope:* taxonomy differs, correctly. decide_state() derived state from three inputs -- draft flag, requested reviewers, submitted reviews -- and read NOTHING about mergeability or checks. With the `if requested "$HUMAN"` short-circuit at the top of its precedence, the label was sticky: once the maintainer was requested, a PR read state:needs-human through conflicts, through red CI, through a force-push that staled every approval. This repo paid for it directly. During the ten-PR batch merged today, every merge re-conflicted the PRs below it through CHANGELOG.md, and each kept its state:needs-human label throughout -- inviting merges that could not happen. It was caught only by opening them one at a time, which is the work the label exists to save. The rule the label now keeps: state:needs-human means a human could merge this RIGHT NOW, so anything making that false outranks the request that put it there. CONFLICTING or failing checks -> state:needs-rebase (new; the agent's to fix) approvals staled by a push -> state:addressing (nobody reviewed this tree) An UNFINISHED round still yields to an explicit human request -- MISSING (nobody has reviewed yet) is a different fact from STALE (everyone reviewed something else). UNKNOWN mergeability is NOT treated as unmergeable: GitHub reports it for about a minute after every merge, and flapping every open PR through needs-rebase on each merge would be worse than the bug. A failed read degrades to the same "do not know" value. Also adds merge-next: queue order is intent, so the reconciler never sets it, only CLEARS it once the PR stops being mergeable-by-a-human. Fixtures 19 -> 29, including that UNKNOWN does not trigger needs-rebase and a draft outranks a conflict. No live dry-run evidence here -- this repo has no open PRs right now -- so the fixtures and box's live dry-run are the proof. Closes #87 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:26:54 +00:00
### Fixed
- Deleting a shipped release heading from `CHANGELOG.md` is 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-human` no longer appears on PRs a human cannot merge
(#87, heavy-duty/box#136)
- A missing `/run/sshd` no longer reads as a broken sshd config (#92)
- CI's shellcheck sweep reaches `.github/scripts/` (#70)
- Ctrl-D at the `rig uninstall` confirm aborts out loud (#68)
- `users apply` tells "revoke everyone" apart from a truncated users file (#65)
feat: rig platform — what is this machine, computed not stored rig read no hardware at all. The single exception was `uname -m` in runner-install.sh, used to pick a runner tarball and then discarded — so "is this the 32GB one, or the M900?" was a question you answered by logging in and running free -h, nproc, df -h and uname -r by hand, four commands deep, on a machine you were already unsure about. `rig platform` prints hostname, OS, kernel, CPU, memory, disk and virtualization, then a provenance block: which rig, when, and the role marker's traits. It COMPUTES rather than stores, and that is the design rather than an implementation detail. Specs change without rig doing anything — RAM added, root disk resized, the unattended-upgrades bootstrap itself enables patching the kernel — so a stored spec is stale the moment the machine changes, and refreshing one on every run would collide with bootstrap's "safe to re-run; a second run changes nothing" contract. Nothing is written, so nothing can go stale. The corollary is deliberate: reading only /proc, uname, /etc/os-release, df and systemd-detect-virt means no root, no network, and it runs on a pristine Debian box rig has never bootstrapped — useful for deciding what to converge a machine into, not only for auditing it afterwards. That also makes it the rare rig command the harness can RUN for real rather than grep: the tests assert the answer describes the actual test machine (kernel and hostname compared against independently computed values), and assert it writes nothing. Both known traps are handled explicitly. /etc/os-release is sourced in a SUBSHELL — it defines VERSION, NAME and ID and would otherwise clobber same-named script variables, the form every other site in this tree uses and test/cli.sh already greps for. systemd-detect-virt exits non-zero on bare metal while printing 'none', a normal answer that set -e would otherwise turn into a failed run, so it is wrapped in `|| true`. Provenance is read, never written, and degrades per file. /etc/rig/manifest is #61 and does not exist yet, so that line reads 'not bootstrapped' on every machine today; the command ships complete without it and neither blocks the other. Named `platform` and not `status`: `users status` and `runner status` cross-check recorded against live state and print DRIFT, and a command that records nothing cannot drift, so calling it status would borrow a promise it structurally cannot make. It also leaves `rig status` free for the machine-wide roll-up it will eventually want to be. Refs #64
2026-07-19 23:35:58 +00:00
### Added
refactor: one drill record per version, in drills/ Drill records move from sections inside drill/RUNS.md to one file per version at drills/<version>.md. The old guard had to parse headings: an em-dash prefix match, an optional " — DATE" tail, a whole-version comparison so 0.3.0-rc1 could not satisfy 0.3.0, and a separate non-blank-body rule. All of it existed only because records shared one file, and both sibling repos shipped a defect out of that complexity in review — a `sed '/./,$!d'` extractor where `.` matches a space (box#149, cast#138), and heading-grammar drift. One file per version makes nearly all of it unrepresentable: 0.3.0.md and 0.3.0-rc1.md are different files, and the filesystem does the whole-version comparison. The awk drill_section() machinery is gone. What survives is the one rule splitting the files does not make unrepresentable: a file of only whitespace is not a record. Plain drills/, NOT .drills/ — a dot-directory is invisible to globs without dotglob, the cause of #70 here and box#116/box#118. drill/RUNS.md is deleted; it was created in this same unmerged PR, held no real records, and its useful reasoning moves to drills/README.md. (box keeps ITS drill/RUNS.md — that one is a genuine harness log.) Also corrects the ordering framing in CONTRIBUTING and the new README: the three repos' drills are INDEPENDENT, run in any order. What makes that safe is that each pins the same fixed set of candidate refs, so box and rig measure the same pair — that, not sequencing, is what dissolves the mutual recursion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:27:08 +00:00
- 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/manifest` records which rig converged a machine, and when (#61)
feat(bootstrap)!: machine roles carry a -server suffix; staging-server restored rig builds two kinds of thing on opposite sides of a trust boundary -- tailnet machines it converges, and guests a box mints -- and both families lived in one flat namespace with nothing in a role name saying which you meant. `staging` is where that stopped being cosmetic: the word names the metal that hosts guests and the guests on it, only one could have it, and #31 gave it to the guests. The VM-host shape was left nameless, spelled `custom --class server --host yes --join authkey`, which is what every refusal recited at an operator who had confused the two. The suffix now names the family: control-plane-server, workload-server, runner-server, dev-server, plus the restored staging-server (class=server host=yes join=authkey). host=yes already installs the box CLI and runs box's setup-host, so staging-server is a table row, not new machinery. It stays OUT of the tag:server allow-list deliberately -- a host is never managed by the control plane, its guests are -- so its key is minted tag:local. custom and workstation keep bare names as the rule, not an exception to it: custom presets nothing and can be any shape including a guest, so a family claim is one it cannot make; a workstation is somebody's own device, joined by interactive login, user-owned and untagged, never tailnet-managed. Hard cut, no aliases -- old names are refused as unknown. Two consequences this reaches beyond the CLI surface. TS_HOSTNAME defaults to the role name, so a box taking the default now comes up control-plane-server. And the two coolify commands match the ROLE NAME in /etc/rig/role, not the traits, so they now look for role=control-plane-server; a pre-rename control plane takes their warning branch, which is advisory and never a gate, so the run proceeds and the message names the repair. dev-server is class=human, which reads like a contradiction and is not: the suffix names the family, the class names the root-SSH door policy. The two axes share the word "server", which is a real wart -- #77 renames the class trait to what it controls, kept separate because it reaches markers on live machines that guard root SSH. Tests cover both directions of the cut: every new name resolves, every old name is refused as unknown, and the two deliberately-bare roles are proven NOT to have been swept up -- the inverse error, which would otherwise only surface at somebody's laptop. Closes #76 (machine-role half) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:01:16 +00:00
### Changed
- `state:needs-human` is set at handoff, not by the cron (#96)
- PR labels split into two axes: `state:*` (whose ball) and `blocker:*` (what
is in the way) (heavy-duty/box#137)
- BREAKING: `--class human|server` is now `--root-door closed|open`; old
markers still resolve (#77)
- BREAKING: the box tenant roles carry a `-box` suffix (#76)
- BREAKING: machine roles carry a `-server` suffix, and `staging-server` is
back (#76)
- Changelog entries are one line each, and the whole file now follows the rule
(#100)
## 0.2.0 — 2026-07-19
feat: users apply grants the box tier, not just the socket Role `box` resolved to exactly one action, `usermod -aG incus`. That is the socket — step 1 of the five `box grant` performs. Without the other four (the user-<uid> project, its narrowing to boxnet and only boxnet, the snapshot and backup allowances clone and `box export` ride, and the shipped box-net profile installed into that project) the user's first `box new` refuses for want of a box-net profile, so apply's promise — the users file is the fleet's source of truth — was not kept for this role. Worse, until an admin arrived by hand the user held an `incus` membership with no converged project, and incus-user would lazily hand them a stock unhardened NAT bridge: a state box's own contract forbids. On host=yes apply now calls `box grant <user>` per box-role user. rig calls box's grant rather than reimplementing four fifths of it — the "rig never installs Incus" boundary is about installation, not invocation, and grant is already script-callable: idempotent, root-or-sudo, stdin-pinned, with its own run-as-the-user touch. Three decisions the code carries in comment form: - Ordering. The call sits after `useradd` (grant opens with a getent passwd and refuses an unknown account) and after the other groups, so a user whose grant fails still lands with everything rig owns outright. - Failure granularity, split the way the host= guard beside it already splits. A missing box CLI on host=yes dies, like the missing incus group: a broken VM host, not a per-user accident. A per-user grant failure warns and continues — one box-role user somewhere in the fleet must not stop apply everywhere VMs don't live. host=no and marker-less boxes keep their existing skip-with-warning untouched. - The group ADD is deferred to grant, while `incus` stays in the wanted set so the exact-convergence loop never strips a box-role user's socket. Grant's rollback only reaches a membership that run added, so rig opening the socket first would leave a failed grant unable to close it. And grant is the authority on whether the group belongs at all: for an incus-admin member it deliberately does not add `incus`. An incus-admin member is warned, never fatal: box grant refuses them today, which heavy-duty/box#99 fixes box-side with no rig change needed. Closes #49 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:15:55 +00:00
### Added
- `users apply` grants the box *tier*, not just its socket (#49)
feat: users apply grants the box tier, not just the socket Role `box` resolved to exactly one action, `usermod -aG incus`. That is the socket — step 1 of the five `box grant` performs. Without the other four (the user-<uid> project, its narrowing to boxnet and only boxnet, the snapshot and backup allowances clone and `box export` ride, and the shipped box-net profile installed into that project) the user's first `box new` refuses for want of a box-net profile, so apply's promise — the users file is the fleet's source of truth — was not kept for this role. Worse, until an admin arrived by hand the user held an `incus` membership with no converged project, and incus-user would lazily hand them a stock unhardened NAT bridge: a state box's own contract forbids. On host=yes apply now calls `box grant <user>` per box-role user. rig calls box's grant rather than reimplementing four fifths of it — the "rig never installs Incus" boundary is about installation, not invocation, and grant is already script-callable: idempotent, root-or-sudo, stdin-pinned, with its own run-as-the-user touch. Three decisions the code carries in comment form: - Ordering. The call sits after `useradd` (grant opens with a getent passwd and refuses an unknown account) and after the other groups, so a user whose grant fails still lands with everything rig owns outright. - Failure granularity, split the way the host= guard beside it already splits. A missing box CLI on host=yes dies, like the missing incus group: a broken VM host, not a per-user accident. A per-user grant failure warns and continues — one box-role user somewhere in the fleet must not stop apply everywhere VMs don't live. host=no and marker-less boxes keep their existing skip-with-warning untouched. - The group ADD is deferred to grant, while `incus` stays in the wanted set so the exact-convergence loop never strips a box-role user's socket. Grant's rollback only reaches a membership that run added, so rig opening the socket first would leave a failed grant unable to close it. And grant is the authority on whether the group belongs at all: for an incus-admin member it deliberately does not add `incus`. An incus-admin member is warned, never fatal: box grant refuses them today, which heavy-duty/box#99 fixes box-side with no rig change needed. Closes #49 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:15:55 +00:00
feat!: bootstrap takes the users file `rig bootstrap` already knew everything else about what a box is — class, host, join, hostname — and wrote /etc/rig/role to 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 one was the forgettable one. --users <path> now runs the `users apply` convergence as bootstrap's final phase: after the traits, after the verified tailnet join, after the role marker (apply reads that marker), and after the host=yes box install (so box-role users find the incus group box's own setup-host built). One command, and the box has its people on it. BREAKING: --users is required on every machine role, with --no-users as the explicit opt-out. Omitting both is a usage error naming both flags; passing both is a usage error too. class=server is required as well: a machine 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. The file is never persisted — passed per invocation, read once through apply, copied nowhere. `--users -` is refused: bootstrap's stdin belongs to the pre-auth key prompt. The box TENANT roles take neither flag; a guest is minted non-interactively, never joins the tailnet, and has no SSH door of its own. rig still never installs Incus and never calls `box setup-host` itself. The host=yes box-role precondition refuses early only where the outcome is already proven (RIG_SKIP_BOX_INSTALL=1); every other way that step can fail lands in `users apply`'s existing refusal, unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:17:49 +00:00
### Changed
- BREAKING: `rig bootstrap` takes the users file, and requires it (#51)
feat!: bootstrap takes the users file `rig bootstrap` already knew everything else about what a box is — class, host, join, hostname — and wrote /etc/rig/role to 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 one was the forgettable one. --users <path> now runs the `users apply` convergence as bootstrap's final phase: after the traits, after the verified tailnet join, after the role marker (apply reads that marker), and after the host=yes box install (so box-role users find the incus group box's own setup-host built). One command, and the box has its people on it. BREAKING: --users is required on every machine role, with --no-users as the explicit opt-out. Omitting both is a usage error naming both flags; passing both is a usage error too. class=server is required as well: a machine 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. The file is never persisted — passed per invocation, read once through apply, copied nowhere. `--users -` is refused: bootstrap's stdin belongs to the pre-auth key prompt. The box TENANT roles take neither flag; a guest is minted non-interactively, never joins the tailnet, and has no SSH door of its own. rig still never installs Incus and never calls `box setup-host` itself. The host=yes box-role precondition refuses early only where the outcome is already proven (RIG_SKIP_BOX_INSTALL=1); every other way that step can fail lands in `users apply`'s existing refusal, unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:17:49 +00:00
### Fixed
- A release no longer disarms the changelog under the PRs still in flight (#67)
- A `host=no` box with an `incus` group no longer hands out the bare socket
(#58)
- Dropping the box role revokes through `box`, not behind its back (#50)
- `rig bootstrap` refuses a users file that names no users (#57)
fix: dropping the box role revokes through box, not behind its back `users apply` converged group `incus` with a bare `gpasswd -d`, the same move it makes for `rig-admin` and `rig`. Those two are rig's. `incus` is box's, and `box revoke` does 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 that session dies, and it hands over `loginctl terminate-user <user>` as the remedy. rig logged "removed <user> from incus" and moved on. 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 loop and the dropped-user sweep — now route the incus group through one `drop_incus` helper that calls `box revoke`, keeping a single 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 (the #12 lesson bootstrap already applies to box's installer): a revoke that returns 0 with the membership still standing has not closed the socket, so the effective state is checked and rig falls back to removing the group itself — as it also does on a host where box is not installed. Every fallback path carries the session warning in rig's own voice, because the silence was the bug. The absent-group case needs no new guard: `id -nG` cannot report a group that does not exist, so the existing `in_group` test at both call sites is already false on a host=no box or one where `box setup-host` never ran. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:18:21 +00:00
## 0.1.0 — 2026-07-19
### Fixed
- The release suite accepts the ceremony's own tree (#44)
- The installer survives an environment with no `$HOME` (#39, #41)
- Headless credential prompts refuse loudly instead of dying silently (#42)
### Added
- Merging a release-labeled PR IS the release, and the release re-arms main
(#47)
- Tagged releases, and an installer that installs them (#32)