fix(bootstrap): stop telling operators to run bare roles; pin the migration

Two review findings from the bot round on this stack.

BLOCKING (codex-bot, claude-bot -- both, independently). bootstrap-tenant.sh
emits the staging guest's tailnet-join next step at the end of a converge
("box shell -> sudo rig bootstrap workload"), repeats it in usage, and two of
its refusals recite the old machine-role list. Fixed here rather than on the
stacked tenant PR because THIS is the branch that removes the `workload` role
-- shipping it alone would print a next step naming a role that no longer
exists.

None of those four sites is code that ACCEPTS a role, which is why the rename
missed them, and is also what makes them the worse failure. A stale flag dies
immediately with a usage error. A stale next-step is copy-pasted by a human
onto a DIFFERENT box, minutes after the run that printed it reported success,
and dies there with no thread back to the cause.

So test/cli.sh sweeps every shipped script under bin/ and commands/ for
`rig bootstrap <pre-#76 name>` rather than pinning the four known sites: the
next instance of this class will be somewhere else. Proven non-vacuous --
reintroducing the bare `workload` next-step turns the suite red (412/1),
restoring it turns it green (413/0).

NON-BLOCKING (claude-bot). The migration story was documented and untested:
every marker fixture was renamed alongside the code, so nothing asserted what
a real pre-rename box does. A `role=control-plane` fixture now pins both
halves of the promise -- such a box WARNS on the coolify verbs (its marker no
longer names a role that exists) and is never REFUSED. Both halves matter: a
rename that turned this into a refusal would break the exact boxes the
CHANGELOG promises keep working, on the command that installs the control
plane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-20 00:21:35 +00:00
parent 1845468765
commit 00f6351e28
3 changed files with 44 additions and 5 deletions

View file

@ -49,6 +49,15 @@ on the way to cutting its first release, and this file starts there.
branch until it is re-bootstrapped. That check has always been advisory and
never a gate, so the run still proceeds and the warning names the repair.
The rename also reaches every string that *tells an operator to run a role*,
not just the code that accepts one — `bootstrap-tenant.sh` emits the staging
guest's tailnet-join next step (`sudo rig bootstrap workload-server`), and
two of its refusals recite the machine-role list. A stale next-step is worse
than a stale flag: it fails when someone copy-pastes it, on a different box,
minutes after the run that printed it reported success. `test/cli.sh` sweeps
every shipped script for pre-rename role names rather than pinning the known
sites, because the next instance of this will be somewhere else.
`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, and
operators enter a dev box as themselves so `close-root` shuts its door. The

View file

@ -10,7 +10,7 @@
# Creds-free BY CONTRACT: box auto-runs these at mint ('box exec … rig
# bootstrap claude'), so every path here is non-interactive and nothing joins
# or admits — no tailnet, no keys, no prompts. staging's tailnet join stays
# operator-run ('rig bootstrap workload' through 'box shell'), exactly the
# operator-run ('rig bootstrap workload-server' through 'box shell'), exactly the
# creds split box#69 designed.
# Convergent: safe to re-run; a second run changes nothing.
set -euo pipefail
@ -43,7 +43,7 @@ tenant on top, and re-runs converge an existing box to a new spec.
hardening. The tailnet workload join is deliberately
NOT here — it holds a credential, so it stays
operator-run: `box shell``sudo rig bootstrap
workload` with a tagged pre-auth key.
workload-server` with a tagged pre-auth key.
--user <name> the tenant user the box seed created (default: the
role's name; staging defaults to `ops`)
@ -77,10 +77,10 @@ while [ $# -gt 0 ]; do
# from the box seed, and the one trait-shaped thing a staging guest
# eventually does (join the tailnet as a workload) is deliberately not
# here: it holds a credential, so it stays operator-run.
die "tenant roles have no traits: $1 belongs to the machine roles (control-plane|workload|runner|dev|workstation|custom). A tenant box's shape comes from its seed; staging's tailnet join is operator-run via 'rig bootstrap workload'." 2 ;;
die "tenant roles have no traits: $1 belongs to the machine roles (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom). A tenant box's shape comes from its seed; staging's tailnet join is operator-run via 'rig bootstrap workload-server'. The METAL that hosts these guests is 'rig bootstrap staging-server'." 2 ;;
--ts-tag)
[ $# -ge 2 ] && shift
die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging's join is operator-run via 'rig bootstrap workload', where the tag comes from the pre-auth key." 2 ;;
die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging's join is operator-run via 'rig bootstrap workload-server', where the tag comes from the pre-auth key." 2 ;;
*) die "unknown flag: $1" 2 ;;
esac
done
@ -384,7 +384,7 @@ fi
log "done — tenant ${ROLE}, user ${TENANT_USER}"
if [ "$ROLE" = "staging" ]; then
log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload --hostname <name> with a tagged pre-auth key"
log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload-server --hostname <name> with a tagged pre-auth key"
else
log "next: creds stay with the operator — ${CLI} authenticates through its own interactive login when a human decides"
fi

View file

@ -93,6 +93,19 @@ check "bootstrap: workstation keeps its bare name" 2 "unset TS_AUTHKEY" \
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workstation
check "bootstrap: custom keeps its bare name" 2 "--hostname" \
"$ROOT/commands/bootstrap.sh" custom --class server --host no --join authkey
# NOTHING may still TELL an operator to run a pre-#76 role. The rename is a
# hard cut, so a next-step string, a usage line or a refusal that still recites
# a bare role name is a command that fails when someone copy-pastes it — and it
# fails later and further from the cause than a broken flag would, because it
# fails on a different box, minutes after this run reported success. The tenant
# script is the one that emits the staging guest's workload-join next step, so
# it is where this bites first (caught in review on #80, fixed here where the
# rename actually happens). Swept across every shipped script rather than
# asserted at the one known site: the next instance of this will be somewhere
# else, and a site-specific check would not see it.
check "roles: no shipped script tells an operator to run a pre-#76 role name" 1 "" \
grep -rnE "rig bootstrap (control-plane|workload|runner|dev)( |'|\"|$)" \
"$ROOT/bin/rig" "$ROOT/commands/"
# --- traits: roles are presets, every trait individually settable (#26) -----
check "bootstrap: unknown role still exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
check "bootstrap: bad --class value exits 2" 2 "human|server" "$ROOT/commands/bootstrap.sh" workload-server --class potato
@ -624,6 +637,12 @@ MARKER_FIX="$(mktemp -d)"
printf 'role=workload-server class=server host=no join=authkey\n' > "$MARKER_FIX/workload"
printf 'role=control-plane-server class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane"
printf 'role=control-plane-server\n' > "$MARKER_FIX/bare-control-plane"
# A PRE-#76 marker, verbatim as a real box bootstrapped before the rename
# carries it. This is the one fixture that must keep its old spelling: the
# CHANGELOG promises such a box takes the warning branch and keeps working,
# and until this existed nothing asserted it — every other fixture here was
# renamed with the code, so the migration story was documented and untested.
printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/pre-rename-cp"
if [ "$(id -u)" -ne 0 ]; then
check "coolify: warns on a non-control-plane marker" 0 "1" \
marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2
@ -635,6 +654,17 @@ if [ "$(id -u)" -ne 0 ]; then
marker_warns "$MARKER_FIX/bare-control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2
check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \
marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2
# The migration story, pinned in both halves: a pre-#76 control plane WARNS
# (its marker no longer names a role that exists) but is never refused. Both
# halves matter — a rename that turned this into a refusal would break the
# exact boxes the CHANGELOG promises keep working, and it would do it on the
# command that installs the control plane.
check "coolify: a PRE-#76 'role=control-plane' marker warns (migration)" 0 "1" \
marker_warns "$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-install.sh" --version 4.1.2
check "coolify: ...and is still never refused" 1 "must run as root" \
env RIG_ROLE_MARKER="$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-install.sh" --version 4.1.2
check "coolify backup: a PRE-#76 'role=control-plane' marker warns (migration)" 0 "1" \
marker_warns "$MARKER_FIX/pre-rename-cp" "$ROOT/commands/coolify-backup-install.sh"
# The warning must stay a warning: the run proceeds past it and stops at the
# root check (exit 1), never turned into a marker refusal.
check "coolify: the marker warns but never refuses" 1 "must run as root" \