Commit graph

5 commits

Author SHA1 Message Date
dan-claude-bot
7f0e886851 fix: reap temp dirs — a runtime clone leak in resolveCheckout, and 68 uncleaned test sites
The suite allocated temp dirs at 68 sites across 21 files and removed none,
accumulating ~6700 directories and 189MB per machine-day, some holding age
keys. All 68 now go through a single `tmp()` helper allocating inside a
per-run root that vitest's globalSetup teardown removes wholesale, and a
class-guard test fails if `mkdtempSync` appears under test/ outside the
helpers.

The per-worker `process.once("exit")` reaper that suggests itself here does
not work under vitest and fails silently: the pool recycles workers by
killing them, so exit handlers registered in a test file never run. Measured
— a probe test writing from an exit hook produced no file, and a full run
with per-worker hooks still left 750 directories. globalSetup's teardown runs
in the main process, after every worker, and vitest awaits it.

Separately, and contrary to #117's framing that "cast itself does not leak":
resolveCheckout() mkdtemps an `infra-checkout-` dir, clones the infra repo
into it, and never removes it, so every `cast apply`/`diff`/`capture` without
--path leaked a full clone. The box that reported #117 was holding 602 such
directories, 73MB of real .git trees, from the same day. The leak fires on
the failure path too, since the dir is created before the clone runs.
Ephemeral checkouts are now reaped on process exit — the lifetime that fits,
since callers read the tree after resolveCheckout returns; a --path checkout
is the operator's own tree and is never registered.

Empirical: /tmp/cast-* + /tmp/infra-* count is 0 before and 0 after a full
`npm test`, against 750 with the exit-hook design. 626 tests green.

Refs #117
2026-07-21 12:58:14 +00:00
dan-claude-bot
ced5c497c0 feat: release flow — tagged releases with a prebuilt dist asset (#96)
The cast half of the flow designed in heavy-duty/box#83, aligned with
box#90 and rig#40, plus the piece unique to cast: a prebuilt release
asset, because cast is the one repo where the source tarball is not the
package.

- CHANGELOG.md (box's format) with this PR's entry under Unreleased;
  feature PRs land their entry as part of the PR.
- `cast --version` / `-V` answers with package.json's version, read
  relative to the compiled module so a source checkout and an installed
  prebuilt tree agree.
- release.yml, on EVERY tag push (no shape filter — a mismatched tag
  must fail the assert loudly, not be pattern-skipped): asserts tag ==
  package.json version FIRST, extracts that version's changelog section
  (.github/scripts/release-notes.sh, shared with the tests; missing or
  empty refuses), 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`. No tests here — ci.yml gated the merge commit, and the
  suite needs age.
- install.sh grows the three channels: default = the latest release's
  asset (tag resolved off the releases/latest redirect Location — no
  API, no token; failure dies loudly naming CAST_REF=main, never a
  silent fallback), CAST_REF=<tag> = pinned (asset first, source
  fallback), 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.
- test/release.test.ts drives it all offline: --version, the extraction
  against fixtures (0.7.0 never matches 0.7.0-rc1) and the real
  changelog, and REAL install.sh runs through all three channels with a
  stub curl and a poisoned npm — including the loud no-releases refusal
  with no $DEST side effects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:27:55 +00:00
dan-claude-bot
199cf6ecaf fix: the flip and the uninstall must run on macOS — rename(2) via node, no mapfile
claude-bot's round-2 catch: cast is the sibling that runs on the
operator's own machine, and the layout port carried two Linux
assumptions in with it.

- The atomic current flip spelled 'replace, don't descend' the GNU way
  (mv -Tf); BSD/macOS mv has no -T and dies. The flip now rides node's
  fs.renameSync — rename(2) is POSIX, node is a cast prerequisite on
  every platform — as one flip_current(), byte-identical in install.sh
  and bin/cast, added to the anti-drift diff.
- cmd_uninstall's de-dup used mapfile — bash 4, and macOS ships bash
  3.2. Now a portable while-read append.
- readlink -f: Apple's readlink grew -f in macOS 12.3 (March 2022); the
  installer now probes it once among the prerequisites and refuses
  loudly on older systems instead of failing weirdly mid-flip.
- A portability test pins both spellings out of the two scripts, so a
  reintroduction fails in CI, not on the first operator Mac.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:39:29 +00:00
dan-claude-bot
5cd5968cf2 refactor: rescope to versioned installations — the release flow moves out
Maintainer direction: this PR's one goal is the versioned layout, the same
one box#79 built and rig#36 ported — the release flow (tags, release.yml,
prebuilt assets, CHANGELOG) is its own PR later, the shape rig#40 has.

So: release.yml, changelog-section.sh, CHANGELOG.md and the asset-aware
installer channels leave this branch, and in their place cast gets the
family layout for real:

- install.sh lands each build at $DEST/versions/<package.json version>,
  'current' names the default (atomic rename flips), $BINDIR/cast points
  through it. Converging no-op on an installed version (nothing rebuilt),
  CAST_REINSTALL=1 replaces, a new version installs beside and becomes
  default. Pre-versioning flat installs migrate in place, bit for bit.
  CAST_INSTALL_SOURCE=<dir|tarball> installs locally (CI/tests, rig's
  RIG_INSTALL_SOURCE precedent). No flip gate: box refuses under live
  boxes, rig warns on a converged host — cast is an API client, a flip
  strands nothing, 'cast use <old>' is one command away.
- bin/cast grows the layout verbs in bash (they must work when dist/ is
  broken): versions (marks current+running), use (atomic flip, then
  asserts the chain ANSWERS the new version), uninstall (consent gate,
  CURRENT guard, dangling-current guard, ends with the absence assert).
  valid_version/pkg_version are byte-identical copies in both files; a
  test diffs them so the gates cannot drift.
- cast --version stays: package.json is the single source of truth,
  printed with the install root, rig-style.
- ci.yml gains the install job: the real installer, from this checkout,
  layout asserted, converge no-op asserted, uninstall --all asserted
  absent — the box CI precedent.
- Tests drive the REAL install.sh and bin/cast offline (npm shim, local
  source): the layout, the chain answering end to end, no-op/reinstall/
  side-by-side/migration semantics, the hostile-version gates, refs/heads
  download, every uninstall refusal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:17:59 +00:00
dan-claude-bot
d992f1833d feat: versioned installs — tagged releases with a prebuilt dist asset (#96)
cast gets the family's release flow (box#83's shape), plus the piece
unique to cast: because cast compiles, the source tarball is not the
package — so release.yml builds ONCE in CI and attaches cast-X.Y.Z.tgz,
and the installer's default channel extracts that asset instead of
running npm ci + tsc on the operator's machine.

- cast --version: package.json is the single source of truth (no VERSION
  file); prints the install root too, rig-style.
- CHANGELOG.md with Unreleased; release notes are the curated section
  (scripts/changelog-section.sh), never the auto-generated PR list.
- release.yml on a bare X.Y.Z tag: assert tag == package.json version,
  check + build + test, prune, tar the runnable tree, gh release create.
- install.sh channels: unset → latest release asset (resolved via the
  releases/latest redirect — no API, no token); CAST_REF=X.Y.Z → that
  tag's asset; CAST_REF=<branch> → build-from-source, the old path.
- Tests drive the REAL install.sh offline via curl/npm PATH shims (all
  three channels, plus the broken-asset and no-release refusals), and
  the real changelog-section.sh against fixture changelogs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:51:15 +00:00