cloudposse / cloudposse/atmos

settings.depends_on: explicit `stack` ignored by `describe dependents` unless context fields also match

Open
#2,910 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
1.4k
Forks
175
Avg merge
2d 3h
Merged PRs (30d)
134

Description

## 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`:

```yaml
# 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 }
```

```console
$ 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:

```console
$ 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`:

```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:

```go
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`:

```go
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

- #2835 removed schema `description` text that recommended the failing shape (`stack` alone, siblings "ignored when `stack` is set").

Contributor guide

Open the contributing guide

Research direction

Read internal/exec/describe_dependents.go, especially isDependencyMatch and the legacy matching helpers, then compare them with matchNewFormatStack and parseDependencyMapEntry. Check existing dependency and describe-dependents fixtures before changing behavior. Done means an explicit stack matches without requiring context fields, while entries without an explicit stack retain their current matching behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, terraform
Domain
cli, devops
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.