release flow: install-from-tag, release automation, and a -dev version convention #83
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The problem
box has a
VERSIONfile, a curatedCHANGELOG.md, a0.6.0tag, and a GitHub release — butinstall.shdownloadsarchive/refs/heads/$REFwithREF=main. The release is a bookmark, not a package:curl | bashhands out whatever main is at that second, and two operators "on 0.6.0" can be running different trees. The 0.6.0 release body is also the auto-generated PR list, not the changelog we actually wrote.#79 delivers the versioned layout (
versions/<v>,current,box versions/use/uninstall) — the mechanism for holding releases side by side — but its installer still hardcodesrefs/heads/$REF, so it cannot install a tag at all yet.The flow
1. A release is a PR, then a tag.
CHANGELOG.mdkeeps its## Unreleasedsection; feature PRs land their entry as part of the PR (as today).release: X.Y.Z— bumpsVERSIONand stamps the Unreleased section with version + date. This PR is also where the release ritual hangs: the full drill on real hardware, recorded indrill/RUNS.md— CI proves the tier's semantics on every PR, a release still proves the boundary.X.Y.Z(the0.6.0tag set the precedent — novprefix), push the tag.2.
release.yml, on tag push:VERSION— fail loudly on mismatch, create nothing.CHANGELOG.mdandgh release createwith it as the body. The release notes are the curated prose, not the PR list.3. The installer defaults to the latest release, not main.
BOX_REFunset → resolve the latest tag by following thehttps://github.com/heavy-duty/box/releases/latestredirect and reading theLocationheader (no API, no token, no rate-limit pain), then downloadarchive/refs/tags/$TAG.tar.gz.BOX_REFset → tryrefs/tags/$REFfirst, fall back torefs/heads/$REF.BOX_REF=0.6.0= pinned,BOX_REF=main= dev. README documents all three.4. Post-release, main's
VERSIONbumps toX.Y.(Z+1)-devimmediately.This becomes load-bearing the moment #79 merges: the versioned layout names install trees after the tree's own
VERSIONfile, and #79 deliberately leaves it at0.6.0— so an install from main would land inversions/0.6.0, impersonating the released 0.6.0. The converging no-op then refuses to update a real 0.6.0 to main tip, andBOX_REINSTALL=1silently swaps a release tree for a dev tree under the same name. A-devversion on main keeps dev installs side by side with releases instead of colliding with them (#79'scaseguard already admits dashed names).Order
release.yml+ the-devconvention.curl | bashreproducibly yieldsversions/0.7.0.Downstream
rig installs box onto hosts (
docs/plans/2026-07-17-host-installs-box.md); onceBOX_REF=<tag>works, rig pins the box version it drilled against instead of installing main. Tracked on the rig side.Release drill report — 2026-07-19, full e2e across box/cast/rig
Ran the whole surface on a disposable host (7.7GiB, /dev/kvm, real incus + docker): every release ceremony rehearsed for real on scratch forks, every install channel exercised against real GitHub, plus live legs for each tool. Verdict: all three flows are GO for their first ceremonies (box 0.7.0/0.6.1, rig 0.1.0, cast 0.1.0), with six findings filed — none of them blockers to the flow itself.
The release flow (all three repos)
release.ymlrehearsed live on forks, three paths each: mismatched tag → fails loudly, creates nothing; matching tag, no CHANGELOG section → fails at the notes step, creates nothing; real ceremony (bump + stamp + bare tag) → release created with the curated section as body (byte-identical torelease-notes.shoutput, modulo GitHub's trailing newline). cast's run also built and attachedcast-0.1.0.tgz— and the published-asset install path ran for the first time ever: prebuilt tree, no npm,cast 0.1.0answers.*_REF=main, exit 1, install nothing — the designed pre-first-release behavior); pinned tag; branch/dev landing side-by-side underversions/with honest converge/flip semantics.Live legs
drill/drill.sh --yeson real incus VMs — 85 passed, 0 failed, full isolation contract measured from inside the boxes. (Host note: 7GiB meant 3GiB/2cpu mints; the claude template's 8GiB default was not drilled.)coolify install --version 4.1.2+coolify backup install(honest loud failure at its credential boundary).apply(project + env + pg + redis, default-productionremoval), idempotent re-apply + clean diff,smokeproving the bulk-env upsert contract on both boxes, sweep +--emit-draft, coverage-honest fleet verdicts,destroybehind the typed gate deleting only manifest resources and preserving the hand-built orphan, read-only-instance refusal.Findings → issues (fix PRs in flight via the usual bot loop)
runner repoint)CAST_AGE_KEY_FILE_<ENV>unsettable for hyphenated envs (PR heavy-duty/cast#105)incus launchwedge (PR in progress)After the fix PRs land: the ceremonies themselves, then the cross-repo pin step (templates'
RIG_REF, rig'sBOX_REF) closes the loop this issue designed.🤖 Posted from the release-drill session.