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 21:29:53 +00:00
|
|
|
# Changelog
|
|
|
|
|
|
|
|
|
|
History before 0.1.0 lives in git — cast has said `0.1.0` in `package.json`
|
|
|
|
|
since its first commit, but grew its release surface (this file,
|
|
|
|
|
`cast --version`, tagged releases with a prebuilt asset) on the way to
|
|
|
|
|
actually cutting it, and this file starts there.
|
|
|
|
|
|
|
|
|
|
## Unreleased
|
|
|
|
|
|
2026-07-19 12:26:35 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
|
|
|
|
- **`apply` no longer demands a GitHub App for a manifest that declares no
|
|
|
|
|
applications** (#103) — found live in the 2026-07-19 release drill, where a
|
|
|
|
|
databases-only manifest (`applications: {}`) rendered its plan of two
|
|
|
|
|
creates and then died in preflight on `no GitHub App bound`, over a binding
|
|
|
|
|
nothing in the run would ever have used: a GitHub App exists to clone
|
|
|
|
|
application source, cast reads it in exactly one call (the application
|
|
|
|
|
create), and databases and services never touch it. That unconditional
|
|
|
|
|
resolution gated infra-only projects — the databases a fleet's other
|
|
|
|
|
projects share — behind the GitHub-App browser-registration ceremony for no
|
|
|
|
|
reason. `apply` now resolves the App only when the desired state actually
|
|
|
|
|
contains an application; a manifest that does declare one still refuses on
|
|
|
|
|
a missing binding exactly as before, clean plan or not, because that
|
|
|
|
|
binding is state the next create will need.
|
|
|
|
|
|
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 21:29:53 +00:00
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
- **Tagged releases with a prebuilt dist asset, and an installer that
|
|
|
|
|
installs them** (#96) — the cast half of the flow designed in
|
|
|
|
|
heavy-duty/box#83, plus the piece unique to cast: a **prebuilt asset**,
|
|
|
|
|
because cast is the one repo where the source tarball is *not* the
|
|
|
|
|
package. A release is a PR, then a tag: the `release: X.Y.Z` PR bumps
|
|
|
|
|
`package.json` (and `package-lock.json`) 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 == `package.json` version (a
|
|
|
|
|
mismatch fails loudly and creates nothing) — with that version's section
|
|
|
|
|
of this file as the body, extracted by the same
|
|
|
|
|
`.github/scripts/release-notes.sh` the test harness drives, and with the
|
|
|
|
|
runnable tree attached as `cast-X.Y.Z.tgz`: `bin/`, compiled `dist/`,
|
|
|
|
|
production `node_modules/`, `package.json`, built once in CI
|
|
|
|
|
(`npm ci && npm run build && npm prune --omit=dev`). `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 that release's asset, so **no `npm ci`, no
|
|
|
|
|
`tsc`, no devDependencies ever run on the operator's machine**. `CAST_REF`
|
|
|
|
|
picks the other two channels: a tag pins a release (its asset first,
|
|
|
|
|
source as the fallback for a ref that has none — `refs/tags` outranks a
|
|
|
|
|
same-named branch), a branch (`CAST_REF=main`) tracks the development
|
|
|
|
|
tree and is the one channel that still builds from source, the only place
|
|
|
|
|
`npm` is required. Until 0.1.0 is cut the default channel has nothing to
|
|
|
|
|
resolve and dies saying exactly that, naming `CAST_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". The channel only decides *which* tree arrives and
|
|
|
|
|
whether it is built here — whatever it fetched lands in the versioned
|
|
|
|
|
layout (`versions/<package.json version>`, `current` flipped atomically)
|
|
|
|
|
like any other install.
|