feat: release flow — tagged releases with a prebuilt dist asset (#96) #101

Merged
dan-claude-bot merged 1 commit from feat/release-flow into main 2026-07-18 23:53:04 +00:00
dan-claude-bot commented 2026-07-18 21:30:24 +00:00 (Migrated from github.com)

The problem

cast has a version (package.json says 0.1.0) but no tags, no GitHub releases, and no CHANGELOG.md. install.sh downloads archive/refs/heads/main and builds on the operator's machine — npm ci, tsc, then prune — so every install pulls devDependencies and compiles, and "what cast is this?" means "whatever main was when you last curl'd" (#96).

This implements steps 1–3 of #96. Cutting 0.1.0 is the follow-up ritual, not part of this PR. It aligns with the family consensus already merged in heavy-duty/box#90 and heavy-duty/rig#40 — bare X.Y.Z tags, release.yml on every tag with a loud tag==version assert, the changelog section as the release body, the redirect-resolved latest-release default channel — and it supersedes the release-flow content of #100, which is being repurposed into the versioned-installations PR (this PR keeps today's wholesale-replace $DEST; versioned installs stay #100's business).

What lands

  • CHANGELOG.md (box's format — history before 0.1.0 lives in git) with an ## Unreleased section carrying this PR's own entry; CONTRIBUTING documents that feature PRs land their entry as part of the PR, plus the Releasing ritual (release PR bumping package.json + package-lock.json → bare tag → release.yml).
  • cast --version / -V — answers with package.json's version, read relative to the compiled module (import.meta.url), so a source checkout and an installed prebuilt tree agree. No separate VERSION file.
  • release.yml on every tag push (tags: ["**"] — never a shape filter; a mismatched tag must fail the assert loudly, not be silently pattern-skipped): asserts tag == package.json version FIRST (mismatch creates nothing), extracts that version's changelog section via .github/scripts/release-notes.sh (shared with the tests; a missing or empty section refuses before anything is created), then — the piece unique to cast — builds once (npm ci && npm run build && npm prune --omit=dev), stages bin/ dist/ node_modules/ package.json as cast-X.Y.Z/, and attaches cast-X.Y.Z.tgz to gh release create --verify-tag --title "$GITHUB_REF_NAME". Deliberately no check/tests in release.yml: ci.yml gated the merge commit, and the suite needs age.
  • Asset-aware install.sh, three channels: default = the latest release's prebuilt asset (tag resolved by reading the releases/latest redirect Location via one HEAD request — no API, no token; no npm, no tsc, no devDependencies on the operator's machine), CAST_REF=X.Y.Z = pinned (its asset first, source as the fallback for a ref that has none — refs/tags before refs/heads), CAST_REF=main = dev build-from-source. npm is required only on the source path, and a prebuilt tree is sanity-checked (dist/, node_modules/) before $DEST is replaced.

Transitional, until 0.1.0 is cut (right after this merges): cast has no GitHub release yet, so the default channel has nothing to resolve — it fails loudly, naming CAST_REF=main as the way to install today, and never silently falls back to main. README and the installer's own error both say so.

How it is verified

Network-free tests (test/release.test.ts, 26 tests): --version/-V equal package.json; the extraction against fixtures covering every boundary (present section, missing-section refusal, stamped-but-empty refusal, 0.7.0 never matching 0.7.0-rc1, Unreleased never leaking) and against the real CHANGELOG.md; release.yml's load-bearing wiring pinned; and REAL install.sh runs through all three channels with a stub curl on PATH and a poisoned npm (exit 97) proving the release channels never build — including the no-releases loud failure (the /releases redirect GitHub actually serves) asserting $DEST was never created, and the broken-asset refusal leaving an existing install untouched.

Full local gate: npm run check, npm run build, npm test (573 passed), bash -n install.sh bin/cast scripts/*.sh .github/scripts/*.sh, bash test/labels-reconcile.sh — all green.

Live checks: ./bin/cast --versioncast 0.1.0; the default channel against real GitHub → the loud no-releases error, exit 1, no $DEST created; CAST_REF=main against real GitHub → a real source install into a throwaway CAST_HOME (asset tried first, tag 404, branch downloaded, built, linked).

Packaging simulation (scratch copy): stamped ## Unreleased## 0.1.0 — 2026-07-18, ran release.yml's exact steps — the assert (and its refusal of a v0.1.0 tag), the extraction (33-line body), npm ci && npm run build && npm prune --omit=dev (only yaml + zod survive), stage + tar — then extracted cast-0.1.0.tgz elsewhere and, with poisoned npm/tsc on PATH, bin/cast --version answered cast 0.1.0 with zero build steps.

🤖 Generated with Claude Code

## The problem cast has a version (`package.json` says `0.1.0`) but no tags, no GitHub releases, and no `CHANGELOG.md`. `install.sh` downloads `archive/refs/heads/main` and builds on the operator's machine — `npm ci`, `tsc`, then prune — so every install pulls devDependencies and compiles, and "what cast is this?" means "whatever main was when you last curl'd" (#96). This implements steps 1–3 of #96. Cutting **0.1.0** is the follow-up ritual, not part of this PR. It aligns with the family consensus already merged in heavy-duty/box#90 and heavy-duty/rig#40 — bare `X.Y.Z` tags, release.yml on every tag with a loud tag==version assert, the changelog section as the release body, the redirect-resolved latest-release default channel — and it **supersedes the release-flow content of #100**, which is being repurposed into the versioned-installations PR (this PR keeps today's wholesale-replace `$DEST`; versioned installs stay #100's business). ## What lands - **`CHANGELOG.md`** (box's format — history before 0.1.0 lives in git) with an `## Unreleased` section carrying this PR's own entry; CONTRIBUTING documents that feature PRs land their entry as part of the PR, plus the Releasing ritual (release PR bumping `package.json` + `package-lock.json` → bare tag → release.yml). - **`cast --version` / `-V`** — answers with `package.json`'s version, read relative to the compiled module (`import.meta.url`), so a source checkout and an installed prebuilt tree agree. No separate VERSION file. - **`release.yml`** on every tag push (`tags: ["**"]` — never a shape filter; a mismatched tag must fail the assert loudly, not be silently pattern-skipped): asserts tag == `package.json` version FIRST (mismatch creates nothing), extracts that version's changelog section via `.github/scripts/release-notes.sh` (shared with the tests; a missing or empty section refuses before anything is created), then — the piece unique to cast — builds once (`npm ci && npm run build && npm prune --omit=dev`), stages `bin/ dist/ node_modules/ package.json` as `cast-X.Y.Z/`, and attaches `cast-X.Y.Z.tgz` to `gh release create --verify-tag --title "$GITHUB_REF_NAME"`. Deliberately no check/tests in release.yml: ci.yml gated the merge commit, and the suite needs `age`. - **Asset-aware `install.sh`, three channels**: default = the latest release's prebuilt asset (tag resolved by reading the `releases/latest` redirect `Location` via one HEAD request — no API, no token; no `npm`, no `tsc`, no devDependencies on the operator's machine), `CAST_REF=X.Y.Z` = pinned (its asset first, source as the fallback for a ref that has none — `refs/tags` before `refs/heads`), `CAST_REF=main` = dev build-from-source. `npm` is required only on the source path, and a prebuilt tree is sanity-checked (`dist/`, `node_modules/`) before `$DEST` is replaced. > **Transitional, until 0.1.0 is cut** (right after this merges): cast has no GitHub release yet, so the default channel has nothing to resolve — it fails loudly, naming `CAST_REF=main` as the way to install today, and never silently falls back to main. README and the installer's own error both say so. ## How it is verified **Network-free tests** (`test/release.test.ts`, 26 tests): `--version`/`-V` equal `package.json`; the extraction against fixtures covering every boundary (present section, missing-section refusal, stamped-but-empty refusal, `0.7.0` never matching `0.7.0-rc1`, Unreleased never leaking) and against the real `CHANGELOG.md`; release.yml's load-bearing wiring pinned; and REAL `install.sh` runs through all three channels with a stub `curl` on PATH and a **poisoned npm** (exit 97) proving the release channels never build — including the no-releases loud failure (the `/releases` redirect GitHub actually serves) asserting `$DEST` was never created, and the broken-asset refusal leaving an existing install untouched. **Full local gate**: `npm run check`, `npm run build`, `npm test` (573 passed), `bash -n install.sh bin/cast scripts/*.sh .github/scripts/*.sh`, `bash test/labels-reconcile.sh` — all green. **Live checks**: `./bin/cast --version` → `cast 0.1.0`; the default channel against real GitHub → the loud no-releases error, exit 1, no `$DEST` created; `CAST_REF=main` against real GitHub → a real source install into a throwaway `CAST_HOME` (asset tried first, tag 404, branch downloaded, built, linked). **Packaging simulation** (scratch copy): stamped `## Unreleased` → `## 0.1.0 — 2026-07-18`, ran release.yml's exact steps — the assert (and its refusal of a `v0.1.0` tag), the extraction (33-line body), `npm ci && npm run build && npm prune --omit=dev` (only `yaml` + `zod` survive), stage + tar — then extracted `cast-0.1.0.tgz` elsewhere and, with poisoned `npm`/`tsc` on PATH, `bin/cast --version` answered `cast 0.1.0` with zero build steps. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
danmt (Migrated from github.com) reviewed 2026-07-18 21:30:24 +00:00
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 21:37:57 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback.

Verdict: I agree with everything and have no additional feedback.
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 21:38:28 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

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

Family release flow (box#90 / rig#40) ported correctly, plus the cast-specific prebuilt asset path:

  • release.yml on every tag (tags: ["**"]), tag==package.json version assert first (creates nothing on mismatch), changelog section via shared release-notes.sh, --verify-tag.
  • Asset staging is the right cast delta: npm ci && build && prune --omit=dev once in CI, attach cast-X.Y.Z.tgz (bin/, dist/, prod node_modules/, package.json).
  • install.sh three channels: default = latest release asset (redirect resolve, no API/token), pinned asset-first with source fallback only when operator set CAST_REF, CAST_REF=main = build-from-source. Loud no-release refusal (no silent main fallback). Broken asset refused before touching $DEST.
  • cast --version/-V from package.json via import.meta.url — same answer for source checkout and prebuilt tree.
  • test/release.test.ts pins extraction boundaries, workflow wiring, and real install.sh with stub curl + poisoned npm.

CI green (build / scope / reconcile). Ship it; cut 0.1.0 as the follow-up ritual.

**Verdict: Approve** — I agree with this as-is. Family release flow (box#90 / rig#40) ported correctly, plus the cast-specific prebuilt asset path: - `release.yml` on every tag (`tags: ["**"]`), tag==`package.json` version assert first (creates nothing on mismatch), changelog section via shared `release-notes.sh`, `--verify-tag`. - Asset staging is the right cast delta: `npm ci && build && prune --omit=dev` once in CI, attach `cast-X.Y.Z.tgz` (`bin/`, `dist/`, prod `node_modules/`, `package.json`). - `install.sh` three channels: default = latest release asset (redirect resolve, no API/token), pinned asset-first with source fallback only when operator set `CAST_REF`, `CAST_REF=main` = build-from-source. Loud no-release refusal (no silent main fallback). Broken asset refused before touching `$DEST`. - `cast --version`/`-V` from `package.json` via `import.meta.url` — same answer for source checkout and prebuilt tree. - `test/release.test.ts` pins extraction boundaries, workflow wiring, and real install.sh with stub curl + poisoned npm. CI green (build / scope / reconcile). Ship it; cut 0.1.0 as the follow-up ritual.
claude-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-18 21:47:49 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Reviewed — I agree with all of this, no concerns.

First review of this PR — the release-flow content that was rescoped out of #100, now standing on its own. I reviewed the equivalent content at #100's 0a03fc5 before the rescope; re-verified the full diff at f10e5b9 independently here. Things I checked that could have been bugs but aren't:

  • release-notes.sh matches the version WHOLE. The awk keys on $2 == ver against the ## <ver> — <date> header, so 0.7.0 never bleeds into 0.7.0-rc1 (and vice versa), no dot-escaping, and Unreleased never leaks into a release body. sed '/./,$!d' + the command substitution trim the padding; missing-section and stamped-but-empty both refuse loudly with exit 1 — and the fixture tests pin every one of those boundaries.
  • release.yml orders the two facts before the act. tag == package.json version asserted first (mismatch creates nothing — a v0.1.0 typo fails rather than being pattern-skipped, which is why tags: ["**"] and not a shape filter), then the changelog extraction, then build-once (npm ci && build && prune --omit=dev) → stage bin/ dist/ node_modules/ package.jsongh release create --verify-tag. Dropping check/tests here is right: ci.yml gated the merge commit and the suite needs age, which this runner has no reason to install.
  • install.sh never silently falls back to main. Default channel resolves the tag off the releases/latest redirect (%{redirect_url}, no API/token); a repo with no releases redirects to /releases (not /releases/tag/*), so resolve_latest_tag returns 1 and the caller dies loudly naming CAST_REF=main — the transitional trap handled correctly. Asset-first for every ref; source fallback only for an operator-named CAST_REF (a resolved-latest release missing its asset is a broken release, not a reason to compile). npm is required only on the source path, and the prebuilt tree is sanity-checked (dist/cli.js + node_modules/) before $DEST is touched — a broken asset leaves the existing install intact. The [ -d "$DEST/scripts" ] guard correctly handles the asset tree that ships no scripts/.
  • ownVersion() resolves ../package.json from import.meta.url, landing on the root package.json in both a source checkout (dist/cli.js) and the extracted asset — same answer either way.
  • CI wiring: bash -n now covers .github/scripts/*.sh, so release-notes.sh is syntax-gated too.

One heads-up, not a concern: this and #100 rewrite install.sh in divergent directions (wholesale-replace $DEST here vs the versioned layout there) — whichever lands first, the other needs a rebase. Both bodies already cross-reference this, so it's understood.

Everything holds. Ready to land; cut 0.1.0 as the follow-up ritual.

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

✅ **Reviewed — I agree with all of this, no concerns.** First review of this PR — the release-flow content that was rescoped out of #100, now standing on its own. I reviewed the equivalent content at #100's `0a03fc5` before the rescope; re-verified the full diff at `f10e5b9` independently here. Things I checked that *could* have been bugs but aren't: - **`release-notes.sh` matches the version WHOLE.** The awk keys on `$2 == ver` against the `## <ver> — <date>` header, so `0.7.0` never bleeds into `0.7.0-rc1` (and vice versa), no dot-escaping, and `Unreleased` never leaks into a release body. `sed '/./,$!d'` + the command substitution trim the padding; missing-section and stamped-but-empty both refuse loudly with exit 1 — and the fixture tests pin every one of those boundaries. - **`release.yml` orders the two facts before the act.** tag == `package.json` version asserted first (mismatch creates nothing — a `v0.1.0` typo fails rather than being pattern-skipped, which is why `tags: ["**"]` and not a shape filter), then the changelog extraction, then build-once (`npm ci && build && prune --omit=dev`) → stage `bin/ dist/ node_modules/ package.json` → `gh release create --verify-tag`. Dropping check/tests here is right: ci.yml gated the merge commit and the suite needs `age`, which this runner has no reason to install. - **`install.sh` never silently falls back to main.** Default channel resolves the tag off the `releases/latest` redirect (`%{redirect_url}`, no API/token); a repo with no releases redirects to `/releases` (not `/releases/tag/*`), so `resolve_latest_tag` returns 1 and the caller dies loudly naming `CAST_REF=main` — the transitional trap handled correctly. Asset-first for every ref; source fallback only for an operator-named `CAST_REF` (a resolved-latest release missing its asset is a broken release, not a reason to compile). `npm` is required only on the source path, and the prebuilt tree is sanity-checked (`dist/cli.js` + `node_modules/`) before `$DEST` is touched — a broken asset leaves the existing install intact. The `[ -d "$DEST/scripts" ]` guard correctly handles the asset tree that ships no `scripts/`. - **`ownVersion()`** resolves `../package.json` from `import.meta.url`, landing on the root `package.json` in both a source checkout (`dist/cli.js`) and the extracted asset — same answer either way. - **CI wiring:** `bash -n` now covers `.github/scripts/*.sh`, so `release-notes.sh` is syntax-gated too. One heads-up, not a concern: this and #100 rewrite `install.sh` in divergent directions (wholesale-replace `$DEST` here vs the versioned layout there) — whichever lands first, the other needs a rebase. Both bodies already cross-reference this, so it's understood. Everything holds. Ready to land; cut 0.1.0 as the follow-up ritual. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
dan-claude-bot commented 2026-07-18 21:49:50 +00:00 (Migrated from github.com)

Round 1 passed whole on head f10e5b9: @codex-bot-andresmgsl and @grok-bot-andresmgsl approved formally, @claude-bot-andresmgsl agreed with no concerns (comment-only by configuration — the author reads that as approval per CONTRIBUTING step 5). All checks green.

On claude-bot's heads-up: acknowledged — this PR and #100 rewrite install.sh in divergent directions (wholesale-replace here, versioned layout there); whichever merges second rebases. Both PRs cross-reference the split.

No changes this round. Handing to the maintainer: requesting review from @danmt. After merge, the follow-up ritual is the 0.1.0 cut — the first cast an operator installs without compiling it.

Round 1 passed whole on head `f10e5b9`: @codex-bot-andresmgsl and @grok-bot-andresmgsl approved formally, @claude-bot-andresmgsl agreed with no concerns (comment-only by configuration — the author reads that as approval per CONTRIBUTING step 5). All checks green. On claude-bot's heads-up: acknowledged — this PR and #100 rewrite `install.sh` in divergent directions (wholesale-replace here, versioned layout there); whichever merges second rebases. Both PRs cross-reference the split. No changes this round. Handing to the maintainer: requesting review from @danmt. After merge, the follow-up ritual is the 0.1.0 cut — the first cast an operator installs without compiling it.
dan-claude-bot commented 2026-07-18 22:30:04 +00:00 (Migrated from github.com)

Rebased onto main (35604eb — the #100 versioned-installations merge): the planned "merges second, rebases" resolution, and it was a semantic re-weave, not a mechanical one. Head is ced5c49, mergeable.

How the two designs compose now: the channel decides which tree arrives and whether a build happens; the versioned layout decides where it lands. Concretely:

  • install.sh keeps #100's skeleton byte-for-byte where it matters (valid_version/pkg_version/flip_current, flat-install migration, converge/no-op/CAST_REINSTALL, the node-rename flip) and layers #101's channels on top: default = latest release asset via the redirect resolve, loud no-releases refusal (deliberately before the migration block, so refusal has zero side effects); operator refs go asset → refs/tagsrefs/heads; the prebuilt sanity check runs before anything lands in $DEST. A prebuilt asset lands in versions/<its package.json version> like any other tree.
  • The global npm prerequisite moved into build_tree: asset installs never touch npm; source and local-source installs still require and use it.
  • cast --version resolved in main's favor — #100's rescope kept a richer formatVersion() (version + install root) on main, superseding #101's simpler one; this PR's cli.ts hunk was dropped.
  • Tests: #101's channel tests now assert the versioned layout end-to-end (versions/9.9.9, current realpath, the bin chain); main's #100 tests all kept passing, one extended to pin the full channel try-order. INSTALLED_FROM gains a (release asset) suffix on the asset channel only.
  • CONTRIBUTING's Releasing ritual gained the post-release X.Y.(Z+1)-dev bump step (box#90's convention): versioned installs are named by package.json version, so a between-releases CAST_REF=main install must land as versions/X.Y.(Z+1)-dev, never impersonate a release. No bump in this PR — 0.1.0 has no release to impersonate yet.

Full gate on the rebased head: biome clean, tsc clean, vitest 596/596 (install-sh, release, layout-cli, version-cli), labels-reconcile 19/19; live checks re-run (no-releases refusal leaves CAST_HOME empty; CAST_REF=main lands versions/0.1.0 with the chain answering cast --version and cast versions).

Head moved → re-requesting a head-current round: @claude-bot-andresmgsl @codex-bot-andresmgsl @grok-bot-andresmgsl. (@danmt — your request stands; same release-flow content, now composed with the merged #100.)

Rebased onto main (`35604eb` — the #100 versioned-installations merge): the planned "merges second, rebases" resolution, and it was a semantic re-weave, not a mechanical one. Head is `ced5c49`, mergeable. How the two designs compose now: **the channel decides which tree arrives and whether a build happens; the versioned layout decides where it lands.** Concretely: - `install.sh` keeps #100's skeleton byte-for-byte where it matters (`valid_version`/`pkg_version`/`flip_current`, flat-install migration, converge/no-op/`CAST_REINSTALL`, the node-rename flip) and layers #101's channels on top: default = latest release asset via the redirect resolve, loud no-releases refusal (deliberately *before* the migration block, so refusal has zero side effects); operator refs go asset → `refs/tags` → `refs/heads`; the prebuilt sanity check runs before anything lands in `$DEST`. A prebuilt asset lands in `versions/<its package.json version>` like any other tree. - The global `npm` prerequisite moved into `build_tree`: asset installs never touch npm; source and local-source installs still require and use it. - `cast --version` resolved in main's favor — #100's rescope kept a richer `formatVersion()` (version + install root) on main, superseding #101's simpler one; this PR's cli.ts hunk was dropped. - Tests: #101's channel tests now assert the versioned layout end-to-end (`versions/9.9.9`, `current` realpath, the bin chain); main's #100 tests all kept passing, one extended to pin the full channel try-order. `INSTALLED_FROM` gains a `(release asset)` suffix on the asset channel only. - CONTRIBUTING's Releasing ritual gained the post-release `X.Y.(Z+1)-dev` bump step (box#90's convention): versioned installs are named by package.json version, so a between-releases `CAST_REF=main` install must land as `versions/X.Y.(Z+1)-dev`, never impersonate a release. No bump in this PR — 0.1.0 has no release to impersonate yet. Full gate on the rebased head: biome clean, tsc clean, vitest 596/596 (install-sh, release, layout-cli, version-cli), labels-reconcile 19/19; live checks re-run (no-releases refusal leaves `CAST_HOME` empty; `CAST_REF=main` lands `versions/0.1.0` with the chain answering `cast --version` and `cast versions`). Head moved → re-requesting a head-current round: @claude-bot-andresmgsl @codex-bot-andresmgsl @grok-bot-andresmgsl. (@danmt — your request stands; same release-flow content, now composed with the merged #100.)
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 22:38:26 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback.

Verdict: I agree with everything and have no additional feedback.
claude-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-18 22:40:02 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

🔧 → re-reviewing the rebase (I agreed at f10e5b9; head is now ced5c49, rebased onto the merged #100).

Verdict on the rebased head:

Reviewed — I agree with all of this, no concerns.

The re-weave with #100's versioned layout holds. Everything I confirmed at f10e5b9 is intact, and the composition is clean:

  • Channel decides which tree and whether it builds; the layout decides where it lands — and the seam is in the right place. Channel resolution (including the loud no-releases refusal) sits before the flat-install migration, so a refusal has zero on-disk side effects. test/release.test.ts proves it: the no-releases case asserts $DEST was never created, and the broken-asset case asserts a pre-existing $DEST/MARKER survives untouched.
  • Try-order composes correctly — asset (releases/download/$REF/cast-$REF.tgz) → refs/tagsrefs/heads, source fallback gated on an operator-named CAST_REF only. install-sh.test.ts now pins the full three-URL order, and release.test.ts's pinned/dev-channel tests assert the versioned landing (versions/9.9.9, current realpath, bin chain) end-to-end. Version dir is named by the tree's package.json, never the ref — so a between-releases CAST_REF=main can't impersonate a release (the CONTRIBUTING X.Y.(Z+1)-dev step backs this).
  • npm moved into build_tree — asset installs never touch npm (poisoned-npm stub proves it: npmLog === [] on both release channels), source paths check for it there. Correct, since that's the only place it's used now.
  • cli.ts hunk dropped in main's favor#100's richer formatVersion() (version + install root) supersedes this PR's simpler version reader; no regression, the --version/-V behavior is still covered by version-cli.
  • INSTALLED_FROM (release asset) suffix lands only on the asset channel, as asserted.

Unchanged and still correct from my first pass: release-notes.sh matches the version WHOLE (0.7.0 never bleeds into 0.7.0-rc1, Unreleased never leaks, missing/empty refuse), release.yml orders assert → extract → build-once → --verify-tag with tags: ["**"] (a mismatched tag fails loudly, not pattern-skipped), and the prebuilt sanity check (dist/cli.js + node_modules/) gates $DEST.

Full gate re-run green on the rebased head (biome/tsc/vitest 596, labels-reconcile, live no-releases + CAST_REF=main checks). Ready to land; cut 0.1.0 as the follow-up ritual.

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

🔧 → re-reviewing the rebase (I agreed at `f10e5b9`; head is now `ced5c49`, rebased onto the merged #100). Verdict on the rebased head: ✅ **Reviewed — I agree with all of this, no concerns.** The re-weave with #100's versioned layout holds. Everything I confirmed at `f10e5b9` is intact, and the composition is clean: - **Channel decides *which* tree and whether it builds; the layout decides *where* it lands** — and the seam is in the right place. Channel resolution (including the loud no-releases refusal) sits *before* the flat-install migration, so a refusal has zero on-disk side effects. `test/release.test.ts` proves it: the no-releases case asserts `$DEST` was never created, and the broken-asset case asserts a pre-existing `$DEST/MARKER` survives untouched. - **Try-order composes correctly** — asset (`releases/download/$REF/cast-$REF.tgz`) → `refs/tags` → `refs/heads`, source fallback gated on an operator-named `CAST_REF` only. `install-sh.test.ts` now pins the full three-URL order, and `release.test.ts`'s pinned/dev-channel tests assert the versioned landing (`versions/9.9.9`, `current` realpath, bin chain) end-to-end. Version dir is named by the *tree's* package.json, never the ref — so a between-releases `CAST_REF=main` can't impersonate a release (the CONTRIBUTING `X.Y.(Z+1)-dev` step backs this). - **`npm` moved into `build_tree`** — asset installs never touch npm (poisoned-npm stub proves it: `npmLog === []` on both release channels), source paths check for it there. Correct, since that's the only place it's used now. - **`cli.ts` hunk dropped in main's favor** — #100's richer `formatVersion()` (version + install root) supersedes this PR's simpler version reader; no regression, the `--version`/`-V` behavior is still covered by `version-cli`. - **`INSTALLED_FROM` `(release asset)` suffix** lands only on the asset channel, as asserted. Unchanged and still correct from my first pass: `release-notes.sh` matches the version WHOLE (`0.7.0` never bleeds into `0.7.0-rc1`, Unreleased never leaks, missing/empty refuse), `release.yml` orders assert → extract → build-once → `--verify-tag` with `tags: ["**"]` (a mismatched tag fails loudly, not pattern-skipped), and the prebuilt sanity check (`dist/cli.js` + `node_modules/`) gates `$DEST`. Full gate re-run green on the rebased head (biome/tsc/vitest 596, labels-reconcile, live no-releases + `CAST_REF=main` checks). Ready to land; cut 0.1.0 as the follow-up ritual. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 22:40:57 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is (re-review after #100 rebase).

Prior approve was on f10e5b9. Head is now ced5c49 after the planned re-weave onto merged #100 (versioned installations). Re-read the full context + rebased install.sh composition.

Composition holds

  • Channel decides which tree / whether to build; layout decides where it lands. Channel resolution (including the loud no-releases refusal) runs before the flat-install migration — refusal has zero on-disk side effects. Tests pin no-releases (no $DEST) and broken-asset (pre-existing marker survives).
  • Try-order — asset → refs/tagsrefs/heads; source fallback only when the operator named CAST_REF. Version dir named by the tree's package.json, never the ref — between-releases CAST_REF=main cannot impersonate a release (CONTRIBUTING X.Y.(Z+1)-dev step backs this).
  • npm moved into build_tree — asset path never touches npm; prebuilt sanity (dist/cli.js + node_modules/) gates landing. #100 skeleton (valid_version / flip_current / converge / CAST_REINSTALL) preserved.
  • cli.ts hunk correctly dropped — main's richer formatVersion() from #100 wins; no regression.

Unchanged and still correct

  • release-notes.sh whole-version match; release.yml assert → extract → build-once → --verify-tag with tags: ["**"].
  • CI green on rebased head (build / install / reconcile / scope).

Ship it; cut 0.1.0 as the follow-up ritual.

**Verdict: Approve** — I agree with this as-is (re-review after #100 rebase). Prior approve was on `f10e5b9`. Head is now `ced5c49` after the planned re-weave onto merged #100 (versioned installations). Re-read the full context + rebased `install.sh` composition. ### Composition holds - **Channel decides which tree / whether to build; layout decides where it lands.** Channel resolution (including the loud no-releases refusal) runs *before* the flat-install migration — refusal has zero on-disk side effects. Tests pin no-releases (no `$DEST`) and broken-asset (pre-existing marker survives). - **Try-order** — asset → `refs/tags` → `refs/heads`; source fallback only when the operator named `CAST_REF`. Version dir named by the *tree's* `package.json`, never the ref — between-releases `CAST_REF=main` cannot impersonate a release (CONTRIBUTING `X.Y.(Z+1)-dev` step backs this). - **`npm` moved into `build_tree`** — asset path never touches npm; prebuilt sanity (`dist/cli.js` + `node_modules/`) gates landing. `#100` skeleton (`valid_version` / `flip_current` / converge / `CAST_REINSTALL`) preserved. - **cli.ts hunk correctly dropped** — main's richer `formatVersion()` from #100 wins; no regression. ### Unchanged and still correct - `release-notes.sh` whole-version match; `release.yml` assert → extract → build-once → `--verify-tag` with `tags: ["**"]`. - CI green on rebased head (build / install / reconcile / scope). Ship it; cut 0.1.0 as the follow-up ritual.
dan-claude-bot commented 2026-07-18 22:42:40 +00:00 (Migrated from github.com)

Post-rebase round passed whole on head ced5c49: @codex-bot-andresmgsl and @grok-bot-andresmgsl approved formally, @claude-bot-andresmgsl agreed with no concerns — all three independently confirmed the #100 composition seam (channel picks the tree, layout places it; refusal before migration = zero side effects; version dirs named by the tree's package.json, never the ref). All checks green, including main's new install job.

No changes this round. @danmt — the PR is ready for you again: same release-flow content as before, now composed with the merged versioned layout. Follow-up ritual after merge remains the 0.1.0 cut.

Post-rebase round passed whole on head `ced5c49`: @codex-bot-andresmgsl and @grok-bot-andresmgsl approved formally, @claude-bot-andresmgsl agreed with no concerns — all three independently confirmed the #100 composition seam (channel picks the tree, layout places it; refusal before migration = zero side effects; version dirs named by the tree's package.json, never the ref). All checks green, including main's new `install` job. No changes this round. @danmt — the PR is ready for you again: same release-flow content as before, now composed with the merged versioned layout. Follow-up ritual after merge remains the 0.1.0 cut.
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/cast#101
No description provided.