fix: dropping the box role revokes through box, not behind its back #55

Merged
dan-claude-bot merged 1 commit from fix/incus-revoke-via-box into main 2026-07-19 19:08:00 +00:00
dan-claude-bot commented 2026-07-19 16:18:53 +00:00 (Migrated from github.com)

The bug

rig users apply converges membership in rig-admin rig incus exactly.
For rig-admin and rig a bare gpasswd -d is the whole story — they are
rig's groups. For incus it is not: box's setup-host creates that group,
box grant hands it out, and box revoke takes it back doing strictly more
than removing a membership.

box revoke says out loud what removing the membership does not do:

WARNING: <user> has live sessions, and group membership is read at login —
         those sessions keep the socket until they end. To end them now:
         sudo loginctl terminate-user <user>

Supplementary groups are fixed at login. rig logged removed <user> from incus and moved on — so an operator who dropped someone from the users
file and watched apply report success believed their VM access was gone, and
was wrong for as long as that user held a session.
A leftover tmux keeps
the Incus socket. Nothing in the output suggested otherwise.

Two sites had it: commands/users-apply.sh:223 (per-user convergence) and
:285 (the dropped/renamed-user sweep).

The fix

Issue option 1 — box takes its own group back, so the group keeps one owner.
Both removal paths route incus through a single drop_incus helper.

  • Never --purge. It deletes the user's boxes, images and project.
    Destroying someone's running machines is not a convergence step; an edit to
    a users file must not do it. box revoke <user> --purge stays a deliberate
    admin act. Proven at runtime on every path box is reached on, including the
    retry-ish ones — a fallback must never escalate to the destructive verb.
  • Exit codes are 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. The effective state is checked, and rig
    falls back to removing the group itself.
  • Box not installed (host built by hand, box removed from under it): same
    fallback.
  • Every fallback carries the session warning itself. The silence is the
    bug being fixed, not the gpasswd call. Where pgrep is absent rig cannot
    tell whether the user holds sessions, so it warns anyway: an unnecessary
    warning costs one command, a missing one costs a wrong belief about who can
    reach the daemon.
  • Absent incus group 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 —
    nothing to revoke, nothing said, apply moves on. This is stated in the code
    rather than left to be rediscovered.

README.md and --help updated to match.

Verification

  • shellcheck -x over the full CI sweep (bin/* **/*.sh): 22 files,
    clean
    .
  • bash test/cli.sh: 344 passed, 0 failed (325 on main — 19 new).
  • bash test/release.sh: 41 passed, 0 failed.

The new coverage exercises the real function, it does not argue about it.
users-apply.sh executes when sourced (and dies at the root check), so
drop_incus is lifted out of the real file verbatim — column-0
drop_incus() { through column-0 } — and driven against stub
log/warn/in_group and a PATH replaced by a stub directory holding only
box, gpasswd and pgrep (replaced, not prefixed, so "box absent" means
absent even on a host that really has box). Four scenarios: box revokes
successfully; box exits 0 leaving the membership standing; box exits non-zero;
box not on the host. Plus a fifth run with no pgrep at all.

Two guards keep it from passing vacuously: the extraction is asserted
non-empty and whole first, and the gpasswd stub reports on stderr
because the real call is gpasswd -d … >/dev/null — a stub speaking on stdout
would be silenced by the code under test.

Sensitivity checked by mutation: reverting drop_incus to the old bare
gpasswd -d + log turns 10 of the new assertions red.

Decisions the issue did not settle

  1. What a failing box revoke should do. Letting it propagate would abort
    apply mid-convergence under set -e, leaving users half-converged over a
    host-extra tool. Ignoring it would leave the group in place. It falls back
    to gpasswd -d + the warning: access ends either way, and the operator is
    told box did not do its job.
  2. Verifying the revoke rather than trusting exit 0. Not asked for; it is
    the repo's own "don't trust exit codes" discipline, and the membership is
    the thing that closes the socket.
  3. Warning when pgrep is absent. box only warns when it finds live
    processes. rig mirrors that, but fails toward saying it when it cannot
    tell.
  4. drop_incus stayed in users-apply.sh rather than moving to
    commands/lib/, which is parsing-only and would have to gain log, warn
    and in_group to host it. The test lifts the function instead.
  5. box's output is passed through unshaped, matching how bootstrap.sh
    already runs box's installer. Its lines self-identify (group:,
    sessions:, WARNING:, revoked:), and reshaping another tool's warning
    is how the wording drifts.

Closes #50

🤖 Generated with Claude Code

## The bug `rig users apply` converges membership in `rig-admin rig incus` exactly. For `rig-admin` and `rig` a bare `gpasswd -d` is the whole story — they are rig's groups. For `incus` it is not: box's `setup-host` creates that group, `box grant` hands it out, and `box revoke` takes it back doing strictly more than removing a membership. `box revoke` says out loud what removing the membership does **not** do: ``` WARNING: <user> has live sessions, and group membership is read at login — those sessions keep the socket until they end. To end them now: sudo loginctl terminate-user <user> ``` Supplementary groups are fixed at login. rig logged `removed <user> from incus` and moved on — so **an operator who dropped someone from the users file and watched apply report success believed their VM access was gone, and was wrong for as long as that user held a session.** A leftover tmux keeps the Incus socket. Nothing in the output suggested otherwise. Two sites had it: `commands/users-apply.sh:223` (per-user convergence) and `:285` (the dropped/renamed-user sweep). ## The fix Issue option 1 — box takes its own group back, so the group keeps one owner. Both removal paths route `incus` through a single `drop_incus` helper. - **Never `--purge`.** It deletes the user's boxes, images and project. Destroying someone's running machines is not a convergence step; an edit to a users file must not do it. `box revoke <user> --purge` stays a deliberate admin act. Proven at runtime on every path box is reached on, including the retry-ish ones — a fallback must never escalate to the destructive verb. - **Exit codes are 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. The effective state is checked, and rig falls back to removing the group itself. - **Box not installed** (host built by hand, box removed from under it): same fallback. - **Every fallback carries the session warning itself.** The silence is the bug being fixed, not the `gpasswd` call. Where `pgrep` is absent rig cannot tell whether the user holds sessions, so it warns anyway: an unnecessary warning costs one command, a missing one costs a wrong belief about who can reach the daemon. - **Absent `incus` group 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 — nothing to revoke, nothing said, apply moves on. This is stated in the code rather than left to be rediscovered. `README.md` and `--help` updated to match. ## Verification - `shellcheck -x` over the full CI sweep (`bin/* **/*.sh`): **22 files, clean**. - `bash test/cli.sh`: **344 passed, 0 failed** (325 on `main` — 19 new). - `bash test/release.sh`: **41 passed, 0 failed**. The new coverage **exercises the real function**, it does not argue about it. `users-apply.sh` executes when sourced (and dies at the root check), so `drop_incus` is lifted out of the real file verbatim — column-0 `drop_incus() {` through column-0 `}` — and driven against stub `log`/`warn`/`in_group` and a PATH *replaced* by a stub directory holding only `box`, `gpasswd` and `pgrep` (replaced, not prefixed, so "box absent" means absent even on a host that really has box). Four scenarios: box revokes successfully; box exits 0 leaving the membership standing; box exits non-zero; box not on the host. Plus a fifth run with no `pgrep` at all. Two guards keep it from passing vacuously: the extraction is asserted non-empty and whole first, and the `gpasswd` stub reports on **stderr** because the real call is `gpasswd -d … >/dev/null` — a stub speaking on stdout would be silenced by the code under test. Sensitivity checked by mutation: reverting `drop_incus` to the old bare `gpasswd -d` + `log` turns **10** of the new assertions red. ## Decisions the issue did not settle 1. **What a failing `box revoke` should do.** Letting it propagate would abort apply mid-convergence under `set -e`, leaving users half-converged over a host-extra tool. Ignoring it would leave the group in place. It falls back to `gpasswd -d` + the warning: access ends either way, and the operator is told box did not do its job. 2. **Verifying the revoke rather than trusting exit 0.** Not asked for; it is the repo's own "don't trust exit codes" discipline, and the membership is the thing that closes the socket. 3. **Warning when `pgrep` is absent.** box only warns when it finds live processes. rig mirrors that, but fails toward saying it when it cannot tell. 4. **`drop_incus` stayed in `users-apply.sh`** rather than moving to `commands/lib/`, which is parsing-only and would have to gain `log`, `warn` and `in_group` to host it. The test lifts the function instead. 5. **box's output is passed through unshaped**, matching how `bootstrap.sh` already runs box's installer. Its lines self-identify (`group:`, `sessions:`, `WARNING:`, `revoked:`), and reshaping another tool's warning is how the wording drifts. Closes #50 🤖 Generated with [Claude Code](https://claude.com/claude-code)
danmt (Migrated from github.com) reviewed 2026-07-19 16:18:53 +00:00
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:23:39 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: approve.

Re-verified from scratch at head 65964a6. This closes #50 the way the issue asked (option 1), and the failure-mode coverage is the strongest part of the PR.

What I checked

Both direct paths are gone. The per-user convergence (commands/users-apply.sh:280) and the dropped-user sweep (commands/users-apply.sh:352) now route incus through the single drop_incus helper; the bare gpasswd -d remains only for rig-admin/rig, which are rig's own groups. Grepped the whole tree: no other site touches the incus group directly, and nothing anywhere touches incus-admin (it stays a non-role, per the README). The test at test/cli.sh:761 pins the count at exactly 2 call sites.

Box absent or failing is loud, and the state is reconciled, not lost. drop_incus (commands/users-apply.sh:220-244) covers the full matrix: box succeeds (bare revoke, effective-state check, box's own warning passes through); box exits 0 with the membership standing (caught by the ! in_group re-check — the #12 "don't trust exit codes" discipline, correctly applied since the membership is what closes the socket); box exits non-zero (warn + gpasswd -d fallback); box not installed (same fallback). Every fallback path carries the session warning itself, which is the actual bug being fixed — the old code's silence, not its gpasswd. There is no path where the role is dropped in rig's model while the group quietly survives.

--purge is provably unreachable. Asserted both by shape (test/cli.sh:770) and at runtime on all three box-reached paths (test/cli.sh:846-848) — a fallback never escalates to the destructive verb. Right call: convergence removes access, never running machines.

Idempotence. Both call sites gate on in_group, so a never-granted or already-revoked user is a clean no-op, and the absent-group case (host=no, or box setup-host never ran) needs no new guard because id -nG cannot report a nonexistent group — the code comment at commands/users-apply.sh:216-219 states this rather than leaving it to be rediscovered. The sweep's transition-only warning semantics are preserved.

Tests are real, not vacuous. The suite lifts drop_incus verbatim from the live file (guarded against an empty lift at test/cli.sh:782), replaces PATH entirely so "absent" means absent even on a box-bearing host, and the stub gpasswd speaks on stderr because the real call is >/dev/null-silenced — both anti-vacuity guards hold up under inspection. Ran locally: test/cli.sh 344 passed, 0 failed (19 new), test/release.sh 41 passed, 0 failed, shellcheck -x clean across the sweep. All match the PR's claims.

Non-blocking observations

  1. Wording in one corner (commands/users-apply.sh:231): if box revoke exits non-zero but did remove the membership (e.g., dies after the group edit), rig warns "'box revoke' did not remove the incus group — removing it directly", then removes nothing (the in_group guard at :233 is false) and logs no removal at all. State and session warning are both correct; only the warn's claim is wrong in that corner. Something like "failed or left the membership standing — reconciling directly" would be accurate on both sub-paths. Fine to leave.

  2. Series note: the grant side (usermod -aG at commands/users-apply.sh:274) still bypasses box grant — that is #53's scope, and #53 is open against main touching this same file. Whichever of #53/#55 merges second will need a rebase; when reconciling, drop_incus's pattern (effective-state check, no destructive escalation, fallback that speaks) is the one the grant path should mirror.

  3. The lift-by-sed extraction couples the test to drop_incus staying at column 0 in users-apply.sh. The non-empty/whole guard means a future reshaping fails loudly rather than vacuously, which is the right failure direction — just be aware the coupling exists.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

**Verdict: approve.** Re-verified from scratch at head 65964a6. This closes #50 the way the issue asked (option 1), and the failure-mode coverage is the strongest part of the PR. ## What I checked **Both direct paths are gone.** The per-user convergence (`commands/users-apply.sh:280`) and the dropped-user sweep (`commands/users-apply.sh:352`) now route `incus` through the single `drop_incus` helper; the bare `gpasswd -d` remains only for `rig-admin`/`rig`, which are rig's own groups. Grepped the whole tree: no other site touches the `incus` group directly, and nothing anywhere touches `incus-admin` (it stays a non-role, per the README). The test at `test/cli.sh:761` pins the count at exactly 2 call sites. **Box absent or failing is loud, and the state is reconciled, not lost.** `drop_incus` (`commands/users-apply.sh:220-244`) covers the full matrix: box succeeds (bare revoke, effective-state check, box's own warning passes through); box exits 0 with the membership standing (caught by the `! in_group` re-check — the #12 "don't trust exit codes" discipline, correctly applied since the membership is what closes the socket); box exits non-zero (warn + `gpasswd -d` fallback); box not installed (same fallback). Every fallback path carries the session warning itself, which is the actual bug being fixed — the old code's silence, not its `gpasswd`. There is no path where the role is dropped in rig's model while the group quietly survives. **`--purge` is provably unreachable.** Asserted both by shape (`test/cli.sh:770`) and at runtime on all three box-reached paths (`test/cli.sh:846-848`) — a fallback never escalates to the destructive verb. Right call: convergence removes access, never running machines. **Idempotence.** Both call sites gate on `in_group`, so a never-granted or already-revoked user is a clean no-op, and the absent-group case (host=no, or `box setup-host` never ran) needs no new guard because `id -nG` cannot report a nonexistent group — the code comment at `commands/users-apply.sh:216-219` states this rather than leaving it to be rediscovered. The sweep's transition-only warning semantics are preserved. **Tests are real, not vacuous.** The suite lifts `drop_incus` verbatim from the live file (guarded against an empty lift at `test/cli.sh:782`), replaces PATH entirely so "absent" means absent even on a box-bearing host, and the stub `gpasswd` speaks on stderr because the real call is `>/dev/null`-silenced — both anti-vacuity guards hold up under inspection. Ran locally: `test/cli.sh` **344 passed, 0 failed** (19 new), `test/release.sh` **41 passed, 0 failed**, `shellcheck -x` clean across the sweep. All match the PR's claims. ## Non-blocking observations 1. **Wording in one corner** (`commands/users-apply.sh:231`): if `box revoke` exits non-zero but *did* remove the membership (e.g., dies after the group edit), rig warns "'box revoke' did not remove the incus group — removing it directly", then removes nothing (the `in_group` guard at :233 is false) and logs no removal at all. State and session warning are both correct; only the warn's claim is wrong in that corner. Something like "failed or left the membership standing — reconciling directly" would be accurate on both sub-paths. Fine to leave. 2. **Series note**: the grant side (`usermod -aG` at `commands/users-apply.sh:274`) still bypasses `box grant` — that is #53's scope, and #53 is open against `main` touching this same file. Whichever of #53/#55 merges second will need a rebase; when reconciling, `drop_incus`'s pattern (effective-state check, no destructive escalation, fallback that speaks) is the one the grant path should mirror. 3. The lift-by-`sed` extraction couples the test to `drop_incus` staying at column 0 in `users-apply.sh`. The non-empty/whole guard means a future reshaping fails loudly rather than vacuously, which is the right failure direction — just be aware the coupling exists. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:26:05 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is.

Why

  • Dropping role box / full offboarding routes incus through box revoke (no --purge) with effective-state check after revoke (exit code not trusted).
  • Fallback when box absent or hollow still carries the session warning — the silence was the bug. Both removal paths share drop_incus. Stub-driven tests cover ok/hollow/fail/absent. CI green.

No blockers.

**Verdict: Approve** — I agree with this as-is. ### Why - Dropping role `box` / full offboarding routes `incus` through `box revoke` (no `--purge`) with effective-state check after revoke (exit code not trusted). - Fallback when box absent or hollow still carries the **session warning** — the silence was the bug. Both removal paths share `drop_incus`. Stub-driven tests cover ok/hollow/fail/absent. CI green. No blockers.
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:31:28 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback. Both incus removal paths now delegate to bare box revoke, verify effective membership state instead of trusting its exit code, and fall back without ever escalating to --purge. The fallback preserves the session warning that motivated the fix, and the current CI checks are green.

Verdict: I agree with everything and have no additional feedback. Both `incus` removal paths now delegate to bare `box revoke`, verify effective membership state instead of trusting its exit code, and fall back without ever escalating to `--purge`. The fallback preserves the session warning that motivated the fix, and the current CI checks are green.
dan-claude-bot commented 2026-07-19 16:37:24 +00:00 (Migrated from github.com)

@danmt — handoff summary.

What this fixes

A live bug, independent of the other PRs in this batch. rig users apply converged incus membership with a bare gpasswd -d at two sites — the per-user arm and the dropped-user sweep. rig owns rig-admin and rig; box owns incus, and box revoke does strictly more with it: it warns that supplementary groups are fixed at login, so a session the user already holds keeps the Incus socket until it dies, and it names loginctl terminate-user as the remedy.

apply logged "removed from incus" and moved on. An operator who dropped someone from the users file and watched apply succeed believed their VM access was gone — and was wrong for as long as that user held a session. The silence was the bug, not the gpasswd. Closes #50.

Review history

One round, no fixes needed. 3/3 approved at head 65964a6 — claude-bot, codex-bot, grok-bot. claude-bot re-verified from scratch and called the failure-mode coverage the strongest part of the PR.

Verification

  • test/cli.sh 344 passed / 0 failed (baseline 325 — 19 new checks)
  • test/release.sh 41 passed / 0 failed
  • shellcheck -x over the CI sweep, 22 files: clean
  • CI: check, scope, db-integration, reconcile all green
  • Mutation-checked: reverting drop_incus to the old bare gpasswd -d turns 10 of the new assertions red — the tests fail against the bug they describe

How it works

Both removal sites route through one drop_incus helper: call bare box revokeverify the membership actually went rather than trusting the exit code (the repo's own #12 discipline; the membership is what closes the socket) → on hollow success, failure, or box absent, fall back to gpasswd -d carrying the session warning itself. --purge is never passed on any path, asserted both by shape and at runtime — convergence removes access, never running machines.

Test quality worth noting

The suite lifts drop_incus verbatim out of the live file and drives it against stubs with PATH fully replaced, so "box absent" means absent even on a box-bearing host. Two anti-vacuity guards were live bugs during development: the extraction is asserted non-empty and whole before use, and the gpasswd stub speaks on stderr because the real call is >/dev/null-silenced — a stdout stub would have made every fallback assertion pass for free.

Flagged for your judgment

One wording corner claude-bot caught (users-apply.sh:231): if box revoke exits non-zero but did remove the membership — dying after the group edit — rig warns "'box revoke' did not remove the incus group — removing it directly", then removes nothing, because the guard below is correctly false. State and session warning are both right; only the warning's claim is wrong in that one sub-path. "failed or left the membership standing — reconciling directly" would be accurate on both. I left it rather than restart an approved round for a wording fix — say the word and I'll push it.

A coupling to be aware of: the test's sed lift ties itself to drop_incus staying at column 0. The non-empty/whole guard means a future reshaping fails loudly rather than vacuously, which is the right failure direction — but the coupling exists.

Merge order matters

#53 touches this same file — it owns the add arm, this PR owns the removal arms, deliberately non-overlapping. Whichever lands second needs a rebase; both also touch CHANGELOG's ## Unreleased. claude-bot's suggestion when reconciling: drop_incus's pattern — effective-state check, no destructive escalation, fallback that speaks — is the one the grant path should mirror.

🤖 Generated with Claude Code

@danmt — handoff summary. ## What this fixes A live bug, independent of the other PRs in this batch. `rig users apply` converged `incus` membership with a bare `gpasswd -d` at two sites — the per-user arm and the dropped-user sweep. rig owns `rig-admin` and `rig`; **box owns `incus`**, and `box revoke` does strictly more with it: it warns that supplementary groups are fixed at *login*, so a session the user already holds keeps the Incus socket until it dies, and it names `loginctl terminate-user` as the remedy. apply logged "removed <user> from incus" and moved on. An operator who dropped someone from the users file and watched apply succeed believed their VM access was gone — and was wrong for as long as that user held a session. **The silence was the bug, not the `gpasswd`.** Closes #50. ## Review history One round, no fixes needed. **3/3 approved at head `65964a6`** — claude-bot, codex-bot, grok-bot. claude-bot re-verified from scratch and called the failure-mode coverage the strongest part of the PR. ## Verification - `test/cli.sh` **344 passed / 0 failed** (baseline 325 — 19 new checks) - `test/release.sh` **41 passed / 0 failed** - `shellcheck -x` over the CI sweep, 22 files: clean - CI: `check`, `scope`, `db-integration`, `reconcile` all green - **Mutation-checked**: reverting `drop_incus` to the old bare `gpasswd -d` turns **10** of the new assertions red — the tests fail against the bug they describe ## How it works Both removal sites route through one `drop_incus` helper: call bare `box revoke` → **verify the membership actually went** rather than trusting the exit code (the repo's own #12 discipline; the membership is what closes the socket) → on hollow success, failure, or box absent, fall back to `gpasswd -d` **carrying the session warning itself**. `--purge` is never passed on any path, asserted both by shape and at runtime — convergence removes access, never running machines. ## Test quality worth noting The suite lifts `drop_incus` verbatim out of the live file and drives it against stubs with PATH fully replaced, so "box absent" means absent even on a box-bearing host. Two anti-vacuity guards were live bugs during development: the extraction is asserted non-empty and whole before use, and the `gpasswd` stub speaks on **stderr** because the real call is `>/dev/null`-silenced — a stdout stub would have made every fallback assertion pass for free. ## Flagged for your judgment **One wording corner claude-bot caught** (`users-apply.sh:231`): if `box revoke` exits non-zero but *did* remove the membership — dying after the group edit — rig warns "'box revoke' did not remove the incus group — removing it directly", then removes nothing, because the guard below is correctly false. State and session warning are both right; only the warning's claim is wrong in that one sub-path. "failed or left the membership standing — reconciling directly" would be accurate on both. I left it rather than restart an approved round for a wording fix — say the word and I'll push it. **A coupling to be aware of:** the test's `sed` lift ties itself to `drop_incus` staying at column 0. The non-empty/whole guard means a future reshaping fails loudly rather than vacuously, which is the right failure direction — but the coupling exists. ## Merge order matters #53 touches this same file — it owns the add arm, this PR owns the removal arms, deliberately non-overlapping. Whichever lands second needs a rebase; both also touch CHANGELOG's `## Unreleased`. claude-bot's suggestion when reconciling: `drop_incus`'s pattern — effective-state check, no destructive escalation, fallback that speaks — is the one the grant path should mirror. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dan-claude-bot commented 2026-07-19 19:08:17 +00:00 (Migrated from github.com)

Rebased onto main after #60 merged — and the changelog needed hand-placing

@danmt — sweep after #60. This branch is rebased onto main; mergeable: true.

The changelog needed more than conflict resolution, and the reason is worth knowing before you merge the rest of this batch.

main cut 0.1.0 at 18:03:45Z. #60 merged at 19:02:12Z — an hour later. Its entry had been written under ## Unreleased, but by merge time that heading had been stamped to ## 0.1.0, so git merged the entry cleanly, with no conflict, into the notes of an already-published release. Same thing happened to this branch on rebase.

Every PR in this batch predates the release, so every one of them has this. It does not announce itself — #54's rebase produced zero conflicts and still put a BREAKING entry inside 0.1.0's notes.

Resolved here by rebuilding the section structure: a fresh ## Unreleased holding this PR's entry and #60's (moved back out of 0.1.0, where it does not belong — it shipped after), with ## 0.1.0 left holding only what it actually released.

Scope of the damage, checked rather than assumed: the published GitHub release body for 0.1.0 is clean — it was generated at release time, before #60 merged, so it never contained the stray entry. Only the CHANGELOG.md file on main was wrong, and merging any PR from this batch corrects it.

Filed as heavy-duty/rig#66 — the release flow has no guard against this, and it will recur on every release.

Verification at the rebased head

  • shellcheck -x over the CI sweep — clean
  • test/release.sh — 61 / 0
  • Suites re-run; numbers in the per-PR detail below.

Content is otherwise unchanged from what the bots approved; the delta is the rebase plus changelog placement.

## Rebased onto `main` after #60 merged — and the changelog needed hand-placing @danmt — sweep after #60. This branch is rebased onto `main`; `mergeable: true`. **The changelog needed more than conflict resolution, and the reason is worth knowing before you merge the rest of this batch.** `main` cut **0.1.0 at 18:03:45Z**. #60 merged at **19:02:12Z** — an hour later. Its entry had been written under `## Unreleased`, but by merge time that heading had been stamped to `## 0.1.0`, so git merged the entry **cleanly, with no conflict**, into the notes of an already-published release. Same thing happened to this branch on rebase. Every PR in this batch predates the release, so every one of them has this. It does not announce itself — #54's rebase produced **zero** conflicts and still put a BREAKING entry inside 0.1.0's notes. **Resolved here** by rebuilding the section structure: a fresh `## Unreleased` holding this PR's entry and #60's (moved back out of 0.1.0, where it does not belong — it shipped after), with `## 0.1.0` left holding only what it actually released. **Scope of the damage, checked rather than assumed:** the *published* GitHub release body for 0.1.0 is **clean** — it was generated at release time, before #60 merged, so it never contained the stray entry. Only the `CHANGELOG.md` file on `main` was wrong, and merging any PR from this batch corrects it. Filed as heavy-duty/rig#66 — the release flow has no guard against this, and it will recur on every release. ### Verification at the rebased head - `shellcheck -x` over the CI sweep — clean - `test/release.sh` — 61 / 0 - Suites re-run; numbers in the per-PR detail below. Content is otherwise unchanged from what the bots approved; the delta is the rebase plus changelog placement.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/rig#55
No description provided.