fix(apply): create databases and services before the applications that need them (#45) #53

Merged
dan-claude-bot merged 1 commit from fix/create-order into main 2026-07-14 22:42:46 +00:00
dan-claude-bot commented 2026-07-14 22:23:01 +00:00 (Migrated from github.com)

The problem

applyPlan walks report.changes in the order the report was built, and that order is the manifest's: desiredFromManifest (resolve.ts) pushes applications (~347) → databases (~373) → services (~385), and computeDiff preserves it faithfully.

So a first apply creates the compose app and deploys it (createResourcesyncEnvredeploy, all inside one loop iteration) before the Postgres and Redis it talks to exist at all. A full build, a full deploy, and a red deployment in the UI — guaranteed, on every first apply. A tool whose first run always looks broken teaches people to ignore its output.

What changed

src/apply.ts only. Apply now acts in dependency orderdatabaseserviceapplication — instead of manifest order.

  • KIND_ORDER is exported as the forward kind-order. It cannot be a computed graph, and the issue is right about why: nothing in a manifest declares that core needs postgres — no resource names another, anywhere — so the dependency edges do not exist to be walked. The direction between kinds is not in question, and three kinds is few enough to legislate.
  • It is ranked as a Record<ResourceKind, number> rather than a list + indexOf, so a fourth ResourceKind fails the build until someone decides where it goes. A list would rank an unranked kind -1 — i.e. ahead of databases — silently reintroducing this exact bug for the new kind. KIND_ORDER is derived from the ranks so the two cannot drift.
  • Creates and updates, not just creates (a small extension of the issue's prescription): a redeploy is a redeploy, and an application restarted against a database whose own pending change has not landed yet is the same failure, one apply later. The dependency direction is identical for both ops, so one order covers both.
  • The sort is on a copy, never in place: report.changes is what renderDiff prints and what a fleet run reports on, and that reading order is the manifest's, deliberately. The sort is stable (ES2019), so within a kind the manifest's order survives.
  • Only when apply acts changed. clean / orphans / placement and the two refusals are untouched — src/diff.ts is not modified.

Where the issue was slightly optimistic, and the honest limit

The issue reasons as though existence is the whole problem ("the database ... was not going to exist for another few seconds' worth of API calls"). Existence is the part cast can fix, and this fixes it — but ordering the API calls is not a readiness barrier, and the PR says so rather than implying a stronger guarantee.

From DeployController@deploy_resource (coollabsio/coolify v4.1.2, the endpoint redeploy hits via client.deploy):

  • Applicationqueue_application_deployment(application: $resource, deployment_uuid: ..., is_api: true) — queued.
  • DatabaseStartDatabase::dispatch($resource) — queued ("Database starting request queued.").
  • ServiceStartService::run($resource) — the only one that runs synchronously.

So cast orders its requests; Coolify runs them on its own queues. An app whose first boot must find a listening database still races it. What this PR removes is the guaranteed failure — deploying against databases that do not exist at all — not the race. A readiness barrier (poll GET /databases/{uuid} until running, before the app deploy) would be a separate change, and a bigger one.

Also confirmed while reading the source, and not acted on because cli.ts is out of scope for this PR: POST /databases/postgresql does accept instant_deploy (DatabasesController $allowedFields, line 1646; documented at 1134+), and cast does not send it — databases are started by the separate redeploy step instead. That works, and changing it is not this PR's business, but it is a simplification available to whoever owns cli.ts next.

Tests

test/apply.test.ts, 5 added (294 → 299, all green under npm run check && npm run build && npm test). Edge paths, not just the happy one:

  1. A first apply from nothing emits create postgres → create redis → create metabase → create core, with core created and deployed last; asserts the report itself still reads in manifest order, and that within a kind manifest order survives (postgres before redis — stable sort).
  2. Updates are ordered too: the apply that adds a Redis and points an existing core at it creates + starts the database before the app's env sync and redeploy.
  3. The refusal still precedes every mutation — a non-updatable build_pack drift on the application refuses the run even though a creatable database now sorts ahead of it. This is the regression the reorder could have introduced (a check folded into the ordered walk would have created postgres first), so it is locked in.
  4. applyPlan does not sort report.changes in place — renderDiff and the fleet summary keep the operator's reading order.
  5. KIND_ORDER is ["database", "service", "application"], and its reverse is the teardown order.

Coordination

  • #43 (cast destroy) needs the exact reverse of KIND_ORDER and defines its own teardown constant in its own new file, per the parallel-PR split. Follow-up: unify the two into one pair of constants once both have landed — the symmetry is the point (things come up in the order their dependencies allow and go down in the reverse), and it should live in one place.
  • src/diff.ts deliberately untouched (owned by other in-flight PRs); the order lives entirely at apply time.

Operator acts

None. No config, no migration — the next cast apply simply acts in the right order.

docs/semantics.md gains a bullet under Apply semantics stating the order, why it is a constant and not a graph, and the queue-vs-readiness limit above.

Closes #45.

🤖 Generated with Claude Code

## The problem `applyPlan` walks `report.changes` in the order the report was built, and that order is the manifest's: `desiredFromManifest` (`resolve.ts`) pushes **applications** (~347) → **databases** (~373) → **services** (~385), and `computeDiff` preserves it faithfully. So a first apply creates the compose app *and deploys it* (`createResource` → `syncEnv` → `redeploy`, all inside one loop iteration) before the Postgres and Redis it talks to exist at all. A full build, a full deploy, and a red deployment in the UI — guaranteed, on every first apply. A tool whose first run always looks broken teaches people to ignore its output. ## What changed `src/apply.ts` only. Apply now acts in **dependency order** — `database` → `service` → `application` — instead of manifest order. - `KIND_ORDER` is exported as the forward kind-order. It cannot be a computed graph, and the issue is right about why: **nothing in a manifest declares that `core` needs `postgres`** — no resource names another, anywhere — so the dependency edges do not exist to be walked. The direction between *kinds* is not in question, and three kinds is few enough to legislate. - It is ranked as a `Record<ResourceKind, number>` rather than a list + `indexOf`, so a **fourth `ResourceKind` fails the build** until someone decides where it goes. A list would rank an unranked kind `-1` — i.e. ahead of databases — silently reintroducing this exact bug for the new kind. `KIND_ORDER` is derived from the ranks so the two cannot drift. - **Creates *and* updates**, not just creates (a small extension of the issue's prescription): a redeploy is a redeploy, and an application restarted against a database whose own pending change has not landed yet is the same failure, one apply later. The dependency direction is identical for both ops, so one order covers both. - The sort is on a **copy**, never in place: `report.changes` is what `renderDiff` prints and what a fleet run reports on, and that reading order is the manifest's, deliberately. The sort is stable (ES2019), so within a kind the manifest's order survives. - Only **when** apply acts changed. `clean` / `orphans` / `placement` and the two refusals are untouched — `src/diff.ts` is not modified. ## Where the issue was slightly optimistic, and the honest limit The issue reasons as though existence is the whole problem ("the database ... was not going to exist for another few seconds' worth of API calls"). Existence *is* the part cast can fix, and this fixes it — but **ordering the API calls is not a readiness barrier**, and the PR says so rather than implying a stronger guarantee. From `DeployController@deploy_resource` (coollabsio/coolify **v4.1.2**, the endpoint `redeploy` hits via `client.deploy`): - **Application** → `queue_application_deployment(application: $resource, deployment_uuid: ..., is_api: true)` — queued. - **Database** → `StartDatabase::dispatch($resource)` — queued (`"Database starting request queued."`). - **Service** → `StartService::run($resource)` — the only one that runs synchronously. So cast orders its **requests**; Coolify runs them on its own queues. An app whose first boot must find a *listening* database still races it. What this PR removes is the guaranteed failure — deploying against databases that do not exist at all — not the race. A readiness barrier (poll `GET /databases/{uuid}` until running, before the app deploy) would be a separate change, and a bigger one. Also confirmed while reading the source, and *not* acted on because `cli.ts` is out of scope for this PR: `POST /databases/postgresql` **does accept `instant_deploy`** (`DatabasesController` `$allowedFields`, line 1646; documented at 1134+), and cast does not send it — databases are started by the separate `redeploy` step instead. That works, and changing it is not this PR's business, but it is a simplification available to whoever owns `cli.ts` next. ## Tests `test/apply.test.ts`, 5 added (294 → **299**, all green under `npm run check && npm run build && npm test`). Edge paths, not just the happy one: 1. A first apply from nothing emits `create postgres → create redis → create metabase → create core`, with `core` created and deployed **last**; asserts the report itself still reads in manifest order, and that within a kind manifest order survives (postgres before redis — stable sort). 2. Updates are ordered too: the apply that adds a Redis and points an existing `core` at it creates + starts the database before the app's env sync and redeploy. 3. **The refusal still precedes every mutation** — a non-updatable `build_pack` drift on the application refuses the run even though a creatable database now sorts *ahead* of it. This is the regression the reorder could have introduced (a check folded into the ordered walk would have created postgres first), so it is locked in. 4. `applyPlan` does not sort `report.changes` in place — `renderDiff` and the fleet summary keep the operator's reading order. 5. `KIND_ORDER` is `["database", "service", "application"]`, and its reverse is the teardown order. ## Coordination - `#43` (`cast destroy`) needs the **exact reverse** of `KIND_ORDER` and defines its own teardown constant in its own new file, per the parallel-PR split. **Follow-up: unify the two into one pair of constants** once both have landed — the symmetry is the point (things come up in the order their dependencies allow and go down in the reverse), and it should live in one place. - `src/diff.ts` deliberately untouched (owned by other in-flight PRs); the order lives entirely at apply time. ## Operator acts None. No config, no migration — the next `cast apply` simply acts in the right order. `docs/semantics.md` gains a bullet under **Apply semantics** stating the order, why it is a constant and not a graph, and the queue-vs-readiness limit above. Closes #45. 🤖 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#53
No description provided.