2026-07-18 20:57:13 +00:00
|
|
|
# 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.
|
|
|
|
|
|
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
|
|
|
## Unreleased
|
2026-07-18 20:57:13 +00:00
|
|
|
|
2026-07-19 21:31:55 +00:00
|
|
|
## 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) — role
|
|
|
|
|
`box` resolved to exactly one action, `usermod -aG incus`. That is the
|
|
|
|
|
socket; it is step 1 of the five `box grant` performs, so every box-role
|
|
|
|
|
user still needed an admin to run `box grant <user>` by hand before their
|
|
|
|
|
first `box new` would do anything but refuse ("your project has no box-net
|
|
|
|
|
profile"), and until that admin arrived they held an `incus` membership
|
|
|
|
|
with no converged project — incus-user would lazily hand them a stock
|
|
|
|
|
unhardened NAT bridge, which is worse than no grant at all. On `host=yes`
|
|
|
|
|
apply now calls `box grant` per box-role user, after `useradd` (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 the `host=` guard beside them already splits: a missing `box` CLI
|
|
|
|
|
on `host=yes` dies (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-admin` member is warned, not fatal — `box grant` refuses them today,
|
|
|
|
|
which heavy-duty/box#99 fixes box-side with no rig change needed.
|
|
|
|
|
|
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) —
|
|
|
|
|
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 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. 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 bootstrap` invocation 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 ran `rig users apply` as 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 on `class=server` as well
|
|
|
|
|
as `class=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 is `box
|
|
|
|
|
shell`, gated by the host's `incus` grants.
|
|
|
|
|
|
|
|
|
|
A bad users file is caught **up front** now (the same parser apply uses,
|
|
|
|
|
before `apt`, the hostname change, and any spent pre-auth key), and on
|
|
|
|
|
`host=yes` with `RIG_SKIP_BOX_INSTALL=1` a box-role user with no `incus`
|
|
|
|
|
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 calls `box setup-host` on its own account; every other way that step
|
|
|
|
|
can fail lands in `users apply`'s existing refusal, unchanged.
|
|
|
|
|
|
2026-07-19 12:15:08 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-19 19:41:49 +00:00
|
|
|
- **A release no longer disarms the changelog under the PRs still in
|
|
|
|
|
flight** (#67) — the ceremony stamps `## Unreleased` to
|
|
|
|
|
`## X.Y.Z — YYYY-MM-DD` and 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.0` at
|
|
|
|
|
`67386b4` and was repaired two minutes later by `0ff520c`; #54 would
|
|
|
|
|
have filed a **BREAKING** entry the same way. The published release body
|
|
|
|
|
is never affected — `release.yml` extracts 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 `## Unreleased` above 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](CONTRIBUTING.md), not to `release.yml` — no workflow
|
|
|
|
|
has ever touched the heading; the stamping was always by hand, and the
|
|
|
|
|
`-dev` re-arm the workflow does perform was only ever about `VERSION`.
|
|
|
|
|
And `test/release.sh` now keys its guard to `VERSION` rather than
|
|
|
|
|
demanding a literal heading: a stamped top section is legal exactly when
|
|
|
|
|
`VERSION` is 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
|
|
|
|
|
constructed `VERSION` + `CHANGELOG.md` pairs, 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 on `main`
|
|
|
|
|
as of this writing and is getting the sibling fix.
|
|
|
|
|
|
2026-07-19 17:29:20 +00:00
|
|
|
- **A `host=no` box with an `incus` group no longer hands out the bare
|
|
|
|
|
socket** (#58) — `users apply` consulted the `host=` trait only when group
|
|
|
|
|
`incus` was ABSENT (die on `host=yes`, skip on `host=no`). When the group
|
|
|
|
|
was PRESENT the trait was never asked, so a `host=no` or marker-less box
|
|
|
|
|
that nonetheless carried the group — `box setup-host` ran, 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, which `incus-user`
|
|
|
|
|
answers by lazily building an UNHARDENED project under whoever opens it
|
|
|
|
|
(`incusbr-<uid>`, NAT on v4 and v6, no ACL, no `dns.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
|
|
|
|
|
like `assert_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 and `rig bootstrap` as the repair. On such a box
|
|
|
|
|
exact-membership convergence now strips box-role users out of `incus`, on
|
|
|
|
|
the same reasoning: a membership inherited from a previous life is the same
|
|
|
|
|
half-grant as a freshly added one.
|
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
|
|
|
- **Dropping the box role revokes through `box`, not behind its back**
|
|
|
|
|
(#50) — `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 it dies, and
|
|
|
|
|
hands over `loginctl terminate-user <user>` as the remedy. rig logged
|
|
|
|
|
`removed <user> from incus` and 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 call `box 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.
|
fix(bootstrap): refuse a users file that names no users
An empty, comments-only or whitespace-only users file is not a parse error,
so it walked straight through the requirement #51 built: pre-flight passed,
apply converged nothing, and the box came up root-only — the exact outcome
--no-users exists to make explicit, reached by the flag added to guarantee
the opposite. `--users ./empty` and `--no-users` produced the identical box
and only one of them said so.
Catch the zero-user parse in bootstrap's pre-flight, where the file is
already parsed for validation and before apt, the hostname change, or a
spent pre-auth key. The refusal names --no-users: the root-only box is
reachable, it just has to be asked for out loud.
Deliberately narrow. This is bootstrap's contract, not the parser's and not
apply's: zero users is a legal file, and a standalone `rig users apply`
against an emptied file is a real de-provisioning operation that must stay
possible. Negative-grep tests pin both.
Closes #57
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:28:39 +00:00
|
|
|
- **`rig bootstrap` refuses 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-users` exists to make explicit, reached by the flag added to
|
|
|
|
|
guarantee the opposite. Bootstrap's pre-flight now catches the zero-user
|
|
|
|
|
parse — before `apt`, the hostname change, or a spent pre-auth key — and
|
|
|
|
|
refuses, naming `--no-users` as the way to ask for a root-only box out loud.
|
|
|
|
|
Scoped to `rig bootstrap`'s contract only: a standalone `rig users apply`
|
|
|
|
|
against an emptied file is a real de-provisioning operation and is
|
|
|
|
|
unchanged.
|
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
|
2026-07-19 17:29:20 +00:00
|
|
|
|
2026-07-19 13:48:41 +00:00
|
|
|
- **The release suite accepts the ceremony's own tree** (#44) —
|
|
|
|
|
`test/release.sh` demanded a literal `## Unreleased` heading in the real
|
|
|
|
|
`CHANGELOG.md`, extracting non-empty and containing `#32`. All three are
|
|
|
|
|
false by construction on the `release: X.Y.Z` tree 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.yml` runs; `ci.yml`
|
|
|
|
|
never does). The guard now asserts what it was for: whatever the TOP
|
|
|
|
|
`## ` section is — `Unreleased` between releases, the stamped version on
|
|
|
|
|
and right after one — the exact `changelog_section` the workflow runs
|
|
|
|
|
extracts it non-empty. The rotting issue-number grep is gone.
|
|
|
|
|
|
2026-07-19 14:41:43 +00:00
|
|
|
- **The installer survives an environment with no `$HOME`** (#39) —
|
|
|
|
|
cloud-init's `runcmd` runs `install.sh` with no `$HOME` set, and under
|
|
|
|
|
`set -u` the first expansion died with a bash unbound-variable stack
|
|
|
|
|
instead of an install — found live by box#88's template seed, which
|
|
|
|
|
pins `HOME=/root` as its own scar. The installer now derives the home
|
|
|
|
|
from `getent` for 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.)
|
2026-07-19 12:15:08 +00:00
|
|
|
- **Headless credential prompts refuse loudly instead of dying silently**
|
|
|
|
|
(#42) — the interactive credential prompts (`TS_AUTHKEY` in `bootstrap`,
|
|
|
|
|
`RUNNER_TOKEN` in `runner install`, `RUNNER_REMOVE_TOKEN` in
|
|
|
|
|
`runner remove`, and both tokens in `runner 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), `read` fails, `set -e` ends 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
|
|
|
|
|
remove` also names `--local`), and every `read` is `|| die`-guarded so
|
|
|
|
|
EOF at a real prompt gets the same courtesy. `db.sh` already held the
|
|
|
|
|
line here; now all of rig does.
|
|
|
|
|
|
2026-07-18 20:57:13 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-07-19 16:34:29 +00:00
|
|
|
- **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.yml` now fires on
|
|
|
|
|
pushes to main (fork-sourced ceremony PRs get a read-only token on
|
|
|
|
|
`pull_request` events), reading the transition from the push itself:
|
|
|
|
|
`event.before` to the pushed head. A decide step answers four states —
|
|
|
|
|
release-flow *work* merged under the `release` label (`-dev` endstates,
|
|
|
|
|
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 to `X.Y.(Z+1)-dev` itself, direct push with a
|
|
|
|
|
loud open-a-PR fallback, so no follow-up bump PR exists on the paved
|
|
|
|
|
road. A `GITHUB_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.
|
feat: merging a release-labeled PR is the release (#47)
The rig twin of heavy-duty/box#96, from the release-ceremony retro: the
tag was a separate, manual, silent-when-forgotten step, and a forgotten
tag produces no red X — the worst failure shape. The ship decision
already lives in the release PR; merging it is "ship". After that,
tagging is transcription, and transcription belongs to machines.
release.yml now also fires on pull_request closed into main, gated on
merged AND the `release` label. The job asserts in order, each fail-loud
and creating nothing: VERSION at the merge commit is non--dev; VERSION
changed in THIS PR (base vs merge — the interlock that fails a
mislabeled ordinary PR); the changelog section for that version extracts
non-empty via the existing changelog_section from release-lib.sh; and no
tag or release exists yet. Then, in the same job, it API-creates the tag
at the merge commit and publishes the release with the extracted notes.
Same-job is load-bearing: a GITHUB_TOKEN-created tag does not fire the
tag-push trigger, so the publish must live next to the tag and the
fallback job cannot double-publish; the nothing-exists assert covers a
manual race. The tag-push path survives verbatim as the documented
manual fallback and backfill, and CONTRIBUTING's Releasing section now
reads merge-is-ship with the manual tag as fallback.
test/release.sh pins the merge path in the house grep-pin style: the
merged+labeled gate, the four asserts, the same-job tag+publish (awk
from release-on-merge: to EOF), the asserts-precede-the-tag ordering,
and the surviving tag-push trigger.
Fixes #47
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:19:47 +00:00
|
|
|
|
2026-07-18 20:57:13 +00:00
|
|
|
- **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: the `release: X.Y.Z` PR bumps `VERSION` and stamps
|
|
|
|
|
this file's Unreleased section with version + date; the merge commit is
|
|
|
|
|
tagged bare `X.Y.Z` (box's tag scheme — no `v` prefix). `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 same `changelog_section` the
|
|
|
|
|
test harness drives. No assets: for a pure-bash tree, GitHub's source
|
|
|
|
|
tarball for the tag IS the package. `install.sh` now defaults to the
|
|
|
|
|
**latest release**: the tag is resolved by following the
|
|
|
|
|
`releases/latest` redirect and reading the `Location` header — no API, no
|
|
|
|
|
token — and the download is `archive/refs/tags/<tag>.tar.gz`. `RIG_REF`
|
|
|
|
|
picks the other two channels: a tag pins (`refs/tags` outranks 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, naming `RIG_REF=main` as 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 — pinning `BOX_REF` in the host-installs-box path
|
|
|
|
|
— stays open until box cuts its next tagged release.
|