-
0.2.0 Stable
released this
2026-07-21 18:06:29 +00:00 | 7 commits to main since this releaseAdded
- CI refuses a release PR with no drill record at
drills/<version>.md - An application can declare HTTP basic auth, and
applysets it (#76) cast github-app create/cast github-app registerrun the App Manifest
flow (#7)
Changed
state:needs-humanis set at handoff, not by the cron (#131)- PR labels split into two axes:
state:*(whose ball) andblocker:*(what
is in the way);state:needs-rebaseis retired (heavy-duty/box#138) - The
NO_API_COVERAGErow for Basic Auth now says services (#76) - Changelog entries are one line each, and the whole file now follows the rule
(#136)
Removed
- BREAKING:
scripts/register-github-app.shis gone; usecast github-app register(#7)
Fixed
- Three test files from #124/#125 allocate through
tmp(), not raw
mkdtempSync(#135) - A PR that deletes a shipped release heading is now CI-red (#133,
heavy-duty/box#122) - A duplicate release heading is caught even where the guard cannot see the
base (#133, heavy-duty/box#143) - A label the repo does not have no longer takes the whole label edit down
with it - An unreadable check rollup no longer reads as "nothing is failing"
state:needs-humanno longer appears on PRs a human cannot merge (#127,
heavy-duty/box#136)- CI lints every tracked shell script, and proves the set is complete (#118)
- The shellcheck sweep covers extensionless scripts such as
bin/cast(#121) castno longer leaves a full repo clone in the temp dir on every run (#117)- The test suite reaps its temp directories (#117)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
cast-0.2.0.tgz
2 downloads · 886 KiB
- CI refuses a release PR with no drill record at
-
0.1.1 Stable
released this
2026-07-19 23:03:50 +00:00 | 58 commits to main since this releaseFixed
- The release ceremony re-arms the changelog, and CI notices when it
doesn't (#113) — stamping## Unreleasedinto## X.Y.Z — DATEis
done by hand in the ceremony PR; no workflow writes this file, and
nothing put the heading back. Somainsat with the shipped section on
top and no## Unreleasedabove it — this repo's state from 0.1.0
until this entry. A PR authored before a release and merged after has
its entry land under whatever heading now occupies that position: the
release that already shipped. Git does that cleanly. The stamped
heading and the incoming entry never overlap textually, so the one
signal an author trusts — "git told me to look" — is missing exactly
when the result is wrong. rig watched it happen (heavy-duty/rig#66, the
origin of this fix): an entry landed inside published## 0.1.0an
hour after 0.1.0 shipped, and was caught only because someone was
reading. The published release body is never at risk —release.yml
extracts notes from the tree at the tag, before anything late can merge
— which is also why nobody notices: the file that drifts is the one
only maintainers read. Three moves.## Unreleasedis back above
## 0.1.0(this entry re-creating it is the repair). CONTRIBUTING's
ceremony step now re-arms in the same diff that stamps. And
test/release.test.tskeys the rule topackage.json: a stamped top
section is legal while the version is bare — the ceremony's own tree,
and main until the-devbump — but once the version says-dev, the
top section must be## Unreleased. That is the distinction #108 had
to collapse to make the ceremony shippable at all, recovered rather
than reverted: the ceremony stays green at every step, and a disarmed
devmaingoes red. The re-arm also forced the older extraction guard
to move. It asserted that the top section extracts non-empty, which
the re-armed ceremony tree — a deliberately empty## Unreleasedabove
the stamp — makes false by construction: the re-arm and the guard would
have contradicted each other, and the next release PR would have been
unshippable for a second time, the way #108 was. Keying to the top
section was only ever a stand-in for "the sectionrelease.ymlwill
publish", so the assert now names that section directly — on a bare
version the## X.Y.Zbeing shipped, on a-devtree the newest
stamped one. Existence is checked with it: a bare version with no
matching section is a bump that never stamped, which used to pass every
test and fail only after the merge, inrelease.yml's notes step,
past the ship decision and leavingmainwith a minted, unreleased
version to repair by hand. A double re-arm — two## Unreleased
headings, the extracted section silently the empty one — is red too.
box and rig carry the same fix (heavy-duty/box#110,
heavy-duty/rig#67); rig#67 retargeted the identical assert for the
identical reason.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
cast-0.1.1.tgz
0 downloads · 860 KiB
- The release ceremony re-arms the changelog, and CI notices when it
-
0.1.0 Stable
released this
2026-07-19 18:02:54 +00:00 | 65 commits to main since this releaseFixed
-
The release suite accepts the ceremony's own tree (#108) —
test/release.test.tsdemanded the realCHANGELOG.md's literal
Unreleasedsection extract non-empty and contain#96: false by
construction on therelease: X.Y.Ztree the ceremony's own PR produces
(it stamps that heading into## X.Y.Z — date), so the first real
release PR turned CI red and the flow blocked itself — invisible to the
fork rehearsals, which tag a branch (release.ymlruns;ci.ymlnever
does). The guard now asserts its actual purpose: whatever the TOP##
section is —Unreleasedbetween releases, the stamped version on and
right after one — the exactrelease-notes.shthe workflow runs
extracts it non-empty. rig's twin is heavy-duty/rig#44. -
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 with
no 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-b
advertisedCAST_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
-
Merging a release-labeled PR is the release — and the release re-arms
main itself (#111; box#96's design) —release.ymlnow also fires on
pushes to main (notpull_requestevents: fork-sourced ceremony PRs get
a read-only token there — the round-1 catch). A decide step reads the
version transition from the push (event.before→ the pushed head) and
answers four states: release-flow work merged under therelease
label —-devendstates, and the post-release window — no-ops green
with a NOTICE; the two genuinely ambiguous bare states refuse loudly;
a true transition then requires a merged,release-labeled PR behind
the commit (read via the API — the label is the operator's declared
intent) before the door opens. It then tags the merge commit, builds
thecast-X.Y.Z.tgzasset once, publishes — and bumps main to
X.Y.(Z+1)-devitself, direct push with a loud open-a-PR fallback, so
no follow-up bump PR exists on the paved road. The tag-push path stays
as the documented fallback and backfill, and both paths run the same
steps so they cannot drift. First-release edge: 0.1.0 never carried
-dev, so its ceremony (#110) ships by manual tag; the automation
applies from 0.1.1 on. -
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: therelease: X.Y.ZPR bumps
package.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 the
releases/latestredirect and reading theLocationheader — no API, no
token — and the download is that release's asset, so nonpm 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/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 place
npmis 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.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
cast-0.1.0.tgz
0 downloads · 859 KiB
-