• 0.2.0 38ea73eb55

    0.2.0 Stable

    github-actions[bot] released this 2026-07-21 18:06:29 +00:00 | 7 commits to main since this release

    Added

    • CI refuses a release PR with no drill record at drills/<version>.md
    • An application can declare HTTP basic auth, and apply sets it (#76)
    • cast github-app create / cast github-app register run the App Manifest
      flow (#7)

    Changed

    • state:needs-human is set at handoff, not by the cron (#131)
    • PR labels split into two axes: state:* (whose ball) and blocker:* (what
      is in the way); state:needs-rebase is retired (heavy-duty/box#138)
    • The NO_API_COVERAGE row 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.sh is gone; use cast 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-human no 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)
    • cast no longer leaves a full repo clone in the temp dir on every run (#117)
    • The test suite reaps its temp directories (#117)
    Downloads
  • 0.1.1 2f30105b6e

    0.1.1 Stable

    github-actions[bot] released this 2026-07-19 23:03:50 +00:00 | 58 commits to main since this release

    Fixed

    • The release ceremony re-arms the changelog, and CI notices when it
      doesn't
      (#113) — stamping ## Unreleased into ## X.Y.Z — DATE is
      done by hand in the ceremony PR; no workflow writes this file, and
      nothing put the heading back. So main sat with the shipped section on
      top and no ## Unreleased above 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.0 an
      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. ## Unreleased is 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.ts keys the rule to package.json: a stamped top
      section is legal while the version is bare — the ceremony's own tree,
      and main until the -dev bump — 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
      dev main goes 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 ## Unreleased above
      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 section release.yml will
      publish", so the assert now names that section directly — on a bare
      version the ## X.Y.Z being shipped, on a -dev tree 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, in release.yml's notes step,
      past the ship decision and leaving main with 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
  • 0.1.0 f098a27377

    0.1.0 Stable

    github-actions[bot] released this 2026-07-19 18:02:54 +00:00 | 65 commits to main since this release

    Fixed

    • The release suite accepts the ceremony's own tree (#108) —
      test/release.test.ts demanded the real CHANGELOG.md's literal
      Unreleased section extract non-empty and contain #96: false by
      construction on the release: X.Y.Z tree 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.yml runs; ci.yml never
      does). The guard now asserts its actual purpose: whatever the TOP ##
      section is — Unreleased between releases, the stamped version on and
      right after one — the exact release-notes.sh the workflow runs
      extracts it non-empty. rig's twin is heavy-duty/rig#44.

    • 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.

    • 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 first apply — apply refused with
      no secret store for <org>/<repo> in <env>, and capture, 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. Now diff/apply gate 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.
      capture and destroy are untouched.

    • CAST_AGE_KEY_FILE_<ENV> is now settable for every environment name
      (#102) — <ENV> was the name uppercased verbatim, so env drill-b
      advertised CAST_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
      _ — env drill-b reads CAST_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.yml now also fires on
      pushes to main (not pull_request events: 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 the release
      label — -dev endstates, 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
      the cast-X.Y.Z.tgz asset once, publishes — and bumps main to
      X.Y.(Z+1)-dev itself, 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: 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.

    Downloads