feat: cast — the Coolify executor, extracted from the infra state repo #1

Merged
dan-claude-bot merged 1 commit from feat/executor-extraction into main 2026-07-11 12:35:50 +00:00
dan-claude-bot commented 2026-07-11 12:34:23 +00:00 (Migrated from github.com)

Extracts the Coolify executor out of heavy-duty/infra — which was half tool, half state — into its own public, stateless CLI.

rig builds the boxes. cast fills them. infra is what they're filled with.

Why

infra carried both a TypeScript executor and the private instance state (bindings, age secrets, tailnet ACL, runbooks). That mix is why nobody could say whether "infra" named a CLI or a runbook — and it meant the tool could never be public. This restores the same tool/state split rig already has.

What changed beyond the move

Two things were required to make it genuinely stateless:

  • The cwd contract is gone. environments.yaml, secrets/, and .coolify.env used to resolve against the working directory — the old README's own words were that running it from elsewhere "silently reads the wrong (or no) bindings/secrets file." Now they resolve against an explicit --state <dir> (or $CAST_STATE).

  • BANNED_IN_PRODforbidden_var_patterns. A hardcoded list of one product's ALLOW_* flags was the only product knowledge in the executor. It's now a generic, operator-owned policy in the state repo's environments.yaml:

    environments:
      prod:
        server: prod-box
        forbidden_var_patterns: ["^ALLOW_"]
    

    The guard now lives in private state, so a product-side manifest change cannot lower its own guard — and it's a pattern, not a list, so it catches siblings nobody has invented yet. Semantics are unchanged: presence, not value (ALLOW_SEED=false still refuses the apply).

Age identities resolve as $CAST_AGE_KEY_FILE_<ENV>~/.config/cast/age-<env>.key. That is the attended-vs-unattended apply mechanism, with no environment names known to the tool: an env whose key you never leave on disk can only be applied by someone who injects it.

Publishability

This repo is public, so instance identity had to go: it's out of the test fixtures (claude-hdb/incubator, the founder domains, hdb-coolify-fork) and out of scripts/register-github-app.sh, which now takes APP_NAME and ORG as arguments instead of baking them in.

The executor's hard-won behavior notes — apply semantics, the Coolify 4.1.2 limitations verified against its source — are preserved in docs/semantics.md.

Verification

  • 69/69 tests green, tsc clean, biome clean, CI passing.
  • Driven end-to-end: cast --help; --state and $CAST_STATE both resolve .coolify.env and environments.yaml from the given directory, and a bad state dir names the exact path it looked in.
  • Parses the real (private) state repo's environments.yaml, new guard field included.
  • The guard fires correctly against the real product manifests: prod's template passes; a sneaked-in ALLOW_DB_RESET=false is refused.

Paired with

heavy-duty/infra — "infra is state, not code", which removes the code this extracts. Merge this one first.

Extracts the Coolify executor out of `heavy-duty/infra` — which was half tool, half state — into its own public, stateless CLI. `rig` builds the boxes. `cast` fills them. `infra` is what they're filled with. ## Why `infra` carried both a TypeScript executor *and* the private instance state (bindings, age secrets, tailnet ACL, runbooks). That mix is why nobody could say whether "infra" named a CLI or a runbook — and it meant the tool could never be public. This restores the same tool/state split rig already has. ## What changed beyond the move Two things were required to make it genuinely stateless: - **The cwd contract is gone.** `environments.yaml`, `secrets/`, and `.coolify.env` used to resolve against the working directory — the old README's own words were that running it from elsewhere *"silently reads the wrong (or no) bindings/secrets file."* Now they resolve against an explicit `--state <dir>` (or `$CAST_STATE`). - **`BANNED_IN_PROD` → `forbidden_var_patterns`.** A hardcoded list of one product's `ALLOW_*` flags was the only product knowledge in the executor. It's now a generic, operator-owned policy in the state repo's `environments.yaml`: ```yaml environments: prod: server: prod-box forbidden_var_patterns: ["^ALLOW_"] ``` The guard now lives in **private state**, so a product-side manifest change cannot lower its own guard — and it's a pattern, not a list, so it catches siblings nobody has invented yet. Semantics are unchanged: presence, not value (`ALLOW_SEED=false` still refuses the apply). Age identities resolve as `$CAST_AGE_KEY_FILE_<ENV>` → `~/.config/cast/age-<env>.key`. That *is* the attended-vs-unattended apply mechanism, with no environment names known to the tool: an env whose key you never leave on disk can only be applied by someone who injects it. ## Publishability This repo is public, so instance identity had to go: it's out of the test fixtures (`claude-hdb/incubator`, the founder domains, `hdb-coolify-fork`) and out of `scripts/register-github-app.sh`, which now takes `APP_NAME` and `ORG` as arguments instead of baking them in. The executor's hard-won behavior notes — apply semantics, the Coolify 4.1.2 limitations verified against its source — are preserved in `docs/semantics.md`. ## Verification - 69/69 tests green, `tsc` clean, biome clean, CI passing. - Driven end-to-end: `cast --help`; `--state` and `$CAST_STATE` both resolve `.coolify.env` and `environments.yaml` from the given directory, and a bad state dir names the exact path it looked in. - Parses the real (private) state repo's `environments.yaml`, new guard field included. - The guard fires correctly against the real product manifests: prod's template passes; a sneaked-in `ALLOW_DB_RESET=false` is refused. ## Paired with `heavy-duty/infra` — "infra is state, not code", which removes the code this extracts. Merge this one first.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#1
No description provided.