Repository navigation
fix(render): hold a render to its own environment's exclusions - #1223
Merged
antoinetoussaint-byte merged 6 commits intoOct 28, 2026
Merged
Conversation
A gitops render of one environment was refused over another environment's exclude-services. `render --all --env blue` exited 1 over `staging`, and without --debug the message named no environment at all, which made it read like blue's own declaration was wrong when blue's was correct and read. ValidateWorkspace called ValidateExcludedServices, which holds EVERY environment to the workspace graph. That graph is the current composition (loadExclusionGraph: LoadServices, LoadModules, ConsumedEndpoints), so an environment imported before a module overlay added a service carries no withdrawal for it and fails against a composition it never saw. Any two environments composing different module sets hit this as soon as one gains a service the other has not withdrawn. The operator could not fix it from the declaration side either: re-importing the offending environment requires the product intent to declare it, and platform-obin's declares only dev, blue and green. A caller that selected an environment now passes it and gets ValidateEnvironmentExclusions' existing contract — "a flow is held to its own environment's composition, never refused over another's" — which run, build and deploy already used through NewFlow. A caller with no selection keeps holding every environment, which is right for a whole-workspace check such as the doctor. Module namespaces and host composition stay whole-workspace invariants, unchanged. The regression asserts both halves: the inconsistent environment still refuses when it is the selected one, so the test cannot pass vacuously, and selecting the clean environment succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Without --debug the refusal printed no environment at all — just `exclude-services withdraws "authority/local"…` — so its reader could not tell which environment's declaration was at fault and re-declared something already declared, twice. The name was there, as an `environment %q:` prefix on the wrapped error, but a prefix is a separate link in the error chain and the plain renderer shows only the leaf; the name appeared solely in the --debug chain. So each message in validateExclusionsAgainst now names the environment itself, and the redundant prefix is gone from both validators rather than duplicating it. Every message in that function is covered, not only the dependency one that surfaced: removing the prefix while leaving the siblings unnamed would have taken the name away from refusals that previously carried it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd document it The earlier regression went through NewFlow, which already used ValidateEnvironmentExclusions at flow.go:484, so it never reached the ValidateWorkspace path this PR changes — it passed against unfixed main, which I verified rather than assumed. The claim in the PR body that it could not pass vacuously was wrong about which path it covered. TestValidateWorkspaceHoldsOnlyTheSelectedEnvironment calls ValidateWorkspace directly and is behaviourally red at the parent: giving the parent the new signature while keeping ValidateExcludedServices fails on "a selected environment must not be refused over another environment's exclusion". It also pins the two halves that must not move — the inconsistent environment still refuses when it is the one selected, and a caller selecting nothing, nil included, still holds every environment. Docs corrected rather than added to. An earlier draft offered `doctor workspace` as the whole-workspace example; doctor already validates the selected environment through its own scope check (cmd/doctor_workspace.go:794), so that was inaccurate. Every CLI path that validates exclusions selects an environment; the whole-workspace form is what remains for a caller that selected none. docs/configuration-contract.md now states that these refusals are judged for the selected environment and name it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e claim Three bounded corrections from the focused closure review. A render-caller regression. The direct ValidateWorkspace test proves the validator narrows, but nothing proved the render entries still pass what they selected: dropping the environment from all four production callers (orchestrate.go twice, dev.go, solution.go) left the whole gitops package green. TestRenderModuleIsNotRefusedOverAnotherEnvironmentsExclusion goes through RenderModule with `stale` carrying a graph-inconsistent exclusion and `staging` clean, asserts the inconsistent environment is still refused when it IS the selection (so the second assertion is not vacuous), and fails when orchestrate.go drops its argument. Verified both ways. Accurate scope. The prose said an operation is "never refused over a different environment's exclude-services", which is broader than the code. The selection narrows the graph-consistency check and nothing else: loading a workspace decodes and validates every environment, so a malformed exclusion anywhere still refuses, and module namespaces, host composition, service-scoped names and managed-service keys are all still held across every environment — the same cross-environment shape, left deliberately whole-workspace. Both pages now say so, and TestSelectionNarrowsTheGraphCheckAndNothingElse pins it: a clean environment selects fine and is still refused over an unqualified name declared somewhere else. Doctor. Three comments said doctor relies on the no-selection fallback. It does not — it selects and calls ValidateEnvironmentExclusions directly (cmd/doctor_workspace.go). Corrected without changing doctor's behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The prose said every environment is validated "whenever a workspace is loaded"
and that a malformed exclusion "refuses any operation". The test added
alongside it shows otherwise: LoadWorkspaceFromDir succeeds and Select("local")
succeeds, and only then does ValidateWorkspace refuse.
Corrected to what actually holds — ValidateWorkspace decodes and validates
every environment's declarations before comparing the selected one against the
graph, so a malformed exclusion refuses whatever was selected — with the
load/select distinction stated rather than implied. Both pages and the test's
own comment. No behaviour or test change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment said the test fails if "any render entry" drops its selection. It covers RenderModule. The other three callers that pass a selection — orchestrate.go's second call, dev.go and solution.go — are reviewed source, not covered here, and the comment now says so. No test or behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
A gitops render of one environment was refused over another environment's
exclude-services.codefly deploy gitops render --all --env blue --app-project platform-obin --local-agentsexited 1 before any module rendered, and without--debugthe message named no environment at all — which is what made this misdiagnose twice as blue's own declaration being wrong. With--debug:Blue's declaration was correct and read: the same run logs
Skipped authority: environment blue withdraws every service of it, and blue's block carrieswithdrawn-from: [robin/chat, robin/sandbox].Cause
environments.ValidateWorkspacecalledValidateExcludedServices, which iteratesFromWorkspace(workspace)and holds every environment to the workspace graph. That graph is built from the current composition (loadExclusionGraph:LoadServices,LoadModules,solutionrun.ConsumedEndpoints), so an environment imported before a module overlay added a service carries no withdrawal for it and is judged against a composition it never saw.robin/sandboxentered through the robin module overlay;stagingpredates it.This is not theoretical: any two environments composing different module sets hit it as soon as one gains a service the other has not withdrawn.
The sibling validator already had the right contract and was used by run, build and deploy through
NewFlow—ValidateEnvironmentExclusions, whose comment reads "a flow is held to its own environment's composition, never refused over another's". Only the gitops render path took the all-environments one.Fix
ValidateWorkspacetakes an optional selected environment. The four gitops callers all had one in scope and now pass it (orchestrate.go×2,dev.go,solution.goviareq.Environment), so a render is held to its own environment. A caller with no selection keeps holding every environment, which is correct for a whole-workspace check such asdoctor workspace. Module namespaces and host composition remain whole-workspace invariants and are unchanged, as is deploy/runtime validation.Nothing was weakened: the inconsistent environment still refuses when it is the one selected.
Why not fix it from the declaration side
Re-importing the offending environment requires the product intent to declare it. platform-obin's
delivery.environments.yamldeclares exactlydev,blueandgreen— nostaging— socodefly environment import staging …correctly exits 1 withproduct intent does not declare environment "staging". Fabricating an unused staging entry was explicitly ruled out, which is why the fix belongs here.Test
TestFlowIsNotRefusedOverAnotherEnvironmentsExclusionasserts both halves, so it cannot pass vacuously: the inconsistent environment still refuses when selected, and selecting the clean environment succeeds.pkg/environments,pkg/gitops(135s) and the non-enginepkg/orchestrationset all pass;go vet ./pkg/...andgolangci-lintclean.Not claimed
I have not run a real render — Blue10901 has the reproducer and will report exactly what it prints. This change removes the cross-environment refusal; it does not assert that blue's render then succeeds end to end.
🤖 Generated with Claude Code