fix: scope stale-service reconciliation to the services that failed to resolve - #238
Merged
Merged
Conversation
…o resolve A service that could not be resolved from YAML raised an environment-wide veto (`canReconcile = false`), which disabled stale-service cleanup for every other service in the environment. One unresolvable reference — an archived dependency repository, or a legacy `serviceId` entry — was enough to switch reconciliation off permanently for that environment. Record the unresolved service names, and the repositories whose config could not be read, and exclude only those from reaping. `canReconcile` now covers just the root-level failures where nothing is known: the source repository or ref is missing, the targeted repository is not live, or no service could be attributed to the targeted scope. Also log the remaining `canReconcile` early return, which was silent and indistinguishable from a clean reconciliation pass.
…ution The `requires:` recursion only runs for a service that resolved, so when exact-name resolution fails the service's inner dependencies are never enumerated and fall out of the expected set. Record the repository, not just the service name, so those children are protected from reaping.
vmelikyan
approved these changes
Aug 24, 2026
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.
Problem
Stale-service reconciliation silently stopped running for any environment whose
lifecycle.yamlnames a service that cannot be fully resolved — most commonly a dependencyrepository:that is no longer live in Lifecycle (archived, or removed from the GitHub App installation), or a legacyserviceId:entry.The failure is invisible: the environment keeps building and deploying services that were renamed or deleted from
lifecycle.yaml, and every one of those deploys fails. Renaming a service is the usual way to hit it — the old deployable row is never reaped, so it redeploys forever alongside the new one.Origin
Reconciliation landed in #198 with a single environment-wide veto: if a referenced YAML config could not be fetched, skip cleanup entirely.
#229 refactored the dependency fetch from a repository name to a repository row:
That needs a non-null row, so a
!repository || repository.deletedAt != nullguard was added — and it reached for the same environment-wide veto. The liveness test thereby moved from "can this YAML be fetched?" to "does Lifecycle hold a live repository row?", which a de-onboarded repository now fails permanently even though its config is still readable.The veto is also checked before the
reconcileDeletedServicesfeature flag, so an operator who enables the flag sees no effect and no log line explaining why.Fix
Make the veto per-service rather than environment-wide.
upsertDeployablesnow returnsunresolvedServiceNamesandunresolvedRepositoryIds. Reaping excludes those names, plus any deployable owned by a repository whose config could not be read —requires:children of such a repository are never enumerated, so they are protected by repository id rather than by name.canReconcileis kept for the root-level failures where genuinely nothing is known:The safety property is unchanged — never reap a service whose config we could not read — it is just enforced at the granularity of the service that actually failed. A service that is absent from a config we read successfully is still reaped, which is what makes renames and deletions work.
The surviving
canReconcileearly return now logs instead of returning silently.Risks
deployableSourceSeam.test.ts, "fails closed before YAML import when the targeted repository has no live row") still passes unmodified.reconcileDeletedServicesfeature flag.Tests
deployableSourceSeam.test.ts— an unresolvable dependency repository, and a legacyserviceIdentry, are recorded as unresolved and no longer flipcanReconcile.build.test.ts— unresolved names and unresolved repository ids are excluded from reaping; an unresolved dependency no longer blocks reaping the rest of the environment.Full suite passes (4198 tests). Lint and Prettier clean.