fleet operations: cast diff/apply --all over the project registry (#26) #32
No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#32
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/fleet-all"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #26.
Every cast verb was single-project. "Do this to the whole instance" was a shell loop the operator wrote from memory — and the project you forget is the one that drifted.
The refactor comes first
The
apply/diffblock was one long inline body. It is nowrunProject(ctx, orgRepo)— checkout → secrets → desired → live read → diff → optionally apply — and both the single-repo path and the--allloop call it. There is exactly one implementation of what a project run is; a second, parallel fleet path is how the two drift apart.openCoolifyand the team assert are hoisted out of the per-project body: one--envmeans one instance and one team, and asserting once still lands strictly before the first project's first read — which is the gate's whole point, since the read is already the lie.Fail closed, because a skipped project reads exactly like a clean one
diff --all0diff --all1diff --all2apply --all0apply --all≠0Cannot reach collapses clone failure, a manifest with no block for this environment, an absent or undecryptable store,
LiveLookup.found === false, and any HTTP error into one fatal outcome — never a skip.fleetExitCodedefaults to 2 on any coverage shape it does not recognize.--allrefuses alongside the positional,--path,--project,--environment,--resourceand--hostname-overlay— each names ONE project's checkout, Coolify name or resources; fleet-wide they are meaningless at best (--project Xapplied to every project in the fleet).diff --allruns every project to completion; stopping early would hide the drift in the ones it never reached.apply --allstops at the first failure and says which projects it applied and which it did not touch — cast does not get to keep mutating a fleet after an unexplained failure.read: 2 of 3) prints above the findings, so nothing to report and I looked at nothing cannot render the same.One fix found en route
The
--path+--env prodban (a feature-branch checkout must not decide what prod runs) was only being caught by accident ofresolveCheckoutrunning before the bindings load. HoistingloadBindingsfor the registry moved that. The rule is now a value inresolve.tsused by both an up-front CLI refusal andresolveCheckout— one rule, one string, two call sites, and the up-front path has its own test rather than riding onresolve.test.ts's.applykeeps its existing position that an absent Coolify project is a create, not a failure.Tests
test/fleet.test.ts(the exit-code table, renderers, refusals) andtest/fleet-cli.test.ts(end-to-end over three projects). Since--allforbids--path, the e2e takes the real clone path — git'sinsteadOfredirectsgithub.comto local repos, so the clone is genuine while the network is not. It covers iteration, per-project reporting, the clean/drift/unreachable aggregates, unreachable-outranks-drift, an unreachable project not stopping the read, missing-store and HTTP-500 as unreachable, both empty-registry variants, all six flag conflicts, andapply --allstopping — asserting from the stub's request log that the un-reached project was never even asked about.Gates:
npm run check,npm run build,npm test(244 passing, was 213) all green.🤖 Generated with Claude Code