Skip to content

Commit 2c6912a

Browse files
ostermanclaudeaknysh
authored
docs(ci): document github/artifacts planfile runtime-token requirement + E2E test (#2649)
* docs(ci): document github/artifacts planfile runtime-token requirement The github/artifacts planfile store talks to the GitHub Actions Artifacts API directly and needs ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL, which GitHub withholds from `run:` steps. That requirement was undocumented and untested end-to-end. - Add a "Using GitHub Artifacts in GitHub Actions" section to the planfile storage docs, leading with the actions/github-runtime helper (mode: env), and cross-reference it from the component docs. - Generalize the in-repo actions/github-runtime action (action.yml + README) so it advertises both `atmos ci cache` and `atmos terraform` (github/artifacts planfile storage) — the JS action already re-exposes all ACTIONS_* generically. - Add a GitHub Actions E2E workflow exercising the upload -> list -> download round-trip against the real Artifacts API (the one path no Go unit test can cover, since the runtime token only exists inside a runner). - Align a cache-centric comment in the github artifact store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ci): make planfile-artifacts E2E assert on download round-trip The first run proved the upload works (the runtime creds surface via github-runtime mode:env, and a fresh planfile artifact lands in the run), but the assertion was wrong: `planfile list` renders artifacts without opening each zip's metadata.json, so STACK/COMPONENT columns are empty and an unfiltered list dumps every repo artifact — `grep mycomponent|prod` could never match, and it blocked the real round-trip step from running. Make `planfile download` (deterministic key from component+stack+SHA) the gate, and demote `list` to informational. The harmless 403 WARNs from the --ci check-run/PR-comment hooks are expected (we don't grant statuses/PR write scope) and do not fail the plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ci): split planfile E2E into separate plan and apply jobs Model the real plan-then-apply CI workflow across two jobs in one run: - plan job: `atmos terraform plan --ci` uploads the planfile to GitHub Actions Artifacts (creds surfaced via the github-runtime action, mode env). - apply job (needs: plan): downloads that artifact cross-job via the named "github" store and runs `atmos terraform apply --planfile=<downloaded>`, applying exactly the reviewed plan with no re-plan. Root-cause of the prior single-job failure: the --ci upload hook auto-detects the runner and writes with prefix "planfile" to GITHUB_REPOSITORY (confirmed via the artifacts API: planfile-prod--mycomponent--<sha>.tfplan.tar), ignoring the named-store config. The dedicated fixture's "github" store now mirrors that (prefix=planfile, no owner/repo) so the readback key matches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ci): download github/artifacts planfiles via runtime API for same-run reuse The github/artifacts store downloaded only through the REST API (GET /artifacts/{id}/zip), which GitHub does not serve while the producing run is still in progress. That broke same-run plan-then-apply: a planfile uploaded by a plan job could not be downloaded by a later apply job in the same run ("zip: not a valid zip file"). Add a runtime-API download path mirroring the existing upload client: a GetSignedArtifactURL Twirp call (scoped to the current run's backend IDs from ACTIONS_RUNTIME_TOKEN) returns a signed blob URL that serves the in-progress run's artifact. Download now prefers the runtime API when runner credentials are present and falls back to REST for artifacts from other (completed) runs, so both same-run and cross-run handoffs work. Adds unit tests for the runtime download path, the REST fallback, and the GetSignedArtifactURL Twirp call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): resolve same-run planfile downloads across jobs via runtime ListArtifacts The first runtime-download attempt called GetSignedArtifactURL with the downloading job's backend IDs, which only addresses artifacts uploaded by that same job. A planfile uploaded by an earlier plan job therefore wasn't found, the store fell back to REST, and REST can't read an in-progress run ("zip: not a valid zip file"). Mirror actions/download-artifact: first ListArtifacts (run-scoped, across all jobs) to find the artifact and the backend IDs of the job that uploaded it, then GetSignedArtifactURL with those IDs. Both runtime Twirp calls share a small postRuntimeJSON helper. Cross-job, same-run plan-then-apply now works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ci): automatic, configurable planfile drift verification on deploy Make native-CI planfiles work end-to-end. When planfile storage is configured and `atmos terraform deploy` runs under CI, Atmos now automatically downloads the stored plan, re-plans, compares them (semantic JSON plan-diff), and applies the verified plan — failing on drift by default. Previously `deploy --ci` downloaded the stored plan and then ignored it unless `--verify-plan` was passed. - Config: `components.terraform.planfiles.verify` (fail | warn | off; default fail under CI). Per-run overrides `--verify-plan` / `--no-verify-plan`. Precedence: CLI > config > CI default. `warn` proceeds on drift; `off` skips verification and the stored-plan download. - `planfile.ResolveVerifyMode` resolves the mode at the download hook and the deploy gate; `VerifyPlanfile` takes the mode, with the post-diff decision in `finalizeVerification` (unit-tested fail/warn/match). - Verification stays on `deploy`, not `apply`: deploy re-runs plan (so a fresh plan exists to diff against), apply never re-plans. Docs explain why a naive diff fails (plans legitimately vary between review and apply) and why semantic comparison is the material improvement. Adds a separate planfile-verify-e2e workflow, a blog post, and a roadmap entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): clarify deploy reconciles a fresh plan rather than replaying the stored one Address review feedback: the default deploy flow does not apply the stored binary planfile — it generates a fresh plan with the apply-time identity and state, reconciles it against the reviewed plan via the diff, and applies the fresh plan only on a match. This avoids the most common saved-plan failure (the role that created the plan differing from the role applying it). Direct replay of the stored planfile remains available via --from-plan / --planfile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): simplify the "reconcile, don't replay" blog section Tighten the wording per review feedback — same point (deploy re-plans and applies the fresh plan rather than replaying the stored binary, avoiding the plan-time vs apply-time role mismatch), said in far fewer words. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): correct the saved-plan failure rationale (encoded role vs base creds) Terraform encodes the assumed role (provider assume_role) into the binary plan, so "a plan built under one role won't apply under another" was imprecise. The real brittleness of replaying a saved plan is that it goes stale when state moves, and that the base credentials authenticating it come from the apply environment, not the plan — so a plan built on a PR can fail to apply on merge. Reconciling via a fresh plan avoids both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): fix the "frozen artifact" contradiction in the planfile blog A planfile IS a frozen artifact, so opening with "a plan isn't a frozen artifact" was self-contradictory. Reframe: the planfile is a frozen snapshot, but the plan it represents never is — details shift between review and apply and you adjust course without changing what you set out to do, which is exactly why a naive diff fails and a semantic one is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): trim wordy 'instead of perpetually red' from planfile blog Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ci): cover runtime download error paths for github/artifacts store Add unit tests for downloadViaRuntime (invalid token, artifact-not-found, signed-URL error, empty URL, blob failure), fetchBlob (non-200, transport error), and ListArtifacts JSON-decode error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ci): configurable on_missing planfile verification; self-asserting E2E Make native-CI planfile verification carry its own contract so the E2E workflow no longer needs `--ci` flags or log-grep scaffolding. - Define `components.terraform.planfiles.on_missing` (fail|warn|off) for the previously-undefined "no stored plan found" case. Defaults to tracking the resolved `verify` mode, so a fail-by-default CI deploy fails loudly instead of silently applying an unverified fresh plan. A green deploy now proves verification ran. Adds ResolveMissingMode + handleMissingStoredPlan and the ErrStoredPlanfileMissing sentinel. - Strip planfile-verify-e2e.yml: drop redundant `--ci` (auto-detected), reduce the verify job to one line, and use the deploy's native non-zero exit for the drift job (no set +e / $? / grep). Add a `planfile list -f json` step that asserts the upload via structured output. - Add empirical list coverage: list_integration_test.go drives the real local filesystem store through createStore -> store.List() across component/stack/ SHA/all filters, asserting contents by value. - Docs: new "Missing stored plan" section; update deploy + planfile-storage pages; amend the planfile blog and roadmap milestone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ci): model planfile missing-plan policy as `required: *bool` The missing-stored-plan condition is binary ("must a reviewed plan exist?"), not a 3-way gradient, so the `on_missing: fail|warn|off` enum from dcd26d4 is replaced with `components.terraform.planfiles.required` (*bool), matching Atmos's established `Required` idiom (SecretDeclaration.Required, CommandArgument.Required). - schema: PlanfilesConfig.OnMissing (enum) -> Required (*bool); nil = unset. - planfile.ResolveMissingMode -> IsPlanRequired: explicit required wins; unset tracks verify strictness (required only when verify resolves to fail); verify=off short-circuits to not-required (also covers --no-verify-plan). - handleMissingStoredPlan now fails when a plan is required, else logs+proceeds. - The boolean inherently closes the prior "invalid enum silently disables the guard" gap (a *bool can't be invalid). - Tests, docs, blog, and roadmap updated from on_missing -> required. Note: the planned fetch-error-vs-absent hardening is deferred. CI hook errors are swallowed by design at two layers (executor.go and runCIHooksForDeploy), so making a download fetch-failure fatal needs the hook to signal the failable deploy path via shared info rather than returning an error -- a separate change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): correct planfile list assertion in E2E (--format, length check) The `List the uploaded planfile` step failed with "unknown shorthand flag 'f'": `planfile list` registers `--format` with no `-f` shorthand. It also keyed JSON objects by uppercase column headers (COMPONENT/STACK/...), so the prior `select(.component==...)` would never have matched even with the flag fixed. Verified locally against a real local-store upload: switch to `--format=json` and assert `jq -e 'length > 0'`. The command already filters by component+stack +SHA, so a non-empty array proves the upload landed; an absent planfile renders `null`, which `length > 0` correctly fails on. This is casing-independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ci): single tri-state --verify-plan flag; cover deploy verify paths Replace the negated --no-verify-plan flag with a tri-state --verify-plan (--verify-plan / --verify-plan=false), matching the --identity/--pager Changed()+viper.IsSet() pattern. Drop ATMOS_TERRAFORM_NO_VERIFY_PLAN; disabling is now ATMOS_TERRAFORM_VERIFY_PLAN=false. TerraformRunOptions.VerifyPlan becomes *bool; extract a testable resolveVerifyPlanMode(cmd, viper) helper. Add tests to lift patch coverage past the 80% gate: PlanfileVerifyMode.IsValid, VerifyPlanCLIOverride + tri-state parsing, resolveVerifyPlanMode, verifyStoredPlanForDeploy (self-contained temp atmos project), onBeforeDeploy download gating, and postRuntimeJSON error branches. Fix hardcoded /tmp planfile paths in finalizeVerification tests to use t.TempDir(). Update deploy docs, env-var reference, planfile/CI docs, blog, and PRDs to --verify-plan=false. * fix(ci): resolve --verify-plan without viper.IsSet (fixes drift-fails E2E) The tri-state --verify-plan resolution used viper.IsSet("verify-plan"), but the flag binding registers a default via viper.SetDefault, and viper.IsSet returns true whenever a default exists. So an unset flag was read as --verify-plan=false => PlanfileVerifyOff: the before.deploy hook skipped the stored-plan download and the missing-plan check resolved to "not required", letting a drifted deploy apply a fresh plan instead of failing. This broke the "deploy (drift fails)" E2E. Resolve the override from cmd.Flags().Changed plus a direct ATMOS_TERRAFORM_VERIFY_PLAN read, in both the PreRunE download-gate path and the RunE verify path. Drop the viper-based TerraformRunOptions.VerifyPlan / VerifyPlanCLIOverride. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): skip deploy planfile verification when storage is not configured verifyStoredPlanForDeploy ran on every `terraform deploy`, so a deploy of a component with no planfile storage configured emitted a spurious "No stored planfile found to verify; applying a fresh plan without verification" warning (and, in CI, could resolve the missing-plan requirement). This broke the atmos_terraform_deploy_locked_component golden snapshot. Gate verification on planfile.StorageConfigured, mirroring the before.terraform.deploy download hook's existing storage check: verification is opt-in via planfile storage, so without it deploy proceeds untouched. Add a regression test plus storage to the verifyStoredPlanForDeploy test fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): correct misleading "apply never re-plans" claim `terraform apply` with no planfile still computes a plan internally, so "applies an existing plan or config directly and never re-plans" was inaccurate. Reframe the deploy-vs-apply distinction around what Atmos captures: `deploy` runs a discrete plan step that produces a fresh planfile to diff against the stored one, while `apply` either applies a planfile passed via --planfile or plans-and-applies in one step — so Atmos has no separately-captured fresh plan to verify against. Fixes the apply command docs, the planfile-verification blog post, and the PRD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(flags): add IsBoolFlagExplicitlySet for tri-state flag detection resolveVerifyPlanMode read os.LookupEnv("ATMOS_TERRAFORM_VERIFY_PLAN") and the raw cobra flag directly, hardcoding the env var name in cmd/terraform and bypassing the pkg/flags infrastructure. Move the tri-state detection into the flags layer: StandardParser.IsBoolFlagExplicitlySet(cmd, name) checks cmd.Flags().Changed for the CLI value and falls back to os.LookupEnv over the flag's registered env vars (from the parser registry, so the env var name lives only in the deploy.go flag definition). This deliberately avoids viper.IsSet, which returns true for any key with a registered SetDefault — the false positive that previously collapsed the unset case to off and disabled verification by default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): address CodeRabbit review on planfile verification docs - terraform-deploy.mdx, planfiles.mdx, roadmap.js: say "applies the fresh plan after verification" instead of "the verified plan" — the reconcile applies the freshly generated plan, not a replay of the stored one. - environment-variables.mdx: move ATMOS_TERRAFORM_VERIFY_PLAN out of "Context Variables" (Atmos-populated shell context) into "Terraform Integration", where user/CI-provided config overrides belong. - roadmap.js: replace the non-existent `--no-verify-plan` with `--verify-plan=false`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
1 parent c61e38b commit 2c6912a

36 files changed

Lines changed: 2563 additions & 141 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# End-to-end test for the `github/artifacts` planfile store via the EXPLICIT CLI
2+
# (manual) path, across two separate jobs:
3+
#
4+
# plan job -> `atmos terraform plan --ci` uploads the planfile to GitHub
5+
# Actions Artifacts.
6+
# apply job -> explicitly `atmos terraform planfile download` the artifact
7+
# (cross-job, same run) and `atmos terraform apply --planfile=...`.
8+
#
9+
# The AUTOMATIC hook-driven flow (plan --ci upload -> deploy --ci auto download +
10+
# verify + apply) is tested separately in planfile-verify-e2e.yml.
11+
#
12+
# This is the one path no Go unit test can cover: the backend talks to the
13+
# GitHub Actions Artifacts API directly using the runner-only
14+
# ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL, which GitHub withholds from
15+
# `run:` steps. We dogfood the in-repo `actions/github-runtime` action
16+
# (mode: env) to surface them — exactly the wiring documented at
17+
# /ci/planfile-storage.
18+
#
19+
# The IaC binary is also dogfooded through the Atmos toolchain: the fixture
20+
# declares `dependencies.tools.opentofu` (+ `command: tofu`), so
21+
# `atmos terraform plan/apply` auto-installs OpenTofu and resolves it from the
22+
# toolchain PATH — no hashicorp/setup-terraform action required.
23+
name: Planfile Artifacts E2E
24+
25+
on:
26+
workflow_dispatch:
27+
pull_request:
28+
types: [opened, synchronize, reopened]
29+
paths:
30+
- 'pkg/ci/artifact/**'
31+
- 'cmd/terraform/planfile/**'
32+
- 'pkg/ci/plugins/terraform/**'
33+
- 'actions/github-runtime/**'
34+
- 'tests/fixtures/scenarios/planfile-artifacts-e2e/**'
35+
- '.github/workflows/planfile-artifacts-e2e.yml'
36+
37+
permissions:
38+
contents: read
39+
actions: read # Required for the GITHUB_TOKEN to list/download artifacts via the REST API.
40+
41+
concurrency:
42+
group: ${{ github.workflow }}-${{ github.ref }}
43+
cancel-in-progress: true
44+
45+
env:
46+
# Pin the SHA so the plan upload and the apply download derive the identical
47+
# planfile key across both jobs.
48+
ATMOS_CI_SHA: ${{ github.sha }}
49+
GITHUB_TOKEN: ${{ github.token }}
50+
FIXTURE_DIR: tests/fixtures/scenarios/planfile-artifacts-e2e
51+
52+
jobs:
53+
plan:
54+
name: plan (upload planfile)
55+
runs-on: ubuntu-latest
56+
steps:
57+
- name: Checkout
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
persist-credentials: false
61+
62+
- name: Set up Go
63+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
64+
with:
65+
go-version-file: "go.mod"
66+
67+
- name: Build atmos
68+
run: |
69+
make build
70+
echo "$PWD/build" >> "$GITHUB_PATH"
71+
72+
# Surface the runner's ACTIONS_* credentials to every later run step.
73+
# This is the entire reason github/artifacts works from a `run:` step.
74+
- name: Expose GitHub Actions runtime credentials
75+
uses: ./actions/github-runtime
76+
with:
77+
mode: env
78+
79+
- name: Plan and upload the planfile
80+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
81+
run: atmos terraform plan mycomponent -s prod --ci
82+
83+
apply:
84+
name: apply (consume planfile)
85+
needs: plan
86+
runs-on: ubuntu-latest
87+
steps:
88+
- name: Checkout
89+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
90+
with:
91+
persist-credentials: false
92+
93+
- name: Set up Go
94+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
95+
with:
96+
go-version-file: "go.mod"
97+
98+
- name: Build atmos
99+
run: |
100+
make build
101+
echo "$PWD/build" >> "$GITHUB_PATH"
102+
103+
- name: Expose GitHub Actions runtime credentials
104+
uses: ./actions/github-runtime
105+
with:
106+
mode: env
107+
108+
- name: Download the planfile uploaded by the plan job
109+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
110+
run: |
111+
set -euo pipefail
112+
# The named "github" store carries prefix=planfile and resolves owner/repo
113+
# from GITHUB_REPOSITORY — matching exactly what the plan job's --ci upload
114+
# hook wrote (planfile-<stack>--<component>--<sha>.tfplan.tar in this run).
115+
# Artifacts are run-scoped, so the plan job's upload is visible here.
116+
atmos terraform planfile download mycomponent -s prod \
117+
--store=github -o "$RUNNER_TEMP/downloaded.planfile"
118+
test -s "$RUNNER_TEMP/downloaded.planfile"
119+
120+
- name: Apply the downloaded planfile
121+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
122+
run: |
123+
set -euo pipefail
124+
# Apply exactly the plan that was reviewed in the plan job — no re-plan.
125+
atmos terraform apply mycomponent -s prod --planfile="$RUNNER_TEMP/downloaded.planfile"
126+
echo "Round-trip OK: planfile uploaded by the plan job was applied by the apply job."
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# End-to-end test for the AUTOMATIC native-CI planfile flow on `deploy`.
2+
#
3+
# Distinct from planfile-artifacts-e2e.yml (which exercises the *manual* CLI:
4+
# `planfile download` + `apply --planfile`). This workflow exercises the
5+
# hook-driven automatic path:
6+
#
7+
# plan job -> `atmos terraform plan` auto-uploads the planfile, then
8+
# `atmos terraform planfile list` asserts it landed.
9+
# deploy-verify job -> `atmos terraform deploy` auto-downloads the stored plan,
10+
# runs the plan-diff, and applies the verified plan
11+
# (verification is on by default under CI).
12+
# deploy-drift job -> mutates the component so the fresh plan differs, then
13+
# asserts `deploy` FAILS on drift (verify mode fail).
14+
#
15+
# CI is auto-detected (CI/GITHUB_ACTIONS) and the fixture sets `ci.enabled: true`,
16+
# so the `--ci` flag is unnecessary — the commands behave natively and assert via
17+
# their own exit codes and structured output rather than grepping logs.
18+
#
19+
# The warn/off mode decisions are covered by unit tests (finalizeVerification,
20+
# ResolveVerifyMode); this workflow covers the real runner integration.
21+
#
22+
# Like the upload, the automatic download talks to the GitHub Artifacts runtime
23+
# API, so every job surfaces ACTIONS_RUNTIME_TOKEN/RESULTS_URL via the in-repo
24+
# github-runtime action. OpenTofu is dogfooded through the Atmos toolchain (the
25+
# fixture declares dependencies.tools.opentofu), so there is no setup-terraform.
26+
name: Planfile Verify E2E
27+
28+
on:
29+
workflow_dispatch:
30+
pull_request:
31+
types: [opened, synchronize, reopened]
32+
paths:
33+
- 'pkg/ci/artifact/**'
34+
- 'cmd/terraform/**'
35+
- 'internal/exec/terraform_verify_plan.go'
36+
- 'internal/exec/terraform_plan_diff*.go'
37+
- 'pkg/ci/plugins/terraform/**'
38+
- 'actions/github-runtime/**'
39+
- 'tests/fixtures/scenarios/planfile-artifacts-e2e/**'
40+
- '.github/workflows/planfile-verify-e2e.yml'
41+
42+
permissions:
43+
contents: read
44+
actions: read # GITHUB_TOKEN reads artifacts via the REST API (fallback path).
45+
46+
concurrency:
47+
group: ${{ github.workflow }}-${{ github.ref }}
48+
cancel-in-progress: true
49+
50+
env:
51+
# Pin the SHA so the plan upload and the deploy download derive the identical
52+
# planfile key across jobs.
53+
ATMOS_CI_SHA: ${{ github.sha }}
54+
GITHUB_TOKEN: ${{ github.token }}
55+
FIXTURE_DIR: tests/fixtures/scenarios/planfile-artifacts-e2e
56+
57+
jobs:
58+
plan:
59+
name: plan (upload planfile)
60+
runs-on: ubuntu-latest
61+
steps:
62+
- name: Checkout
63+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
64+
with:
65+
persist-credentials: false
66+
- name: Set up Go
67+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
68+
with:
69+
go-version-file: "go.mod"
70+
- name: Build atmos
71+
run: |
72+
make build
73+
echo "$PWD/build" >> "$GITHUB_PATH"
74+
- name: Expose GitHub Actions runtime credentials
75+
uses: ./actions/github-runtime
76+
with:
77+
mode: env
78+
- name: Plan and upload the planfile
79+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
80+
# No --ci: GitHub Actions is auto-detected (CI/GITHUB_ACTIONS) and the
81+
# fixture sets ci.enabled, so the upload hook runs natively.
82+
run: atmos terraform plan mycomponent -s prod
83+
- name: List the uploaded planfile
84+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
85+
# Exercises the github/artifacts List() against the real runner API and
86+
# proves the upload landed, asserting the command's structured JSON output
87+
# (not grepping log prose). The command already filters by component+stack
88+
# +SHA, so a non-empty array means the planfile is there; an absent one
89+
# renders `null`, which `length > 0` fails on.
90+
run: |
91+
set -euo pipefail
92+
atmos terraform planfile list mycomponent -s prod --format=json \
93+
| jq -e 'length > 0' > /dev/null
94+
95+
deploy-verify:
96+
name: deploy (auto verify + apply)
97+
needs: plan
98+
runs-on: ubuntu-latest
99+
steps:
100+
- name: Checkout
101+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
102+
with:
103+
persist-credentials: false
104+
- name: Set up Go
105+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
106+
with:
107+
go-version-file: "go.mod"
108+
- name: Build atmos
109+
run: |
110+
make build
111+
echo "$PWD/build" >> "$GITHUB_PATH"
112+
- name: Expose GitHub Actions runtime credentials
113+
uses: ./actions/github-runtime
114+
with:
115+
mode: env
116+
- name: Deploy with automatic verification
117+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
118+
# No drift: the deploy re-plans the unchanged component, the plan-diff matches
119+
# the stored plan, and the verified plan is applied. No --ci, no log grep: a
120+
# green deploy *proves* verification ran, because a missing stored plan now
121+
# fails loudly (planfiles.required defaults to tracking the verify mode,
122+
# which is fail under CI).
123+
run: atmos terraform deploy mycomponent -s prod
124+
125+
deploy-drift:
126+
name: deploy (drift fails)
127+
needs: plan
128+
runs-on: ubuntu-latest
129+
steps:
130+
- name: Checkout
131+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
132+
with:
133+
persist-credentials: false
134+
- name: Set up Go
135+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
136+
with:
137+
go-version-file: "go.mod"
138+
- name: Build atmos
139+
run: |
140+
make build
141+
echo "$PWD/build" >> "$GITHUB_PATH"
142+
- name: Expose GitHub Actions runtime credentials
143+
uses: ./actions/github-runtime
144+
with:
145+
mode: env
146+
- name: Induce drift and expect deploy to fail
147+
working-directory: tests/fixtures/scenarios/planfile-artifacts-e2e
148+
# Native exit code is the contract: deploy exits non-zero on drift
149+
# (ErrPlanVerificationFailed), so an inverted check needs no --ci, no
150+
# captured `$?`, and no log grep.
151+
run: |
152+
set -euo pipefail
153+
# Mutate the component so the fresh plan differs from the stored plan.
154+
sed -i 's/test = "test"/test = "drifted"/' components/terraform/mock/main.tf
155+
156+
if atmos terraform deploy mycomponent -s prod; then
157+
echo "ERROR: expected drift to block the deploy, but it succeeded."
158+
exit 1
159+
fi
160+
echo "Drift correctly blocked the deploy."

actions/github-runtime/README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# Atmos GitHub Runtime action
22

33
Exposes the GitHub Actions **runtime credentials** (`ACTIONS_RUNTIME_TOKEN`,
4-
`ACTIONS_RESULTS_URL`, …) to your `run:` steps so the Atmos-managed cache backend
5-
(`atmos ci cache restore` / `save`) can reach the GitHub Actions cache service.
4+
`ACTIONS_RESULTS_URL`, …) to your `run:` steps so the Atmos backends that talk
5+
to the runner's cache and artifacts services can reach them:
6+
7+
- **Cache**`atmos ci cache restore` / `save` (GitHub Actions cache service).
8+
- **Planfile storage**`atmos terraform plan --ci` / `atmos terraform deploy`
9+
/ `atmos terraform planfile upload`|`download` when the `github/artifacts`
10+
store is selected (GitHub Actions Artifacts service).
11+
12+
Both read the same two env vars, so this single action serves both.
613

714
## Why this exists
815

@@ -56,6 +63,23 @@ The runtime token is always masked with `::add-mask::` regardless of mode.
5663
run: atmos ci cache save
5764
```
5865

66+
### Planfile storage (`github/artifacts`)
67+
68+
The same credentials let the [`github/artifacts` planfile store](https://atmos.tools/ci/planfile-storage)
69+
upload/download planfiles from a `run:` step. With `mode: env` no per-step wiring is needed —
70+
`atmos terraform` reads the credentials from the environment automatically:
71+
72+
```yaml
73+
- uses: cloudposse/atmos/actions/github-runtime@v1 # pin to a release or SHA
74+
with:
75+
mode: env
76+
- run: atmos terraform plan mycomponent -s prod --ci # uploads to github/artifacts
77+
- run: atmos terraform deploy mycomponent -s prod --ci # downloads & verifies the planfile
78+
```
79+
80+
Prefer `mode: output` (the default) when you want to scope the credentials to only these steps via
81+
explicit `env:`, exactly as in the cache examples above.
82+
5983
## Versioning
6084

6185
This action ships **inside the Atmos repository**, so the ref is an Atmos

actions/github-runtime/action.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
#
33
# Exposes the GitHub Actions runtime credentials (ACTIONS_RUNTIME_TOKEN,
44
# ACTIONS_RESULTS_URL, ...) that the runner injects only into *action* steps,
5-
# so a following `run:` step can use `atmos ci cache restore/save` (whose GitHub
6-
# backend reads them from the environment).
5+
# so a following `run:` step can use any Atmos command whose GitHub backend
6+
# reads them from the environment, namely:
7+
# - `atmos ci cache restore/save` (GitHub Actions cache service)
8+
# - `atmos terraform plan --ci` / `atmos terraform deploy` / `atmos terraform
9+
# planfile upload|download` with the `github/artifacts` planfile store
10+
# (GitHub Actions Artifacts service).
711
#
812
# Two modes (the runtime token is always masked with ::add-mask:: either way):
913
# - output (default): expose credentials as masked *step outputs*. You thread
@@ -13,7 +17,7 @@
1317
#
1418
# See actions/github-runtime/README.md for the security tradeoff and examples.
1519
name: 'Atmos GitHub Runtime'
16-
description: 'Expose GitHub Actions runtime credentials to run steps so `atmos ci cache` can reach the cache service.'
20+
description: 'Expose GitHub Actions runtime credentials to run steps so `atmos ci cache` and the `atmos terraform` github/artifacts planfile store can reach the runner cache & artifacts services.'
1721
author: 'Cloud Posse'
1822
branding:
1923
icon: 'unlock'
@@ -29,7 +33,7 @@ outputs:
2933
runtime-token:
3034
description: 'ACTIONS_RUNTIME_TOKEN (masked). Set ACTIONS_RUNTIME_TOKEN from this in the consuming step.'
3135
results-url:
32-
description: 'ACTIONS_RESULTS_URL — the cache service v2 base URL used by save/restore.'
36+
description: 'ACTIONS_RESULTS_URL — the cache & artifacts service v2 base URL used by cache save/restore and the github/artifacts planfile store.'
3337
cache-url:
3438
description: 'ACTIONS_CACHE_URL — legacy v1 cache URL, when present.'
3539
runtime-url:

cmd/terraform/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func init() {
121121
flags.WithBoolFlag("affected", "", false, "Deploy the affected components in dependency order"),
122122
flags.WithBoolFlag("all", "", false, "Deploy all components in all stacks"),
123123
flags.WithBoolFlag("ci", "", false, "Enable CI mode for automated pipelines (writes job summary, outputs)"),
124-
flags.WithBoolFlag("verify-plan", "", false, "Verify stored planfile matches current state before applying"),
124+
flags.WithBoolFlag("verify-plan", "", false, "Force planfile drift verification before applying, overriding config (use --verify-plan=false to disable)"),
125125
flags.WithEnvVars("deploy-run-init", "ATMOS_TERRAFORM_DEPLOY_RUN_INIT"),
126126
flags.WithEnvVars("from-plan", "ATMOS_TERRAFORM_DEPLOY_FROM_PLAN"),
127127
flags.WithEnvVars("planfile", "ATMOS_TERRAFORM_DEPLOY_PLANFILE"),

0 commit comments

Comments
 (0)