• 0.3.0 c6a3c23869

    0.3.0 Stable

    github-actions[bot] released this 2026-07-21 18:06:51 +00:00 | 125 commits to main since this release

    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)

    Added

    • CI refuses a release PR with no drill record at drills/<version>.md
    • rig platform — what this machine is, computed at run time, stored nowhere
      (#64)
    • /etc/rig/manifest records which rig converged a machine, and when (#61)

    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)
    Downloads
  • 0.2.0 1acc163163

    0.2.0 Stable

    github-actions[bot] released this 2026-07-19 23:04:21 +00:00 | 193 commits to main since this release

    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.

    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.

    Fixed

    • 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, 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.

    • 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.

    • 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.

    • 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.

    Downloads
  • 0.1.0 6ff90f04ea

    0.1.0 Stable

    github-actions[bot] released this 2026-07-19 18:03:35 +00:00 | 211 commits to main since this release

    Fixed

    • 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.

    • 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.)

    • 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.

    Added

    • Merging a release-labeled PR IS the release — and the release re-arms
      main itself
      (#47) — the rig twin of heavy-duty/box#96, born of the
      ceremony retro: the tag was a separate, manual, silent-when-forgotten
      step, and a forgotten tag produces no red X. release.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.

    • 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.

    Downloads