fix(deploy): skip dead service rows in preflight, scope migration compose project - #327
Merged
Hydralerne merged 2 commits intoJul 31, 2026
Merged
Conversation
…hard-failing The multi-service preflight's sub-app check hard-fails the ENTIRE project's deploy when any enabled monorepo-kind service row lacks an install/build/ start command it's supposed to have. That's correct for a genuinely misconfigured, actively-used sub-app — but it applies just as unconditionally to a saved row that has never once been deployed: a duplicate/leftover row from an earlier import, or a half-finished "add service" that was never followed through. Hit this on a real self-hosted install: a docker-migration-imported project accumulated a stray, never-deployed monorepo-kind service row alongside its real (already-working, already-routed) compose services. That one dead row's missing start command blocked EVERY subsequent deploy of the whole project indefinitely — including a plain git-push redeploy that touched none of the project's actual services — with no way to recover short of an operator finding and editing the row directly in the database. Fix: track whether a service has ever produced a service_deployment row (one extra round trip via the existing `serviceDeployment.latestByProject` lookup — no new query shape). A saved row with NO install/build/start command AND no deployment history is downgraded from a hard failure to a warning: the operator still sees it flagged, but the rest of the project's services keep deploying. A row that HAS been deployed before, or that came from an explicit in-flight request (not projected from the DB, so its history is unknown), is unaffected and still fails exactly as before.
…project
adoptServerStack() resolved the caller's serviceNames against every service
discovered on the server, server-wide - service names are only unique WITHIN
a single compose project, so a server running several stacks has a bare name
like "app"/"db"/"redis" matching a container in EACH of them.
Those extra cross-stack matches were not dropped: buildAdoptedServiceRows
suffixed them ("app-2", "redis-3") and silently adopted another project's
containers into the one being migrated.
Fix: accept an optional composeProject to restrict resolution to ONE
discovered group (the compose project name, or null for the standalone/
hand-run-container group). Omitting it keeps the legacy server-wide match
for existing callers.
Contributor
Author
|
The failing |
This was referenced Jul 31, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
Two related deploy/migration robustness fixes.
1. Preflight hard-fails a whole project over one dead service row
The multi-service preflight's sub-app check hard-fails the entire
project's deploy when any enabled
monorepo-kind service row lacks aninstall/build/start command it's supposed to have. That's the right call
for a genuinely misconfigured, actively-used sub-app — but it applies just
as unconditionally to a saved row that has never once been deployed: a
duplicate/leftover row from an earlier import, or a half-finished "add
service" that was never followed through.
Hit this on a real self-hosted install: a docker-migration-imported project
accumulated a stray, never-deployed
monorepo-kind service row alongsideits real (already-working, already-routed) compose services. That one dead
row's missing start command blocked every subsequent deploy of the whole
project indefinitely — including a plain git-push redeploy that touched
none of the project's actual services — with no way to recover short of an
operator finding and editing the row directly in the database.
Fix: track whether a service has ever produced a
service_deploymentrow (one extra round trip via the existing
serviceDeployment.latestByProjectlookup — no new query shape, just reusing what's already there). A saved
row with no install/build/start command and no deployment history is
downgraded from a hard failure to a warning: the operator still sees it
flagged, but the rest of the project's services keep deploying. A row that
has been deployed before, or one that came from an explicit in-flight
request (not projected from the DB, so its history is unknown), is
unaffected and still fails exactly as before.
2. Adopted service-name resolution isn't scoped to a compose project
adoptServerStack()resolved the caller'sserviceNamesagainst everyservice discovered on the server, server-wide. Service names are only
unique within a single compose project, so a server running several
stacks has a bare name like
app/db/redismatching a container ineach of them — and those extra cross-stack matches weren't dropped:
buildAdoptedServiceRowssuffixed them (app-2,redis-3) and silentlyadopted another project's containers into the one being migrated.
Fix: accept an optional
composeProjectto restrict resolution to ONEdiscovered group (the compose project name, or
nullfor the standalone/hand-run-container group). Omitting it keeps the legacy server-wide match
for existing callers, so this is purely additive.
Test plan
tsc --noEmitonapps/api— clean.apps/api/test/modules/deployments/preflight.test.ts:one confirming a never-deployed, commandless monorepo row now produces a
warn(andresult.ok === true), one confirming a row that has beendeployed before still hard-fails exactly as before (no regression).
composeProjectmigration change; theexisting
migrate.service.test.tssuite is unaffected since the newparameter is optional and defaults to the prior server-wide behavior.
vitestrun hits a pre-existing, unrelated failure(
TypeError: undefined is not an object (evaluating '__vite_ssr_import_0__.z.object')inpackages/core/src/apps/schema.ts)affecting every test file in
apps/api/packages/adaptersidentically ona clean
origin/maincheckout with zero changes (confirmed viagit stash), so it isn't introduced by this PR. I tracedrunPreflightChecks'saggregation (
ok: checks.every(check => check.status !== "fail")) by handto confirm a
warn-only result correctly leavesok: true.