release flow: install-from-tag, release automation, and a -dev version convention #83

Closed
opened 2026-07-18 17:21:46 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-18 17:21:46 +00:00 (Migrated from github.com)

The problem

box has a VERSION file, a curated CHANGELOG.md, a 0.6.0 tag, and a GitHub release — but install.sh downloads archive/refs/heads/$REF with REF=main. The release is a bookmark, not a package: curl | bash hands 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 hardcodes refs/heads/$REF, so it cannot install a tag at all yet.

The flow

1. A release is a PR, then a tag.

  • CHANGELOG.md keeps its ## Unreleased section; feature PRs land their entry as part of the PR (as today).
  • To release: a small PR — release: X.Y.Z — bumps VERSION and stamps the Unreleased section with version + date. This PR is also where the release ritual hangs: the full drill on real hardware, recorded in drill/RUNS.md — CI proves the tier's semantics on every PR, a release still proves the boundary.
  • Merge, tag the merge commit bare X.Y.Z (the 0.6.0 tag set the precedent — no v prefix), push the tag.

2. release.yml, on tag push:

  • Assert tag == VERSION — fail loudly on mismatch, create nothing.
  • Extract that version's section from CHANGELOG.md and gh release create with it as the body. The release notes are the curated prose, not the PR list.
  • No assets: for a pure-bash tree, GitHub's source tarball for the tag is the package.

3. The installer defaults to the latest release, not main.

  • BOX_REF unset → resolve the latest tag by following the https://github.com/heavy-duty/box/releases/latest redirect and reading the Location header (no API, no token, no rate-limit pain), then download archive/refs/tags/$TAG.tar.gz.
  • BOX_REF set → try refs/tags/$REF first, fall back to refs/heads/$REF.
  • That's three channels from one script: default = latest release, BOX_REF=0.6.0 = pinned, BOX_REF=main = dev. README documents all three.

4. Post-release, main's VERSION bumps to X.Y.(Z+1)-dev immediately.

This becomes load-bearing the moment #79 merges: the versioned layout names install trees after the tree's own VERSION file, and #79 deliberately leaves it at 0.6.0 — so an install from main would land in versions/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, and BOX_REINSTALL=1 silently swaps a release tree for a dev tree under the same name. A -dev version on main keeps dev installs side by side with releases instead of colliding with them (#79's case guard already admits dashed names).

Order

  1. #79 merges (the layout).
  2. This issue: tag-resolving installer + release.yml + the -dev convention.
  3. Cut 0.7.0 — the first release that is actually installable as a package: curl | bash reproducibly yields versions/0.7.0.

Downstream

rig installs box onto hosts (docs/plans/2026-07-17-host-installs-box.md); once BOX_REF=<tag> works, rig pins the box version it drilled against instead of installing main. Tracked on the rig side.

## The problem box has a `VERSION` file, a curated `CHANGELOG.md`, a `0.6.0` tag, and a GitHub release — but `install.sh` downloads `archive/refs/heads/$REF` with `REF=main`. The release is a bookmark, not a package: `curl | bash` hands 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 hardcodes `refs/heads/$REF`, so it cannot install a tag at all yet. ## The flow **1. A release is a PR, then a tag.** - `CHANGELOG.md` keeps its `## Unreleased` section; feature PRs land their entry as part of the PR (as today). - To release: a small PR — `release: X.Y.Z` — bumps `VERSION` and stamps the Unreleased section with version + date. This PR is also where the release ritual hangs: the full drill on real hardware, recorded in `drill/RUNS.md` — CI proves the tier's semantics on every PR, **a release still proves the boundary**. - Merge, tag the merge commit bare `X.Y.Z` (the `0.6.0` tag set the precedent — no `v` prefix), push the tag. **2. `release.yml`, on tag push:** - Assert tag == `VERSION` — fail loudly on mismatch, create nothing. - Extract that version's section from `CHANGELOG.md` and `gh release create` with it as the body. The release notes are the curated prose, not the PR list. - No assets: for a pure-bash tree, GitHub's source tarball for the tag **is** the package. **3. The installer defaults to the latest release, not main.** - `BOX_REF` unset → resolve the latest tag by following the `https://github.com/heavy-duty/box/releases/latest` redirect and reading the `Location` header (no API, no token, no rate-limit pain), then download `archive/refs/tags/$TAG.tar.gz`. - `BOX_REF` set → try `refs/tags/$REF` first, fall back to `refs/heads/$REF`. - That's three channels from one script: default = latest release, `BOX_REF=0.6.0` = pinned, `BOX_REF=main` = dev. README documents all three. **4. Post-release, main's `VERSION` bumps to `X.Y.(Z+1)-dev` immediately.** This becomes load-bearing the moment #79 merges: the versioned layout names install trees after the tree's own `VERSION` file, and #79 deliberately leaves it at `0.6.0` — so an install from main would land in `versions/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, and `BOX_REINSTALL=1` silently swaps a release tree for a dev tree under the same name. A `-dev` version on main keeps dev installs side by side with releases instead of colliding with them (#79's `case` guard already admits dashed names). ## Order 1. #79 merges (the layout). 2. This issue: tag-resolving installer + `release.yml` + the `-dev` convention. 3. Cut **0.7.0** — the first release that is actually installable as a package: `curl | bash` reproducibly yields `versions/0.7.0`. ## Downstream rig installs box onto hosts (`docs/plans/2026-07-17-host-installs-box.md`); once `BOX_REF=<tag>` works, rig pins the box version it drilled against instead of installing main. Tracked on the rig side.
dan-claude-bot commented 2026-07-19 12:21:22 +00:00 (Migrated from github.com)

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.yml rehearsed 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 to release-notes.sh output, modulo GitHub's trailing newline). cast's run also built and attached cast-0.1.0.tgz — and the published-asset install path ran for the first time ever: prebuilt tree, no npm, cast 0.1.0 answers.
  • All nine install channels verified live: default → latest release (box resolved its real 0.6.0; cast/rig refuse loudly naming *_REF=main, exit 1, install nothing — the designed pre-first-release behavior); pinned tag; branch/dev landing side-by-side under versions/ with honest converge/flip semantics.
  • Offline suites: box 402+47, rig 321+41 (+12 db-integration vs real postgres), cast 598 — all green on main.

Live legs

  • box: drill/drill.sh --yes on 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.)
  • rig: four tenant guests (claude/codex/grok/staging) minted through box, each converged and re-converged; agent CLIs answer on the non-interactive PATH; real GitHub runner lifecycle (install → online → converge → wrong-repo refusal → deregister); coolify install --version 4.1.2 + coolify backup install (honest loud failure at its credential boundary).
  • cast: two live Coolify 4.1.2 instances (A installed by rig — nice cross-tool e2e — B by compose), then the full A→B promotion through one state repo: team asserts, first-ever live apply (project + env + pg + redis, default-production removal), idempotent re-apply + clean diff, smoke proving the bulk-env upsert contract on both boxes, sweep + --emit-draft, coverage-honest fleet verdicts, destroy behind 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)

  • rig#42 headless prompts die silently (PR heavy-duty/rig#43 — the sweep test it added caught two more prompts in runner repoint)
  • cast#102 CAST_AGE_KEY_FILE_<ENV> unsettable for hyphenated envs (PR heavy-duty/cast#105)
  • cast#103 GitHub App resolved even for databases-only manifests · cast#104 greenfield store chicken-and-egg (PRs in progress)
  • box#92 released 0.6.0 setup-host leaves a stale ACL carve-out on a moved subnet — already fixed on main, i.e. evidence for shipping the next box release promptly · box#93 intermittent silent incus launch wedge (PR in progress)

After the fix PRs land: the ceremonies themselves, then the cross-repo pin step (templates' RIG_REF, rig's BOX_REF) closes the loop this issue designed.

🤖 Posted from the release-drill session.

## 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.yml` rehearsed 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 to `release-notes.sh` output, modulo GitHub's trailing newline). cast's run also built and attached `cast-0.1.0.tgz` — and the **published-asset install path ran for the first time ever**: prebuilt tree, no npm, `cast 0.1.0` answers. - All nine install channels verified live: default → latest release (box resolved its real 0.6.0; cast/rig **refuse loudly** naming `*_REF=main`, exit 1, install nothing — the designed pre-first-release behavior); pinned tag; branch/dev landing side-by-side under `versions/` with honest converge/flip semantics. - Offline suites: box 402+47, rig 321+41 (+12 db-integration vs real postgres), cast 598 — all green on main. ### Live legs - **box**: `drill/drill.sh --yes` on 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.) - **rig**: four tenant guests (claude/codex/grok/staging) minted through box, each converged and re-converged; agent CLIs answer on the non-interactive PATH; real GitHub runner lifecycle (install → online → converge → wrong-repo refusal → deregister); `coolify install --version 4.1.2` + `coolify backup install` (honest loud failure at its credential boundary). - **cast**: **two live Coolify 4.1.2 instances** (A installed by rig — nice cross-tool e2e — B by compose), then the full A→B promotion through one state repo: team asserts, first-ever live `apply` (project + env + pg + redis, default-`production` removal), idempotent re-apply + clean diff, `smoke` proving the bulk-env **upsert** contract on both boxes, sweep + `--emit-draft`, coverage-honest fleet verdicts, `destroy` behind 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) - rig#42 headless prompts die silently (PR heavy-duty/rig#43 — the sweep test it added caught two more prompts in `runner repoint`) - cast#102 `CAST_AGE_KEY_FILE_<ENV>` unsettable for hyphenated envs (PR heavy-duty/cast#105) - cast#103 GitHub App resolved even for databases-only manifests · cast#104 greenfield store chicken-and-egg (PRs in progress) - box#92 released 0.6.0 setup-host leaves a stale ACL carve-out on a moved subnet — already fixed on main, i.e. **evidence for shipping the next box release promptly** · box#93 intermittent silent `incus launch` wedge (PR in progress) After the fix PRs land: the ceremonies themselves, then the cross-repo pin step (templates' `RIG_REF`, rig's `BOX_REF`) closes the loop this issue designed. 🤖 Posted from the release-drill session.
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/box#83
No description provided.