fix: apply creates the environment its resources name (#38) #39

Merged
dan-claude-bot merged 1 commit from fix/ensure-environment into main 2026-07-14 16:44:29 +00:00
dan-claude-bot commented 2026-07-14 16:43:37 +00:00 (Migrated from github.com)

Fixes #38.

The bug

POST /projects hands a newly created project Coolify's own default environment, production — never ours. cast then created every resource with environment_name: <our --env>, so the first apply against a project that did not exist yet 404'd on its first resource and left the project behind, created and empty:

POST /applications/private-github-app → 404: {"message":"Environment not found."}

The route to create an environment exists in the vendored 4.1.2 spec (POST /projects/{uuid}/environments); cast simply never called it. Two comments in the source (cli.ts:716, :808) and the README already asserted the behaviour as though it were implemented — this makes them true.

It went unseen because every environment cast had touched until now was hand-built in a UI and then adopted, so it always already existed under whatever name someone typed (which is why --environment exists at all). The genuinely-from-nothing apply is the one path that had never been run.

The fix

apply reconciles project + environment once per run, lazily, before the first create — memoized, so five creates reconcile once rather than five times.

Read-before-write, which is the property that matters for blast radius: an environment that already exists takes the read and stops, and the create route is never called at all. Every environment cast has ever touched is in that case, so adoption is untouched and this cannot regress an apply that works today. A 409 is read as "present" — the race between our read and our write.

The decision the issue left open

The issue asked whether to delete the empty production Coolify leaves behind. Left alone, and reported instead:

note: new project widget carries Coolify's default environment(s): production — empty, unused, and cast never removes (delete by hand if unwanted)

Deleting it would be the first delete cast ever performs, against a guarantee the tool states in docs/semantics.md and enforces everywhere else — an orphan is reported and not repaired, "removal is a manual runbook act". An empty environment beside the one everything lives in is the mildest possible case of exactly that rule, so it gets the same courtesy an orphan gets: named, and left to the operator.

Tests

The regression test drives the real failure, not a call count — the fake Coolify 404s a create whose environment_name it does not carry, exactly as a live box does. Against the old executor it reproduces the reported error verbatim:

× creates the environment on a project it just created, and the create names it
  → POST /applications/private-github-app → 404: {"message":"Environment not found."}

Covered: from-nothing create · already-exists (never writes) · 409-as-present · non-409 surfaces · reconciles-once across several creates. Suite is 284 passing (279 before); npm run check, build, test all green.

Three existing wire.test.ts mocks gained the environments route — they modelled an existing project but did not serve the route a real Coolify serves for one.

Not verified against a live Coolify

Per D-208 an agent does not run cast, so this is verified against the vendored 4.1.2 spec and a fake that enforces Coolify's rule. The operator's first apply is the real test — and it is the exact path (prod-migration Task 8 step 4) that the workaround in the issue currently unblocks.

🤖 Generated with Claude Code

Fixes #38. ## The bug `POST /projects` hands a newly created project Coolify's **own** default environment, `production` — never ours. cast then created every resource with `environment_name: <our --env>`, so the first `apply` against a project that did not exist yet 404'd on its first resource and left the project behind, created and empty: ``` POST /applications/private-github-app → 404: {"message":"Environment not found."} ``` The route to create an environment exists in the vendored 4.1.2 spec (`POST /projects/{uuid}/environments`); cast simply never called it. Two comments in the source (`cli.ts:716`, `:808`) and the README already asserted the behaviour as though it were implemented — this makes them true. It went unseen because every environment cast had touched until now was hand-built in a UI and then adopted, so it always already existed under whatever name someone typed (which is why `--environment` exists at all). The genuinely-from-nothing apply is the one path that had never been run. ## The fix `apply` reconciles project + environment once per run, lazily, before the first create — memoized, so five creates reconcile once rather than five times. **Read-before-write**, which is the property that matters for blast radius: an environment that already exists takes the read and stops, and the create route is *never called at all*. Every environment cast has ever touched is in that case, so adoption is untouched and this **cannot regress an apply that works today**. A `409` is read as "present" — the race between our read and our write. ## The decision the issue left open The issue asked whether to delete the empty `production` Coolify leaves behind. **Left alone**, and reported instead: > `note: new project widget carries Coolify's default environment(s): production — empty, unused, and cast never removes (delete by hand if unwanted)` Deleting it would be the first delete cast ever performs, against a guarantee the tool states in `docs/semantics.md` and enforces everywhere else — an orphan is *reported and not repaired*, "removal is a manual runbook act". An empty environment beside the one everything lives in is the mildest possible case of exactly that rule, so it gets the same courtesy an orphan gets: named, and left to the operator. ## Tests The regression test drives the **real failure**, not a call count — the fake Coolify 404s a create whose `environment_name` it does not carry, exactly as a live box does. Against the old executor it reproduces the reported error verbatim: ``` × creates the environment on a project it just created, and the create names it → POST /applications/private-github-app → 404: {"message":"Environment not found."} ``` Covered: from-nothing create · already-exists (never writes) · 409-as-present · non-409 surfaces · reconciles-once across several creates. Suite is 284 passing (279 before); `npm run check`, `build`, `test` all green. Three existing `wire.test.ts` mocks gained the environments route — they modelled an *existing* project but did not serve the route a real Coolify serves for one. ## Not verified against a live Coolify Per D-208 an agent does not run `cast`, so this is verified against the vendored 4.1.2 spec and a fake that enforces Coolify's rule. **The operator's first apply is the real test** — and it is the exact path (prod-migration Task 8 step 4) that the workaround in the issue currently unblocks. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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#39
No description provided.