Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/exec/describe_affected.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/cloudposse/atmos/pkg/auth"
"github.com/cloudposse/atmos/pkg/ci"
cfg "github.com/cloudposse/atmos/pkg/config"
ghactions "github.com/cloudposse/atmos/pkg/github/actions"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/matrix"
"github.com/cloudposse/atmos/pkg/pager"
Expand Down Expand Up @@ -361,7 +362,13 @@ func (d *describeAffectedExec) view(a *DescribeAffectedCmdArgs, repoUrl string,
// Handle matrix format specially - it bypasses the normal view flow.
if a.Format == "matrix" {
entries := convertAffectedToMatrix(affected)
return matrix.WriteOutput(entries, a.GithubOutputFile)

// Resolve output file: explicit flag > CI auto-detect > stdout.
outputFile := a.GithubOutputFile
if outputFile == "" && d.atmosConfig.CI.Enabled {
outputFile = ghactions.GetOutputPath()
}
return matrix.WriteOutput(entries, outputFile)
}

// Reject --output-file for non-matrix formats — it would be silently ignored.
Expand Down
67 changes: 67 additions & 0 deletions internal/exec/describe_affected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,73 @@ func TestExecute_MatrixFormat(t *testing.T) {
assert.Contains(t, string(content), "matrix=")
assert.Contains(t, string(content), "count=1")
})

t.Run("matrix auto-detects GITHUB_OUTPUT when CI is enabled", func(t *testing.T) {
// When ci.enabled=true and no explicit --output-file is given,
// the matrix output should be written to $GITHUB_OUTPUT automatically.
autoFile := filepath.Join(t.TempDir(), "auto_output")
t.Setenv("GITHUB_OUTPUT", autoFile)

dCI := d
dCI.atmosConfig = &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: true}}

err := dCI.Execute(&DescribeAffectedCmdArgs{
Format: "matrix",
CLIConfig: &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: true}},
})
require.NoError(t, err)

content, err := os.ReadFile(autoFile)
require.NoError(t, err)
assert.Contains(t, string(content), "matrix=")
assert.Contains(t, string(content), "count=1")
})

t.Run("explicit --output-file wins over CI auto-detect", func(t *testing.T) {
// When both --output-file and CI auto-detect would apply, the explicit
// flag must win. The GITHUB_OUTPUT path should be left untouched.
explicitFile := filepath.Join(t.TempDir(), "explicit_output")
ghOutputFile := filepath.Join(t.TempDir(), "github_output")
t.Setenv("GITHUB_OUTPUT", ghOutputFile)

dCI := d
dCI.atmosConfig = &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: true}}

err := dCI.Execute(&DescribeAffectedCmdArgs{
Format: "matrix",
GithubOutputFile: explicitFile,
CLIConfig: &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: true}},
})
require.NoError(t, err)

content, err := os.ReadFile(explicitFile)
require.NoError(t, err)
assert.Contains(t, string(content), "matrix=")

// The GITHUB_OUTPUT path must NOT have been written to.
_, err = os.Stat(ghOutputFile)
assert.True(t, os.IsNotExist(err), "GITHUB_OUTPUT file should not exist when --output-file is set explicitly")
})

t.Run("no auto-detect when CI is disabled", func(t *testing.T) {
// When ci.enabled=false, GITHUB_OUTPUT should be ignored even if it's
// set in the environment. Output goes to stdout (unverifiable here, but
// we assert the GITHUB_OUTPUT file remains untouched).
ghOutputFile := filepath.Join(t.TempDir(), "github_output")
t.Setenv("GITHUB_OUTPUT", ghOutputFile)

dNoCI := d
dNoCI.atmosConfig = &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: false}}

err := dNoCI.Execute(&DescribeAffectedCmdArgs{
Format: "matrix",
CLIConfig: &schema.AtmosConfiguration{CI: schema.CIConfig{Enabled: false}},
})
require.NoError(t, err)

_, err = os.Stat(ghOutputFile)
assert.True(t, os.IsNotExist(err), "GITHUB_OUTPUT file should not exist when CI is disabled")
})
}

// TestView_OutputFileRejectsNonMatrix tests that --output-file is rejected for non-matrix formats.
Expand Down
2 changes: 1 addition & 1 deletion website/blog/2025-12-15-list-affected-command.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ atmos list affected --repo-path /tmp/target-repo --format json

- [atmos list affected documentation](/cli/commands/list/affected)
- [atmos describe affected documentation](/cli/commands/describe/affected)
- [GitHub Actions integration](/integrations/github-actions/affected-stacks)
- [Native CI for GitHub Actions](/ci)

## Get Involved

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ jobs:

- [Describe Affected Command](/cli/commands/describe/affected) - Full command reference
- [List Affected Command](/cli/commands/list/affected) - Human-readable table view
- [Affected Stacks GitHub Action](/integrations/github-actions/affected-stacks) - CI/CD integration
- [Native CI in GitHub Actions](/ci) - CI/CD integration
- [Component Inheritance](/design-patterns/inheritance-patterns/abstract-component) - Abstract component patterns
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
slug: describe-affected-matrix-auto-output
title: "describe affected --format=matrix auto-routes to GITHUB_OUTPUT"
authors: [osterman]
tags: [enhancement]
---

`atmos describe affected --format=matrix` now writes to `$GITHUB_OUTPUT` automatically when CI is enabled, matching the behavior already shipped for `atmos list instances --format=matrix`. No more `--output-file=$GITHUB_OUTPUT` boilerplate in workflow YAML.

<!--truncate-->

## What Changed

When `ci.enabled: true` is set in `atmos.yaml` and `$GITHUB_OUTPUT` is present in the environment (i.e. you're running on GitHub Actions), the matrix JSON is written there automatically. The explicit `--output-file=$GITHUB_OUTPUT` flag is no longer required.

Before:

```yaml
- id: affected
run: atmos describe affected --format=matrix --output-file=$GITHUB_OUTPUT
```

After:

```yaml
- id: affected
run: atmos describe affected --format=matrix
```

The same goes for `atmos list instances --format=matrix`, which already had this behavior. Both commands now resolve their output destination the same way: explicit `--output-file` flag wins; otherwise, fall back to `$GITHUB_OUTPUT` when CI is enabled; otherwise, write JSON to stdout.

## Why This Matters

Before this change, the two `--format=matrix` commands behaved differently — `list instances` auto-detected, `describe affected` didn't. Workflow authors had to remember which one needed the explicit flag. With this change the two commands are symmetric, the docs are simpler, and copy-pasted workflows from one command to the other don't silently break.

It's also one fewer thing to type. The matrix output is the canonical pattern for fan-out workflows in GitHub Actions, and the explicit redirect was always boilerplate.

## How to Use It

Set `ci.enabled: true` in your `atmos.yaml`:

```yaml
ci:
enabled: true
```

Then use the matrix command without the `--output-file` flag in your workflow:

```yaml
jobs:
affected:
runs-on: ubuntu-latest
container:
image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
outputs:
matrix: ${{ steps.affected.outputs.matrix }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- id: affected
run: atmos describe affected --format=matrix

deploy:
needs: affected
if: ${{ needs.affected.outputs.matrix != '' }}
runs-on: ubuntu-latest
container:
image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
strategy:
matrix: ${{ fromJson(needs.affected.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v6

- env:
COMPONENT: ${{ matrix.component }}
STACK: ${{ matrix.stack }}
run: atmos terraform deploy "$COMPONENT" -s "$STACK"
```

The explicit `--output-file` flag still works if you need it (writing to an arbitrary path, non-GitHub CI providers, etc.) — explicit always wins over auto-detection.

## Get Involved

See the full CI workflow patterns at [Native CI](/ci), including the new "Deploy Affected" and "Deploy All" examples. The two reference repos — [`cloudposse-examples/atmos-native-ci`](https://github.com/cloudposse-examples/atmos-native-ci) and [`atmos-native-ci-advanced`](https://github.com/cloudposse-examples/atmos-native-ci-advanced) — show end-to-end working pipelines you can clone and adapt.
Loading
Loading