First apply against a fresh project 404s — cast never creates the Coolify environment #38

Closed
opened 2026-07-14 16:23:25 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-14 16:23:25 +00:00 (Migrated from github.com)

What happens

The first cast apply against a project that does not yet exist creates the project, then fails on the first resource create:

team id=0 name="Root Team" ✓
create application core
  …
5 change(s), 0 orphan(s)
POST /applications/private-github-app → 404: {"message":"Environment not found."}

The project is left created and empty. Every subsequent create in the same run would fail the same way.

Root cause

ensureProject creates the project with the name only (src/cli.ts:1679):

const p = (await client.post("/projects", { name })) as { uuid: string };

Coolify gives a newly created project its own default environment, named production. cast then sends every resource create with environment_name: coolifyEnv (src/cli.ts:1819, :1841, :1866), where:

const coolifyEnv = ctx.environmentOverride ?? ctx.envName;   // src/cli.ts:716

With --env prod and no --environment, that is prod — an environment which does not exist on the fresh project. Hence the 404.

Two comments in the source assert the missing behaviour as though it were implemented:

  • src/cli.ts:716"since apply creates the environment from this value"
  • src/cli.ts:808"The name the environment gets ON COOLIFY when apply creates it"

cast never creates an environment. There is no call to the route that would do it, and that route does exist in the vendored 4.1.2 spec (reference/coolify-openapi-4.1.2.json):

POST /projects/{uuid}/environments      body: { "name": string }   → 201
GET  /projects/{uuid}/environments

Why it has gone unnoticed

Every environment cast has touched so far was created by hand in a UI and then adopted — so the environment always already existed, under whatever name someone typed (which is exactly why --environment was added). The genuinely-from-nothing first apply is the one path that had never been exercised.

Suggested fix

In ensureProject (or immediately after it), reconcile the environment before any resource create:

  1. GET /projects/{uuid} (or /projects/{uuid}/environments) → list existing environment names.
  2. If coolifyEnv is absent, POST /projects/{uuid}/environments { name: coolifyEnv }.
  3. Proceed.

Idempotent, so it is safe on every apply, not just the first — and it makes the two comments above true.

Worth deciding alongside: a brand-new project is now left carrying an empty production environment beside ours. That is precisely the shape that makes an adopted box confusing to read later (an empty production sitting next to the environment everything actually lives in). Options are to leave it, or to delete it when cast created the project itself and the default is empty — DELETE /projects/{uuid}/environments/{name} exists.

Workaround

Create the environment by hand once, then re-run apply unchanged:

curl -s -X POST -H "Authorization: Bearer $COOLIFY_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' -d '{"name":"prod"}' \
  "$COOLIFY_BASE_URL/api/v1/projects/<project-uuid>/environments"

Passing --environment production also gets past it, but adopts Coolify's default name permanently and means carrying the flag on every subsequent verb for that environment — the trade-off --environment explicitly exists to avoid making by accident.

## What happens The first `cast apply` against a project that does not yet exist creates the project, then fails on the first resource create: ``` team id=0 name="Root Team" ✓ create application core … 5 change(s), 0 orphan(s) POST /applications/private-github-app → 404: {"message":"Environment not found."} ``` The project is left created and empty. Every subsequent create in the same run would fail the same way. ## Root cause `ensureProject` creates the project with the name only (`src/cli.ts:1679`): ```ts const p = (await client.post("/projects", { name })) as { uuid: string }; ``` Coolify gives a newly created project **its own** default environment, named **`production`**. cast then sends every resource create with `environment_name: coolifyEnv` (`src/cli.ts:1819`, `:1841`, `:1866`), where: ```ts const coolifyEnv = ctx.environmentOverride ?? ctx.envName; // src/cli.ts:716 ``` With `--env prod` and no `--environment`, that is `prod` — an environment which does not exist on the fresh project. Hence the 404. Two comments in the source assert the missing behaviour as though it were implemented: - `src/cli.ts:716` — *"since apply creates the environment from this value"* - `src/cli.ts:808` — *"The name the environment gets ON COOLIFY when apply creates it"* cast never creates an environment. There is no call to the route that would do it, and that route **does exist** in the vendored 4.1.2 spec (`reference/coolify-openapi-4.1.2.json`): ``` POST /projects/{uuid}/environments body: { "name": string } → 201 GET /projects/{uuid}/environments ``` ## Why it has gone unnoticed Every environment cast has touched so far was created by hand in a UI and then adopted — so the environment always already existed, under whatever name someone typed (which is exactly why `--environment` was added). The genuinely-from-nothing first apply is the one path that had never been exercised. ## Suggested fix In `ensureProject` (or immediately after it), reconcile the environment before any resource create: 1. `GET /projects/{uuid}` (or `/projects/{uuid}/environments`) → list existing environment names. 2. If `coolifyEnv` is absent, `POST /projects/{uuid}/environments { name: coolifyEnv }`. 3. Proceed. Idempotent, so it is safe on every apply, not just the first — and it makes the two comments above true. Worth deciding alongside: a brand-new project is now left carrying an **empty `production`** environment beside ours. That is precisely the shape that makes an adopted box confusing to read later (an empty `production` sitting next to the environment everything actually lives in). Options are to leave it, or to delete it when cast created the project itself and the default is empty — `DELETE /projects/{uuid}/environments/{name}` exists. ## Workaround Create the environment by hand once, then re-run apply unchanged: ```sh curl -s -X POST -H "Authorization: Bearer $COOLIFY_ACCESS_TOKEN" \ -H 'Content-Type: application/json' -d '{"name":"prod"}' \ "$COOLIFY_BASE_URL/api/v1/projects/<project-uuid>/environments" ``` Passing `--environment production` also gets past it, but adopts Coolify's default name permanently and means carrying the flag on every subsequent verb for that environment — the trade-off `--environment` explicitly exists to avoid making by accident.
Sign in to join this conversation.
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#38
No description provided.