Describe the Bug
A settings.depends_on entry that names an explicit stack is honored by the dependency DAG but silently ignored by atmos describe dependents unless the two components' context fields (namespace / tenant / environment / stage) also happen to match.
Because describe dependents backs affected-based workflows, a cross-stage dependency can order correctly under atmos terraform --all and still be invisible to CI — a missing deployment gate rather than a visible error.
Expected Behavior
When stack is set on a depends_on entry, it should identify the target stack on its own. The context fields exist to derive a stack when one is not given; once it is given they should not be able to veto the match.
Steps to Reproduce
Two stages, dev and prod, sharing namespace/tenant/environment:
# stacks/deploy/dev.yaml
vars: { namespace: acme, tenant: core, environment: ue2, stage: dev }
components:
terraform:
upstream:
metadata: { component: mock }
# stacks/deploy/prod.yaml
vars: { namespace: acme, tenant: core, environment: ue2, stage: prod }
components:
terraform:
downstream-by-stack: # `stack` alone
metadata: { component: mock }
settings:
depends_on:
1: { component: upstream, stack: dev }
downstream-stack-and-stage: # `stack` plus a redundant `stage`
metadata: { component: mock }
settings:
depends_on:
1: { component: upstream, stack: dev, stage: dev }
$ atmos describe dependents upstream -s dev --format json
[
{ "component": "downstream-stack-and-stage", ... }
]
downstream-by-stack is missing. Adding a stage: dev that the stack: dev should already imply is what makes it appear.
The DAG path disagrees — it resolves the same edge correctly:
$ atmos terraform plan --all --dry-run
✓ Would plan upstream in dev (dry run)
✓ Would plan downstream-by-stack in prod (dry run) # correctly ordered after upstream
…and emits no "Dependency target not found" warning for downstream-by-stack.
Cause
internal/exec/describe_dependents.go:
func isDependencyMatch(p *dependencyMatchParams) bool {
...
return matchLegacyStack(p.dependsOn, p.args.Stack, p.stackName) &&
matchLegacyContextFields(p.dependsOn, p.providedComponentVars, p.stackComponentVars)
}
matchLegacyStack short-circuits correctly on dependsOn.Stack, but it is ANDed with matchLegacyContextFields, which still runs. For a field left unset, matchContextField falls through to requiring the depending component's value to equal the target's:
func matchContextField(depValue, providedValue, stackValue string) bool {
if depValue != "" {
return providedValue == depValue
}
return providedValue == stackValue // dev != prod -> whole match fails
}
So stage is compared even when stack was explicit, and a cross-stage dependency fails on exactly the field the explicit stack was meant to settle.
The two other consumers behave differently:
parseDependencyMapEntry (internal/exec/dependency_parser.go) reads only component and stack — correct.
matchNewFormatStack, used for the replacement dependencies.components format, returns on Stack with no context matching — also correct.
Only the legacy settings.depends_on path ANDs them.
Possible Fix
Skip the context-field check when stack is explicit, mirroring matchNewFormatStack:
if p.dependsOn.Stack != "" {
return matchLegacyStack(p.dependsOn, p.args.Stack, p.stackName)
}
return matchLegacyStack(...) && matchLegacyContextFields(...)
Worth confirming against existing fixtures first — some may rely on the current AND for entries that set both.
Note settings.depends_on is deprecated in favor of dependencies.components, which is unaffected. If the decision is to leave legacy behavior frozen, the alternative is to document the requirement explicitly, since the shape most users would reach for is the one that silently fails.
Related
Describe the Bug
A
settings.depends_onentry that names an explicitstackis honored by the dependency DAG but silently ignored byatmos describe dependentsunless the two components' context fields (namespace/tenant/environment/stage) also happen to match.Because
describe dependentsbacks affected-based workflows, a cross-stage dependency can order correctly underatmos terraform --alland still be invisible to CI — a missing deployment gate rather than a visible error.Expected Behavior
When
stackis set on adepends_onentry, it should identify the target stack on its own. The context fields exist to derive a stack when one is not given; once it is given they should not be able to veto the match.Steps to Reproduce
Two stages,
devandprod, sharingnamespace/tenant/environment:downstream-by-stackis missing. Adding astage: devthat thestack: devshould already imply is what makes it appear.The DAG path disagrees — it resolves the same edge correctly:
…and emits no "Dependency target not found" warning for
downstream-by-stack.Cause
internal/exec/describe_dependents.go:matchLegacyStackshort-circuits correctly ondependsOn.Stack, but it is ANDed withmatchLegacyContextFields, which still runs. For a field left unset,matchContextFieldfalls through to requiring the depending component's value to equal the target's:So
stageis compared even whenstackwas explicit, and a cross-stage dependency fails on exactly the field the explicit stack was meant to settle.The two other consumers behave differently:
parseDependencyMapEntry(internal/exec/dependency_parser.go) reads onlycomponentandstack— correct.matchNewFormatStack, used for the replacementdependencies.componentsformat, returns onStackwith no context matching — also correct.Only the legacy
settings.depends_onpath ANDs them.Possible Fix
Skip the context-field check when
stackis explicit, mirroringmatchNewFormatStack:Worth confirming against existing fixtures first — some may rely on the current AND for entries that set both.
Note
settings.depends_onis deprecated in favor ofdependencies.components, which is unaffected. If the decision is to leave legacy behavior frozen, the alternative is to document the requirement explicitly, since the shape most users would reach for is the one that silently fails.Related
stackproperty todepends_on_manifest#2835 removed schemadescriptiontext that recommended the failing shape (stackalone, siblings "ignored whenstackis set").