# Native CI

Atmos brings first-class CI/CD support directly into the CLI. Run Atmos commands in
GitHub Actions and get rich job summaries, status checks, output variables, pull-request plan
comments, and stored planfile verification without wrapper actions. PR comments require
`ci.comments.enabled: true` and `pull-requests: write`. And because every command is **git-aware**,
the same CLI powers GitOps end to end: plan and apply only what changed, vendor reusable catalogs
across repositories, and commit results back to your source of truth. Kubernetes components also
write native job summaries, with a deliberately smaller v1 surface.

:::note Replaces the old `cloudposse/github-action-*` Actions
You do not need the separate [`cloudposse/github-action-atmos-terraform-*` Actions](/deprecated/github-actions)
anymore. The `atmos` CLI now does this work:

- It writes job summaries.
- It posts status checks.
- It sets output variables.
- It posts pull-request plan summaries.
- It manages planfiles.

Do not use the old Actions in new workflows. Existing workflows that use them still work. `actions/checkout`
plus an `atmos` command is the minimum for a basic workflow (job summaries, output variables) — it is not
universally sufficient. Status checks, check runs, PR comments, and OIDC need matching permissions, and SBOM
uploads and `github/artifacts` planfile storage also need the `github-runtime` action. See
[Permissions](#permissions) below.
:::

> ⚠️ Experimental

## Quick Start

**File:** `atmos.yaml`

```yaml
ci:
  enabled: true
  summary:
    enabled: true
  output:
    enabled: true
    variables:
      - has_changes
      - has_additions
      - has_destructions
      - plan_summary
  checks:
    enabled: true
  comments:
    enabled: true
    behavior: upsert
```

```shell
- name: Plan
  run: atmos terraform plan vpc -s prod
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Deploy
  run: atmos terraform deploy vpc -s prod
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

**CI Configuration**

Configure CI providers, job summaries, output variables, status checks, pull-request plan comments, planfile storage, and templates in your `atmos.yaml`.

Configuration Reference[Read more](/cli/configuration/ci)

## GitHub Actions Workflows

Running Atmos in GitHub Actions reduces to two steps for the basics: **check out the repository, then run an `atmos` command.** Atmos detects the CI environment automatically and produces job summaries and output variables with no extra setup. Status checks, check runs, and PR comments need matching [permissions](#permissions); SBOM uploads and planfile storage in `github/artifacts` also need the `github-runtime` action.

Terraform commands use the full native CI feature set. Kubernetes commands currently emit
human-readable job summaries only; they do not write `$GITHUB_OUTPUT` values, commit statuses,
PR comments, or stored artifacts.

The examples below pin the Atmos version via a [repository variable](https://docs.github.com/en/actions/learn-github-actions/variables) named `ATMOS_VERSION` (e.g. set to `1.200.0`). We don't publish a `latest` tag, so always pin to a specific release.

### Permissions

Atmos's native CI features rely on the standard GitHub Actions permission scopes — grant only what each workflow's triggers require:

| Feature | Permission |
| --- | --- |
| Job summaries (`$GITHUB_STEP_SUMMARY`) | none required |
| Output variables (`$GITHUB_OUTPUT`) | none required |
| Commit [status checks](/cli/configuration/ci/checks) | `statuses: write` |
| Check runs (modern Checks API) | `checks: write` |
| PR comments | `pull-requests: write` |
| Checkout | `contents: read` |
| OIDC token issuance | `id-token: write` |
| SBOM workflow artifact upload | `contents: read`; GitHub Actions runtime credentials (see below) |

There is no `comments: write` scope — PR comment writes use `pull-requests: write` (PR comments are issue comments under the hood). If you disable a feature in `atmos.yaml` (e.g. `ci.checks.enabled: false`), you can drop the matching permission.

### SBOM Artifacts

Running `atmos sbom generate --upload` uses the detected native CI provider to retain the generated SBOM with the CI run. In GitHub Actions, this is a workflow artifact, not a Dependency Graph import. GitHub's SBOM REST API can export or request a GitHub-generated SPDX report, but it does not accept an arbitrary submitted SBOM. Add `github-runtime` before the command so Atmos can access the runtime API for Actions artifacts.

```yaml
- uses: cloudposse/atmos/actions/github-runtime@v1
  with:
    mode: env
- run: atmos sbom generate --format spdx-json --output sbom.spdx.json --upload
  env:
    GITHUB_TOKEN: ${{ github.token }}
```

### Validate Workflows

Lint GitHub Actions workflows directly with Atmos before running infrastructure commands:

```yaml title=".github/workflows/validate.yml"
- name: Validate workflows
  run: atmos validate ci
```

`atmos validate ci` is an alias for [`atmos ci validate`](/cli/commands/ci/validate). With no file arguments it checks every `.yml` and `.yaml` workflow under the current working directory's `.github/workflows`. In GitHub Actions it emits inline annotations when `ci.enabled: true` and annotations are enabled. Use `--format=sarif` to write a SARIF document for an explicit code-scanning upload step.

To lint workflow fixtures or another workflow directory, pass `--workflow-path`. The repository includes an intentionally invalid scenario that is useful for a local smoke test:

```shell
atmos ci validate --workflow-path tests/fixtures/scenarios/invalid-github-actions-workflows/.github/workflows
```

The command reports the invalid `branch` key and exits with status 1.

Atmos respects `.github/actionlint.yaml` or `.github/actionlint.yml`, including custom self-hosted runner labels. See [`atmos ci validate`](/cli/commands/ci/validate) for file selection, output formats, and configuration details.

### Plan on Pull Request

**File:** `.github/workflows/plan.yml`

```yaml
name: Plan
on:
  pull_request:

permissions:
  id-token: write
  contents: read
  statuses: write
  checks: write
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
    steps:
      - uses: actions/checkout@v6

      - run: atmos terraform plan vpc -s prod
```

### Apply on Merge

**File:** `.github/workflows/apply.yml`

```yaml
name: Apply
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read
  statuses: write
  checks: write

jobs:
  apply:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
    steps:
      - uses: actions/checkout@v6

      - run: atmos terraform deploy vpc -s prod
```

`atmos terraform deploy` runs a fresh plan and applies it with `-auto-approve`. When [planfile storage](/ci/planfile-storage) is configured under `components.terraform.planfiles` in `atmos.yaml`, a CI deploy **automatically** downloads the planfile uploaded during the PR run, generates a fresh plan, and performs a semantic comparison before applying (failing on drift by default). The `--verify-plan` flag (or `ATMOS_TERRAFORM_VERIFY_PLAN=true`) is only a per-run override — it forces verification on (and `--verify-plan=false` forces it off), and it **requires planfile storage to be configured**: without it there is no stored plan to verify against, and the deploy errors. You can also run `atmos terraform apply` directly. See [Planfile Storage](/ci/planfile-storage) for details.

:::note
Storing planfiles in the `github/artifacts` backend requires the runner's artifact credentials,
which GitHub withholds from `run:` steps. Add the
[`github-runtime` action](/ci/planfile-storage#using-github-artifacts-in-github-actions) before the
`plan`/`deploy` steps in the workflows above.
:::

### Deploy Affected

Fan out across only the components that changed in the PR using `atmos describe affected --format=matrix`. When `ci.enabled: true` is set in `atmos.yaml`, the matrix is automatically written to `$GITHUB_OUTPUT` — no `--output-file` flag needed.

**File:** `.github/workflows/deploy-affected.yml`

```yaml
on:
  pull_request:

permissions:
  id-token: write
  contents: read
  statuses: write
  checks: write
  pull-requests: write

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 `affected` job emits a matrix of `{component, stack}` pairs; the `deploy` job spreads across them in parallel.

### Caching the Toolchain

Every CI job reinstalls the same toolchain (Terraform, Helm, and friends). The **build cache** restores the Atmos cache root — which includes the toolchain install path — at the start of a job and saves it at the end, using the same GitHub Actions cache store that `actions/cache` uses. A warm cache turns a multi-minute toolchain install into a near-instant restore.

The most secure, lowest-boilerplate wiring is the `actions/cache` composite: Atmos derives the cache key and paths, native `actions/cache` does the storage, and **no runtime token is exposed to your job.**

**File:** `.github/workflows/plan.yml`

```yaml
steps:
      - uses: actions/checkout@v6

      - uses: cloudposse/atmos/actions/cache@v1   # pin to a release or SHA

      - run: atmos toolchain install              # near-instant on a cache hit
      - run: atmos terraform plan vpc -s prod
```

Prefer fully automatic caching? Set `ci.cache.auto: both` in `atmos.yaml` and Atmos restores on start and saves on exit for every invocation — no extra workflow steps:

**File:** `atmos.yaml`

```yaml
ci:
  cache:
    enabled: true     # master switch (required)
    auto: both        # restore on start AND save on end
```

You can also drive the cache explicitly with the [`atmos ci cache`](/cli/commands/ci/cache) subcommands (`restore`, `save`, `paths`, `list`, `delete`). The cache key defaults to a hash of the toolchain lockfile plus OS/arch with a prefix restore-key fallback, mirroring `actions/cache`; entries are write-once, so an exact hit skips the save. See the [cache configuration reference](/cli/configuration/ci/cache) for keys, paths, and the four GitHub Actions integration options with their security trade-offs.

### Deploy All

Fan out across **every** instance defined in your stacks using `atmos list instances --format=matrix`. Use this for full deploys, drift sweeps across the whole estate, or initial bootstraps. Like `describe affected`, it auto-routes to `$GITHUB_OUTPUT` when CI is enabled.

**File:** `.github/workflows/deploy-all.yml`

```yaml
on:
  workflow_dispatch:
  schedule:
    - cron: '0 6 * * *'   # Daily drift sweep

permissions:
  id-token: write
  contents: read
  statuses: write
  checks: write

jobs:
  inventory:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
    outputs:
      matrix: ${{ steps.list.outputs.matrix }}
    steps:
      - uses: actions/checkout@v6

      - id: list
        run: atmos list instances --format=matrix

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

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

### Authentication

The shape of the story: **define a CI profile in `atmos.yaml`, point the workflow at it, done.** Atmos exchanges the GitHub OIDC token for cloud credentials transparently — there is no `atmos auth login` step in CI.

**1. Define a `github` [profile](/cli/configuration/profiles).** The profile name is arbitrary; we use `github` to match the [example repos](https://github.com/cloudposse-examples/atmos-native-ci). It holds the `github/oidc` provider plus the identity CI should use:

**File:** `atmos.yaml`

```yaml
auth:
  providers:
    github-oidc:
      kind: github/oidc
      region: us-east-1
  identities:
    plat-dev/terraform:
      provider: github-oidc
      role_arn: arn:aws:iam::111122223333:role/atmos-terraform
      default: true   # Use this identity unless a component overrides it
```

The IAM role's trust policy must allow GitHub's OIDC issuer for your repo — see [Configuring OpenID Connect in AWS](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) for the trust-policy template. Other clouds work the same way: see [`azure/oidc`](/cli/configuration/auth/providers) and [`gcp/workload-identity-federation`](/cli/configuration/auth/providers).

**2. Pick how identities are selected.** All three work under the same profile:

- **One identity for everything**
  Mark one identity 
  `default: true`
   (as above), or set the 
  `ATMOS_IDENTITY`
   env var. Simplest setup — works when CI talks to one cloud account/role.
- **Per-component identity**
  Set 
  `settings.identity`
   on a component or stack to pick a different identity for that scope. Useful when prod components need a different role than dev.
- **Inheritance**
  Identities flow through the 
  [stack inheritance](/stacks)
   chain like everything else, so you can set the identity on a base stack and let descendants inherit (or override) it.

**3. Wire the workflow.** Two pieces: the `id-token: write` permission, and `ATMOS_PROFILE` set to the profile name.

**File:** `.github/workflows/apply.yml`

```yaml
permissions:
  id-token: write    # Required for GitHub to mint the OIDC token
  contents: read
  statuses: write
  checks: write

env:
  ATMOS_PROFILE: github

jobs:
  deploy:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
    steps:
      - uses: actions/checkout@v6

      - run: atmos terraform deploy vpc -s prod
```

`id-token: write` lets GitHub issue the OIDC JWT; `ATMOS_PROFILE: github` activates the profile defined above. **No `atmos auth login` step is needed** — Atmos exchanges the OIDC token for cloud credentials when it runs the terraform command. (`atmos auth login` exists for interactive/local use; in CI it's redundant.)

### Gating Production with Environments

GitHub Actions [environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) buy you three things: per-environment secrets and variables, required manual approvals before the job runs, and deployment history visible in the GitHub UI. Wire one in by adding `environment:` to the deploy job:

**File:** `.github/workflows/apply.yml`

```yaml
jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/cloudposse/atmos:${{ vars.ATMOS_VERSION }}
    environment: prod    # Requires approval if configured in repo settings
    permissions:
      id-token: write
      contents: read
      statuses: write
      checks: write
    env:
      ATMOS_PROFILE: github
    steps:
      - uses: actions/checkout@v6

      - run: atmos terraform deploy vpc -s prod
```

The **GitHub environment** (`prod`) and the **Atmos stack** (`-s prod`) are independent concepts — one gates the workflow run, the other selects the stack configuration. Many teams happen to name them the same; nothing requires it.

For deeper auth reference: [Profiles](/cli/configuration/profiles), [Auth concepts](/stacks/auth), [Providers](/cli/configuration/auth/providers), [Identities](/cli/configuration/auth/identities).

**Working Examples**

Two reference repositories you can clone and adapt — both include `atmos.yaml`, stack configuration, components, and full workflows. They build on the patterns above with use-case-specific design patterns (preview environments, image promotion, label gating).

Basic Example[Read more](https://github.com/cloudposse-examples/atmos-native-ci)
 
Matrix Example[Read more](https://github.com/cloudposse-examples/atmos-native-ci-advanced)

:::warning Concurrency groups aren't a deployment queue
By default (`concurrency.queue: single`), a GitHub Actions `concurrency` group holds at most two
runs at a time: one **in progress** and one **pending**. Triggering a third run cancels the
pending one and takes its place — this eviction happens regardless of `cancel-in-progress`, so
intermediate triggers can be silently dropped. Setting `cancel-in-progress: true` additionally
cancels the **in-progress** run itself, which can interrupt a Terraform apply and leave a remote
state lock that needs manual recovery. Setting `queue: max` instead allows up to 100 pending runs
before any get canceled, but it is still not a FIFO deployment queue and cannot be combined with
`cancel-in-progress: true`.

Remote state locking only prevents concurrent writers — it doesn't protect against a partially
applied change or automatically recover an interrupted run. Inspect the affected resources,
confirm the previous run has actually stopped, and only then run
[`atmos terraform force-unlock`](/cli/commands/terraform/force-unlock) before retrying the apply.
GitHub environments add approval gates and merge queues order merges, but neither guarantees
deployment _execution_ order — reach for an explicit promotion workflow or deployment controller
when you require that.
:::

## Features

### [Job Summaries](/ci/job-summaries)

Rich Markdown summaries with resource counts, inline badges, collapsible diffs, and captured
command output written to `$GITHUB_STEP_SUMMARY`. Terraform includes plan/apply summaries plus
the broader native CI features below. Helm and Helmfile currently write summaries only.
Templates are fully customizable with Go template syntax.

Kubernetes summaries are intentionally compact: `plan`/`diff` show created, changed, and
no-change objects; `apply`/`deploy` show applied or delivered objects; `delete` shows deleted
and not-found objects; `validate` shows valid and invalid objects plus errors.

### [Outputs](/cli/configuration/ci/output)

Terraform plan and apply results exported as CI output variables for use in downstream jobs.
On GitHub Actions, these are written to `$GITHUB_OUTPUT`. Kubernetes commands do not emit
output variables in v1.

### [Checks](/cli/configuration/ci/checks)

Live commit status checks showing real-time operation progress — "Plan in progress" while
running and "3 to add, 1 to change, 0 to destroy" when complete.

### [PR Comments](/cli/configuration/ci/comments)

When `ci.comments.enabled: true`, Terraform plans running in a pull-request context can post their
rendered plan summary as a PR comment. The comment uses the same template as the job summary and,
with the default `upsert` behavior, updates one comment per command, component, and stack on later
runs. This requires GitHub Actions `pull-requests: write`; comments are not posted for non-PR runs,
when no summary is available, or by apply, deploy, Kubernetes, Helm, or Helmfile commands.

### [Planfile Storage](/ci/planfile-storage)

Store and retrieve planfiles across CI pipeline stages using S3, GitHub Artifacts, or local
filesystem. The `deploy` command downloads stored planfiles, generates a fresh plan, and
performs a semantic comparison to detect drift before applying.

### [Build Cache](/cli/configuration/ci/cache)

Warm-start the toolchain across CI jobs by restoring and saving the Atmos cache root via the
CI provider's cache store — the same store `actions/cache` uses. Runs automatically with
`ci.cache.auto: both`, or explicitly with the [`atmos ci cache`](/cli/commands/ci/cache)
subcommands.

### [GitOps](/cli/configuration/git)

Atmos is **git-aware**, which is what makes true GitOps possible: the repository is the source of
truth, and the pipeline reconciles only what changed.

- **Plan what's affected**
  [`atmos describe affected`](/cli/commands/describe/affected)
   diffs two Git commits and reports exactly which components and stacks changed — including changes that ripple through dependencies, imports, and remote state. See 
  [Deploy Affected](#deploy-affected)
   for the matrix workflow.
- **Apply only what changed**
  Fan out across just the affected components, so a PR plans — and a merge applies — only the work that actually changed, instead of re-running the entire estate on every commit.
- **Reusable across repositories**
  Publish service catalogs and module libraries once and 
  [vendor](/vendor/)
   them into every workload repository, so many repos share one versioned source of truth instead of copy-pasting configuration.
- **Automate workload repositories**
  Commit generated artifacts back to a source-of-truth repository as part of the pipeline. Define managed repositories once under 
  `git.repositories`
  , then have Atmos commit and push automatically — with signed commits, a bot author identity, and bounded non-fast-forward retries — from 
  [`kind: git` hooks](/stacks/hooks#kind-git)
  , the 
  [`atmos git`](/cli/commands/git/usage)
   commands, or CI workflows. This is the foundation for GitOps with Argo CD, Flux, or downstream CI consuming the committed output.

## Commands

CI features are activated with the `--ci` flag on supported commands or automatically when running in a CI environment (e.g. GitHub Actions):

- **[`atmos terraform plan [--ci]`](/cli/commands/terraform/plan)**
  Run plan with job summary, output variables, status checks, and planfile upload.
- **[`atmos terraform apply [--ci]`](/cli/commands/terraform/apply)**
  Run apply with job summary, output variables, and status checks.
- **[`atmos terraform deploy [--ci]`](/cli/commands/terraform/deploy)**
  Deploy with stored planfile verification, drift detection, and full CI reporting.
- **[`atmos helm template|diff|apply|deploy|delete [--ci]`](/cli/commands/helm/usage)**
  Run native Helm components with job summaries for rendered/applied object metadata.
- **[`atmos helmfile template|diff|apply|sync|deploy|destroy [--ci]`](/cli/commands/helmfile/usage)**
  Run Helmfile components with job summaries that include captured masked command output.
- **[`atmos terraform planfile`](/cli/commands/terraform/planfile)**
  Manage stored planfiles: upload, download, list, delete, and show.
- **[`atmos describe affected --format=matrix`](/cli/commands/describe/affected)**
  Generate GitHub Actions matrix strategy from affected components.
- **[`atmos kubernetes render|plan|diff|apply|deploy|delete|validate [--ci]`](/cli/commands/kubernetes/usage)**
  Run Kubernetes operations with a native job summary only. No output variables, status checks, comments, or artifacts are emitted.

## Providers

Atmos auto-detects the CI environment and selects the appropriate provider:

- ****GitHub Actions****
  Integrates with GitHub job summaries, commit status checks, and output variables. Requires 
  `GITHUB_TOKEN`
   for checks and PR features.
- ****Generic CI****
  Prints summaries, checks, and outputs to stdout. Useful for local development and testing, or any CI provider without native integration.

:::note Looking for our old GitHub Actions?
The Cloud Posse `cloudposse/github-action-atmos-terraform-*` actions have been [deprecated](/deprecated/github-actions) in favor of native CI. Existing workflows still function, but new projects should use the patterns above.
:::

## Related

- [CI Configuration](/cli/configuration/ci) - Configure CI integration in `atmos.yaml`
- [CI Commands](/cli/commands/ci) - CI command reference
- [Profiles](/cli/configuration/profiles) - Configure CI-specific profiles
- [Auth](/stacks/auth) - Configure OIDC authentication for CI
