envName.toUpperCase() alone turned env 'drill-b' into CAST_AGE_KEY_FILE_DRILL-B — a variable no POSIX shell can export, so the injected-key channel (and its process-substitution trick) was unreachable for every hyphenated environment name, while the refusal advertised it anyway. Characters outside [A-Z0-9] now map to _; the standing-key path keeps the exact env name, so names that collide on the variable still resolve their own keys on disk. Fixes #102 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.2 KiB
5.2 KiB
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
Fixed
applyno 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 onno 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.applynow 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.- A manifest with no
${…}refs applies without a store (#104) — the greenfield manifest-first bootstrap was a chicken-and-egg with no exit, found by the 2026-07-19 release drill against two fresh Coolify 4.1.2 instances: a registered project whose manifest declared databases only (zero${…}refs) could not take its firstapply— apply refused withno secret store for <org>/<repo> in <env>, andcapture, the documented way to get a store, rightly refuses a project that is absent on the box, because apply is the verb that would create it. The drill unblocked with a hand-rolled empty store (printf '' | age -r … -o secrets/….env.age), documented nowhere. Nowdiff/applygate that refusal on the manifest actually referencing a secret, asked via the same parser resolution uses: when the templates resolve zero${…}refs, an absent store is treated as empty and the run proceeds, printing a loud one-line note naming the path the store would live at — and since there is nothing to decrypt, the age key is not demanded either. The moment any template gains a${…}ref, the refusal returns byte-identical to before.captureanddestroyare untouched. CAST_AGE_KEY_FILE_<ENV>is now settable for every environment name (#102) —<ENV>was the name uppercased verbatim, so envdrill-badvertisedCAST_AGE_KEY_FILE_DRILL-B: a variable no POSIX shell can export, which walled off the injected-key channel (and its process-substitution trick) for every hyphenated environment. Found live in the 2026-07-19 release drill. Characters outside[A-Z0-9]now map to_— envdrill-breadsCAST_AGE_KEY_FILE_DRILL_B— and the refusal advertises the mapped name. The standing-key path keeps the exact environment name, so two names that collide on the variable still resolve their own keys on disk.
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.ZPR bumpspackage.json(andpackage-lock.json) and stamps this file's Unreleased section with version + date; the merge commit is tagged bareX.Y.Z(box's tag scheme — novprefix).release.ymlturns the tag into the GitHub release — after asserting tag ==package.jsonversion (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.shthe test harness drives, and with the runnable tree attached ascast-X.Y.Z.tgz:bin/, compileddist/, productionnode_modules/,package.json, built once in CI (npm ci && npm run build && npm prune --omit=dev).install.shnow defaults to the latest release: the tag is resolved by following thereleases/latestredirect and reading theLocationheader — no API, no token — and the download is that release's asset, so nonpm ci, notsc, no devDependencies ever run on the operator's machine.CAST_REFpicks the other two channels: a tag pins a release (its asset first, source as the fallback for a ref that has none —refs/tagsoutranks 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 placenpmis required. Until 0.1.0 is cut the default channel has nothing to resolve and dies saying exactly that, namingCAST_REF=mainas 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>,currentflipped atomically) like any other install.