github cloudposse/atmos v1.226.0

latest releases: v1.227.0-test.2, v1.227.0-test.1, v1.227.0-test.0...
2 hours ago
refactor(lint): go-native Mage tooling + goimports→gci formatter speedup @osterman (#2955)

What

Replaces the bash staleness-check/build orchestration for custom-gcl, lintroller, and
gomodcheck with Go-native Mage targets under magefiles/, invoked via
go tool mage <target> (Go 1.24+ tool directive — zero global install required). Also swaps the
goimports import formatter for gci, cutting full-repo formatter time by ~15-20x.

  • .atmos.d/lint.yaml's custom-gcl/lintroller/gomodcheck/changed subcommands now each
    delegate to a one-line go tool mage lint:<target> shell step instead of a ~15-20 line bash
    staleness-check block.
  • scripts/run-custom-golangci-lint.sh (per-worktree cache/lock isolation, staged-patch vs
    --new-from-rev branching for the pre-commit hook) is fully ported to
    magefiles/mage_lint_golangci_run.go and deleted.
  • .pre-commit-config.yaml's golangci-lint/gomodcheck hooks now call
    go tool mage lint:precommit / go tool mage lint:goModCheck.
  • .golangci.yml gets build-tags: [mage] so the new build tooling is linted too, with scoped
    exclusions for forbidigo/lintroller (dev tooling outside the Atmos CLI/UI runtime — same
    precedent already used for tools/gomodcheck/main.go).
  • .github/workflows/codeql.yml's custom-gcl build step also moved onto go tool mage.
  • .golangci.yml's formatters section swaps goimports for gci, with explicit import-section
    ordering (standard, default, prefix(github.com/cloudposse/atmos), custom-order: true).
    Benchmarked directly on this repo (custom-gcl fmt -d, two runs each, warm cache both ways):
    goimports ~95-108s real vs. gci ~5-7s real. gci is a stock golangci-lint v2 formatter, so
    no .custom-gcl.yml plugin change is needed. CLAUDE.md and the lint-fix/test-coverage-fix
    agent docs are updated to name gci instead of goimports.

The historical "building custom-gcl in a pre-commit hook corrupts worktrees" invariant (hook only
ever checks + fails fast, never builds) is preserved exactly — see the comment on
Lint.Precommit in magefiles/mage_lint_precommit.go.

Why

  • Less bash: the staleness checks used find -newer/[ -nt ], which are POSIX-only and
    silently never worked on Windows (this repo's Cross-Platform requirement is MANDATORY per
    CLAUDE.md). The Go port fixes this for free via os.Stat().ModTime() comparisons, and also
    fixes the per-worktree cache isolation to set TMP/TEMP on Windows (Go's os.TempDir()
    ignores TMPDIR there), which the bash version never handled.
  • gomodcheck previously had two different implementations (a staleness-cached binary in the
    atmos-command path, a plain go run in the pre-commit-hook path). Unified onto go run -C tools/gomodcheck . <go.mod> for both — go run already benefits from GOCACHE, so the cached
    binary bought little for the extra code.
  • goimports was a measurable bottleneck in local and CI lint runs; gci performs the same
    import-ordering job in a fraction of the time with an equivalent (arguably clearer, since it's
    explicit) section-ordering configuration.

Verification

  • go build ./..., go vet ./..., go vet -tags=mage ./magefiles/... all pass.

  • All customGCL staleness branches (missing binary / config newer / plugin source newer)
    manually triggered and confirmed correct.

  • lint:precommit fail-fast path: deleted ./custom-gcl, confirmed the same error message,
    nonzero exit, and — the most important regression check — that ./custom-gcl is not
    created as a side effect.

  • Staged-patch branch and --new-from-rev branch both exercised directly; MERGE_HEAD-present
    branch verified via a simulated merge state.

  • Full pre-commit hook run end-to-end through the real pre-commit framework (binary-missing
    failure path and binary-present passing path).

  • atmos lint lintroller/atmos lint gomodcheck/atmos lint custom-gcl all verified through the
    real atmos binary with correct exit-code propagation.

  • atmos test (full suite) passes.

  • goimportsgci swap benchmarked directly: ./custom-gcl fmt -d (diff mode, no writes)
    against the full repo, two runs per formatter with a warm filesystem cache both directions to
    rule out a cold-cache artifact:

    Full-repo custom-gcl fmt -d (diff mode, no writes), two runs each:

    Formatter Run 1 Run 2
    goimports (old) 107.6s real 94.9s real
    gci (new) 5.3s real 6.9s real

    ./custom-gcl formatters confirms gci is active and goimports disabled after the swap.

  • This PR's own commits went through the new pre-commit hook wiring live (go-fumpt,
    golangci-lint via go tool mage lint:precommit, gomodcheck) and passed cleanly.

No user-visible behavior change — this is internal dev/CI tooling only.

Centralized auth guide, GitHub CLI import auth, and caching fixes @osterman (#2923)

what

  • New tutorial website/docs/tutorials/centralized-auth-config.mdx: centralize an organization's Atmos auth: config in one private repo and import: it into every project, with side-by-side AWS/Azure/GCP examples.
  • Fixed CustomGitDetector.resolveToken to fall back to gh auth token (GitHub CLI) for private git:: imports, matching the fallback already used for HTTPS/API GitHub fetches.
  • Fixed a silent import-failure mode: a broken import: entry (typo'd ref, unreachable host, unauthenticated private repo) now warns by default instead of continuing silently with an empty configuration and exit code 0.
  • Fixed a credential-leak bug: the new failure warning (and a related pre-existing log in the local-file adapter) could leak credentials embedded in import URLs; both now sanitize the path before logging.
  • Unified imports.ttl caching across every remote import form. It previously covered only git:: imports that use a subdirectory; it now also covers plain remote URLs and git:: imports without a subdirectory, without touching the shared pkg/cache package's behavior for its other (unrelated) consumers.
  • Supporting docs: website/docs/cli/configuration/imports.mdx (caching section), changelog post website/blog/2026-08-11-remote-import-github-auth-and-caching.mdx (written in ASD-STE100 style), a roadmap milestone, and fix-log records under docs/fixes/.

why

  • Developers commonly distribute AWS SSO access as ad hoc [profile] blocks pasted in Slack or wiki pages, which drift stale, don't scale to multi-cloud orgs, and have no audit trail or single source of truth. The tutorial documents Atmos's centralized-import pattern for this exact use case.
  • Field-testing that tutorial surfaced three real Atmos bugs, not just doc gaps: private git:: imports didn't actually get GitHub CLI auth, a broken import failed with zero visibility, and remote imports had inconsistent (in one path, nonexistent) caching. Fixed all three in code instead of documenting them as known limitations.
  • Review caught a credential-leak risk in the new warning log; fixed and covered with a regression test that's confirmed to fail without the fix (verified by temporarily reverting it).

references

  • N/A
Shared per-stack networking for containers, emulators & run steps @osterman (#2942)

what

  • Container components, one-shot atmos container run containers, and stack-scoped workflow type: container, action: run steps now automatically join a shared per-stack Docker/Podman network and resolve each other by a <stack>-<component> DNS alias — no configuration required, similar to the default network docker compose creates for a project. Emulator components join the same network, so a container and an emulator in one stack can resolve each other too.
  • Broadens the emulator's job-container network detection to any containerized run (not just GITHUB_ACTIONS), fixing connection refused when reproducing a socket-mounted job container locally.
  • Fixes the Native CI atmos terraform test summary silently dropping its results table (only badges and the repro command rendered) when per-run output lines weren't captured.
  • Adds the blog post and roadmap update for the new networking capability.

why

  • Container components had no automatic networking — every container landed on the default bridge with no inter-container DNS, so nothing in a stack could resolve a sibling service by name without hand-wiring a Docker network. This was a real functionality gap relative to what emulator components already had.
  • The emulator job-container networking fix and the CI summary fix were uncovered while investigating and testing that same networking code path, and share enough surface area with the main feature to land together.

references

  • N/A
ci(test): shard acceptance tests 10-way per OS to cut CI runtime @osterman (#2940)

what

  • Split the acceptance-test job into 10 parallel shards per OS (linux/windows/macos) instead of one ~60-90 minute job per OS.
  • tests/cli_test.go's TestCLICommands now deterministically assigns each CLI test case to a shard by hashing its name, gated by ATMOS_TEST_SHARD/ATMOS_TEST_SHARD_COUNT env vars (a no-op locally when unset - atmos test --full still runs everything).
  • .github/workflows/test.yml's test job matrix expands to flavor x shard(1..10). TestTerraformRegistryCache now runs once per OS (shard 1 only, guarded by matrix.shard == 1) instead of implicitly once per job. Added a test-required aggregator job (mirrors the existing k3s-required pattern) so branch protection can key off one stable check name regardless of shard count.
  • scripts/collect-coverage.sh writes to a configurable COVERAGE_OUT path and pins -covermode=atomic explicitly. The coverage job now downloads all 10 Linux shard coverage files and hands them to codecov-action in a single call so Codecov aggregates line hits server-side, instead of uploading one Linux-only coverage.out.

why

  • The acceptance suite's runtime was dominated by ~388 sequential CLI-driven subtests in the tests package (no t.Parallel()), run once per OS. Sharding spreads that work across parallel jobs so CI feedback lands in minutes instead of the better part of an hour.
  • Coverage collection had to change alongside sharding: a single Linux job previously produced one coverage.out; this keeps the aggregate coverage number correct once that work is spread across 10 files.

Branch protection: no admin action is needed. test-required's matrix check: values (Acceptance Tests (linux), Acceptance Tests (macos), Acceptance Tests (windows)) are the exact same check names the old per-OS test job produced, so it keeps those names live as compatibility aliases - each one only succeeds once every shard for that OS passes, and test-required now also gates on terraform-registry-cache.

references

  • N/A
chore: remove approvers team from CODEOWNERS requirements @johncblandii (#2938) ## what
  • Remove @cloudposse/approvers as a required owner from .github/CODEOWNERS — the four patterns
    that listed it (**/*.tf, README.yaml, README.md, docs/*.md) now require only
    @cloudposse/engineering

why

  • With require_code_owner_review enforced, any PR touching a matching file demands an approval
    from the approvers team in addition to engineering review. The bare README.md pattern matches
    READMEs anywhere in the tree, so even CI-only changes (e.g. #2934, which updates a README inside
    .github/actions/) get blocked on the extra team
  • Requested by Erik: drop the approvers requirement so engineering review alone suffices

references

  • #2934 (currently blocked on this requirement)
chore: upgrade actions to Node 24 runtime (SHA-pinned) @johncblandii (#2934)

what

  • Bump the last node20-era action pin to Node 24, SHA-pinned per this repo's convention:
    • golangci/golangci-lint-action@4afd733a # v8.0.0@ba0d7d2e... # v9.3.0 — safe here since
      the workflow uses install-mode: none with a custom-built golangci-lint from a Nov-2025 (v2-era)
      commit, which v9 supports
  • Re-pin the cloudposse/.github shared-workflow refs (shared-go-auto-release.yml,
    shared-release-branches.yml) from 8244c7c9 # main to current main 49ac8cd5 # main — the old
    pin predates cloudposse/.github#261, so release workflows were still running node20 action
    versions from the stale snapshot

why

  • GitHub is deprecating the Node 20 runtime; these were the remaining refs in this repo resolving
    to runs.using: node20
  • Verified: every changed SHA matches its upstream tag / branch head, this repo's own
    verify-sha-pinning test suite passes (28/28) over the modified tree, and actionlint is clean

references

still on Node 20

feat(profiles): interactive multi-select for bare --profile @osterman (#2951)

what

  • Bare --profile (no value) now opens an interactive multi-select of every discovered profile, mirroring bare --identity's existing selector, instead of failing with a raw pflag "flag needs an argument" error.
  • Extends pflag's NoOptDefVal bare-flag mechanism — previously hardcoded to StringFlag only — to also work for StringSliceFlag, across the flag registry, parser, and preprocessing pipeline.
  • Adds a ProfileSelector dependency-injection seam in pkg/config, fulfilled by pkg/flags at init() time, so the interactive picker can be wired in without creating an import cycle (pkg/flags and pkg/profile both already import pkg/config).
  • Explicit profile names typed alongside the bare flag (e.g. --profile ci --profile) are always preserved in the final selection. Non-interactive contexts (CI, scripts, no TTY) get a clear, actionable error instead of a confusing parse failure.
  • Docs, roadmap, and a changelog post updated to describe the new behavior.

why

  • --profile was a StringSliceFlag, and that flag type never supported the bare-flag sentinel pattern --identity already used — so atmos auth login -i <identity> --profile (expecting an interactive prompt like -i gives) instead errored with a confusing, low-level pflag message.
  • Users shouldn't need to memorize exact profile names or run atmos profile list first just to activate one.

references

N/A

feat(vendor): add --stack/--labels flags, fix --tags selector bugs @osterman (#1889)

what

  • Add --stack/-s and --labels selector flags to atmos vendor pull: vendor every component
    declared in a stack (or matching stack metadata.labels) that has its own component.yaml,
    bypassing vendor.yaml entirely for installation.
  • Extend --tags across all five vendor subcommands (pull, diff, clean, update, verify)
    to compose as an independent filter with --component or --stack/--labels, instead of being
    mutually exclusive with them — narrows whichever base selector resolved by vendor.yaml-declared
    source tags.
  • Fix four bugs found in a field-test pass of the above (see
    docs/fixes/2026-08-08-vendor-pull-selector-silent-failures.md):
    • vendor pull -c <component> --tags <tag> no longer silently exits 0 with nothing installed
      when the tag matches a different declared component instead of the named one.
    • An undeclared --component now reports "not defined" instead of a misleading tags-mismatch
      message when --tags also happens to match nothing.
    • Repeated -c/--component on vendor pull is now rejected instead of silently keeping only
      the last value (--component is now a slice, matching vendor update's flag).
    • --stack/--labels now warns when a resolved component also has a vendor.yaml entry,
      surfacing the risk that the two can install different content for the same target.
  • Also remediates 6 of 8 open Dependabot alerts encountered on this branch (go-git symlink/path
    traversal, dompurify XSS, nanoid DoS); the remaining 2 (image-size DoS) have no upstream
    patch available yet.

why

  • Feature request: vendor all components used by a stack or label selection without specifying
    each one individually via repeated --component invocations, and filter that selection further
    by declared tags.
  • The bug fixes came from a hands-on DX field-test pass of the flag-composition work — silent
    no-ops and silently dropped flags are the most dangerous class of CLI bug, since they look
    identical to success.

references

  • This PR originally also carried a registry-pattern refactor of vendor internals
    (internal/exec/vendor*.gopkg/vendor/). While this branch was in flight, origin/main
    independently shipped a more complete rewrite of the same internals into pkg/vendoring/
    (lockfile, SBOM/provenance, native component-updater PR workflow — #2756). That refactor has
    been dropped from this PR to avoid duplicating it; --stack/--labels/--tags are implemented
    directly on top of main's current pkg/vendoring/internal/exec architecture.
  • docs/fixes/2026-08-07-vendor-selector-flag-consistency.md — the --tags composition fix and
    its design iteration.
  • docs/fixes/2026-08-08-vendor-pull-selector-silent-failures.md — the four bug fixes above.
  • website/blog/2025-12-18-vendor-stack-flag.mdx
feat(scaffold): dynamic per-combination file generation via matrix @jorrite (#2928)

what

  • Adds spec.files[].matrix for dynamic per-combination file generation in scaffold templates: a file entry declaring matrix: expands into one generated file per combination of one or more axes, reusing the exact shape the workflow matrix: step already uses. when: prunes combinations that don't apply, evaluated once per resolved combination.
  • Each axis's list of values is a literal YAML list, a dot-path into answers.* referencing an already list-shaped answer, a free-text answer split via any Sprig/Gomplate function (e.g. splitList), or a Go-template expression computing the list from nested/structured answer data.
  • Adds a collectKeys template function for computed axes: collectKeys(m) returns m's sorted top-level keys; collectKeys(m, "nestedKey") collects nestedKey's own keys from every value in m, flattened and deduplicated. Registered under its own name so it doesn't shadow Sprig's own keys, and registered in every Go-template FuncMap Atmos builds — not just scaffold templates — so it's available anywhere Sprig/Gomplate functions are (stack configs, locals, store references, toolchain templates, etc).
  • A resolved combination is exposed as .matrix.<axis> in target:, in when: (via the matrix CEL variable), and in the file's own rendered content.
  • New examples/scaffolding-matrix example (one concept: a multiselect field driving a single matrix axis), keeping examples/scaffolding itself matrix-free. Comprehensive testing (all three axis kinds, plus deliberately-wrong inputs) moved to dedicated tests/fixtures/scenarios/scaffold-* fixtures rather than piggybacking on the end-user example.
  • Updates the atmos-scaffold/atmos-templates agent skills, the CLI reference docs, and the file-browser plugin so all three are current with matrix/collectKeys.
  • Adds a changelog post and roadmap entry.

why

spec.files[].when: gates whether a fixed-count file is generated — it can skip a file, never multiply it. matrix: is the mechanism for producing more than one file from a single entry, reusing conventions already familiar from Atmos workflow matrix: steps and CEL when: conditions. Some axes aren't list-shaped anywhere in the answers themselves — e.g. every region used by any environment — so collectKeys derives that list from nested/structured answer data instead of requiring template authors to hand-roll files outside the template.

references

Design discussed in https://github.com/orgs/cloudposse/discussions/126. See docs/prd/atmos-scaffold.md's "Dynamic File Generation (matrix)" section for the full behavior/validation/non-goals writeup.

feat(auth): add GKE kubeconfig integration @shirkevich (#2937)

Summary

Adds native GKE kubeconfig authentication as an Atmos Auth integration. This is the main-based continuation of #2901 after its dependency, #2790, merged and its source base branch was deleted; GitHub consequently prevents reopening or retargeting the original approved PR.

  • registers gcp/gke with required project_id, location, and cluster name
  • describes the exact projects/{project}/locations/{location}/clusters/{name} resource through the native GKE API using Atmos-issued GCPCredentials
  • reuses the shared kubeconfig writer for merge, replace, error, no-op, and cleanup behavior
  • adds atmos gcp gke token as the kubeconfig exec plugin without persisting bearer tokens
  • exposes KUBECONFIG and KUBE_CONFIG_PATH through the Auth identity environment
  • authenticates the upstream chain for provider-backed gcp/project identities
  • adds an opt-in Helm identity/endpoint guard with consistent cluster-contact semantics
  • includes generated schemas, CLI snapshots and casts, unit/Auth/Helm tests, a PRD, and CLI documentation

Helm guard contract

When a component sets auth.require_identity: true, Atmos resolves its default identity when --identity is omitted, requires a successfully provisioned GKE endpoint, and compares it with the effective Kubernetes REST configuration.

The guard applies consistently to every native Helm operation that contacts a Kubernetes cluster:

  • live/default helm plan and helm diff
  • explicit --against=release
  • helm apply / helm deploy
  • helm delete / helm destroy

Truly offline paths remain usable without authentication: helm template, --from-manifest, and non-cluster --against=target comparisons. The guard is disabled by default, is GKE-specific, and does not change AWS EKS or Azure AKS behavior.

auth:
  integrations:
    example-gke:
      kind: gcp/gke
      via:
        identity: example-deployer
      spec:
        cluster:
          name: example-cluster
          project_id: example-project
          location: us-central1
          alias: example
          kubeconfig:
            update: merge

components:
  helm:
    example-release:
      auth:
        require_identity: true
        identities:
          example-deployer:
            default: true

Architecture and security

The kubeconfig stores only the GKE endpoint, CA data, and an Atmos exec stanza. It never persists an OAuth bearer token. Cluster discovery and token output require the GCP credential type already produced by Atmos Auth.

The integration does not invoke gcloud, bootstrap Application Default Credentials itself, or require gke-gcloud-auth-plugin. Credentials may come from any configured Atmos GCP identity chain. The token command writes only Kubernetes ExecCredential JSON to stdout and excludes sensitive token material from errors.

The runtime identity needs permission to call container.clusters.get; Kubernetes authorization remains controlled by cluster RBAC.

Manual verification

The implementation was exercised end-to-end against a real regional GKE cluster using genericized evidence recorded in docs/prd/gke-kubeconfig-authentication.md:

  • first-use atmos auth exec provisioned kubeconfig without preparatory gcloud commands
  • kubectl reached all cluster nodes through atmos gcp gke token
  • kubeconfig contained endpoint, CA data, and the Atmos exec stanza, but no bearer token
  • the same flow passed with both gcloud and gke-gcloud-auth-plugin absent from PATH

Validation

  • go test ./pkg/component/helm -count=1
  • go test ./pkg/auth/... -count=1
  • go test ./internal/exec ./pkg/datafetcher -run 'Auth|GKE|GlobalAuth|Schema' -count=1
  • git diff --check origin/main...HEAD

All passed after replaying only the GKE commits onto current main. No inherited Azure implementation commits remain in this branch.

Prior review

#2901 was approved before its base branch was deleted. This replacement retains the same implementation and review fixes, adds the consistent live-plan guard described above, and links the original discussion for provenance.

docs(ci): document PR plan comments @osterman (#2939)

what

  • Make Terraform PR plan-summary comments discoverable from the Native CI overview and plan command documentation.
  • Correct the CI comments reference to describe GitHub-only, Terraform-plan-only, explicit opt-in behavior.

why

  • Users could not discover this capability in high-traffic Native CI docs, and the reference incorrectly claimed comments were enabled by default.
feat(toolchain): add update command, fix version-pinning bugs @osterman (#2894)

what

  • Adds atmos toolchain update [tool...] to move a pinned tool to its newest available version and reinstall it, with --dry-run and bounded --max-concurrency. Tools pinned to pr:/sha:/ref: are skipped with an explanation instead of silently left alone.
  • Fixes which/exec resolving the wrong version (last token instead of the default first token) on a multi-version .tool-versions line, which caused false "not installed" errors.
  • Fixes set appending instead of replacing the default version, contradicting its documented behavior.
  • Fixes add/install silently accepting SemVer range syntax (^1.2.0, ~>1.0.0) and only failing later with a raw HTTP 404; now rejected immediately with a hint toward dependencies.tools/atmos version track.
  • Fixes atmos version track add/set corrupting any value containing <, >, or & (a json.Marshal HTML-escaping bug), which broke the exact ~>/>= constraint syntax the toolchain docs recommend.
  • Fixes atmos toolchain versions --help silently rendering the wrong command's help and exiting 0 instead of erroring; fixed globally in root help routing (atmos <cmd> <bogus-subcommand> --help now errors for every command tree). Removes the stale toolchain-versions and toolchain-aliases docs/casts for commands that were never implemented.
  • Implements six previously documented-but-missing flags: list --format/--installed-only/--pending-only, clean --dry-run/--cache-only/--force, exec --dry-run.
  • Fixes updateToolVersionsFile writing to the hardcoded default .tool-versions path instead of the configured one.
  • Adds a changelog post and roadmap entry for the new update command.

why

  • A field test of atmos toolchain surfaced that there was no way to update a pinned tool to a newer version, and no clear signal for why range/constraint syntax (^1.2.0, ~>1.0.0) didn't work when the docs implied it should.
  • Live-testing the closest existing workaround (add <tool>@latest + install --reinstall) reproduced a real crash in which/exec, which led to finding the rest of the bugs above along the way — a documented set behavior that didn't match reality, a JSON-escaping bug corrupting exactly the constraint syntax the toolchain skill doc recommends, and a help-routing bug that let atmos toolchain versions --help silently succeed for a command that doesn't exist (which is also why its docs page and cast looked legitimate despite documenting nothing real).
  • Together these close the gap between what atmos toolchain's docs promised and what the CLI actually did, and give users a real, safe way to move a pinned tool forward.

references

  • Field test and fix session: this branch (osterman/toolchain-update-pinning-field-test)
feat(scaffold): add --merge-driver flag to force text-based merging @jorrite (#2925)

what

  • Adds a --merge-driver flag (auto / text) to atmos scaffold generate --update and atmos init --update, alongside the existing --merge-strategy flag.
  • auto (default) is today's existing behavior: pick the merger by file extension (YAML-aware for .yaml/.yml, line-oriented text otherwise).
  • text forces every file — YAML included — through the line-oriented diff3 merger, bypassing the YAML-aware re-encode that has no concept of blank lines between blocks and silently drops them on every update, even when nothing meaningful changed.
  • Adds a changelog post and roadmap entry for the new flag.

why

Structure-aware YAML merging is the right default for most files, but it re-encodes the whole document through a YAML parser/serializer, and formatting like blank lines between top-level blocks isn't part of what a YAML parser models. Templates that bundle CI pipeline YAML (a common convention uses blank lines to visually separate jobs/stages) lost that formatting on every --update, whether or not the file actually changed. --merge-driver=text gives users an explicit opt-out, mirroring git's own merge driver concept (auto/text), so this class of file can go through the same merge algorithm git merge itself uses on ordinary text files.

references

Closes #2886.

ci(test): make macOS k3s reliable by fixing the colima timeout budget @arcaven (#2936)

Why

[k3s-macos] is a required check (via [k3s] demo-helmfile) and flakes on unrelated PRs, costing the full step budget each time and blocking merge. The failure is entirely in CI setup: the job dies at "Start Docker-compatible runtime on macOS" without running a single helm test. Closes #2935.

What

The colima setup ladder was longer than the step cap that contained it, so the step was guillotined before the ladder could finish, and a status-propagation bug hid which rung actually failed. This PR makes the ladder shorter than its cap instead of growing the cap:

  • vz only; the qemu fallback is removed. docs/fixes/2026-07-01-macos-k3s-runner-research.md already selected vz for macos-15-intel and records qemu failing there on usernet unable to resolve IP for SSH forwarding before any test ran, and a green run (31723563772) confirms vz starting on attempt 1 with the whole setup step under 6 minutes. The qemu rung (plus its 7-minute brew install qemu cap) was a dead rung that only added minutes to the failure path. Two bounded vz attempts remain as the half-started-VM mitigation.
  • Fix failure propagation in start_colima. status=$? ran after the completed if, which reads the if statement's own zero when no branch executes, so the function returned success after every attempt had failed and the job died later at docker version with a misleading error. The status is now captured in the else branch.
  • docker pull rancher/k3s:latest moves out of the start/retry loop and retries on its own (two independent 5-minute attempts). In the loop, a slow or rate-limited Docker Hub pull counted as a runtime-start failure and forced a full VM delete + rebuild, so registry flakiness masqueraded as hypervisor flakiness.
  • Diagnostics are kept after every failed attempt, including the last, and bounded (colima status and limactl list capped at 30s so they cannot hang the step). Teardown and backoff run only between attempts; after the final failure the runner is discarded, so cleanup is dead time.
  • Step cap 40 minutes; the job-level 60-minute cap is unchanged, preserving fail-fast behavior; the observed healthy setup leaves ample time for both 15-minute test attempts.

Timeout arithmetic

Caps on the setup path: brew installs 7m + initial cleanup 1.5m + two vz start/info attempts 10m each + bounded diagnostics and intermediate cleanup ~3m + docker verify 1m + two 5-minute pull attempts with backoff. Every cap hit simultaneously sums to ~43 minutes; the 40-minute cap deliberately undercuts that. The recovery path fits when the final image pull completes normally; anything past 40 is treated as pathological. The observed healthy path is under 6 minutes.

Cost of merge

CI-only; no product code changes. The failure mode changes shape: instead of a slow crawl through a dead qemu rung, a runner where vz cannot start now fails hard after two bounded attempts, with diagnostics captured both times.

Out of scope (raised in #2935)

Pinning the k3s image would need a change to pkg/emulator/driver/k3s.go (the ref is hardcoded there), so this PR keeps :latest to stay CI-only. And macos-15-intel is on GitHub's deprecation path; a longer-term plan for [k3s-macos] is noted in the issue.

feat(auth): add Azure AKS/ACR integrations mirroring EKS/ECR @osterman (#2790)

what

  • Adds atmos azure aks token, atmos azure aks update-kubeconfig, and atmos azure acr login, mirroring the existing atmos aws eks/atmos aws ecr integrations.
  • Generalizes pkg/auth/cloud/kube.KubeconfigManager from AWS-specific to a cloud-agnostic writer shared by EKS and AKS, with a regression suite locking in byte-identical AWS output.
  • Widens the existing IntegrationSpec.Cluster/.Registry schema structs (renamed from EKSCluster/ECRRegistry to Cluster/Registry) so spec.cluster/spec.registry are reused verbatim across aws/eks+azure/aks and aws/ecr+azure/acr — no new per-cloud config keys.
  • Adds AKS-scoped AAD token acquisition to all three Azure identity providers (device-code, OIDC, Azure CLI), alongside their existing Graph/KeyVault token acquisition, since Azure AAD tokens are scope-bound at issuance (unlike AWS SigV4).
  • Adds docs (website/docs/cli/commands/azure/), a changelog post, a roadmap update, two new agent skills (atmos-azure-aks, atmos-azure-acr), and a PRD documenting the design (docs/prd/azure-aks-acr-integrations.md).
  • Remediates 4 open Dependabot alerts found on push: google.golang.org/grpc (xDS RBAC auth bypass / HTTP2 rapid-reset bypass), and three transitive website npm packages (fast-uri, svgo, dompurify).

why

  • Atmos already lets an AWS identity configure kubectl and Docker credentials in one step via atmos auth login. Azure had the same auth foundation (providers, identities) but no equivalent for AKS/ACR, so Azure users still needed the az CLI — and for AAD-enabled clusters, the separate kubelogin binary — outside of Atmos entirely.
  • This closes that gap using the same integration pattern, with no new external tool dependency: AKS cluster description parses the exec-format kubeconfig Azure returns and points the exec plugin at atmos azure aks token instead of kubelogin; ACR login is a plain OAuth2 token exchange, matching what az acr login does under the hood.

references

  • Design: docs/prd/azure-aks-acr-integrations.md
  • Precedent: EKS kubeconfig PRD (docs/prd/eks-kubeconfig.md), ECR authentication PRD (docs/prd/ecr-authentication.md)

manual testing

Exercised end-to-end against a live AAD-enabled AKS cluster (Azure CNI Overlay + Cilium, AAD + Azure RBAC, local accounts disabled) — the live path that PRD Success Metric #2 had previously left to unit tests only. This surfaced, and fixed, a registration gap.

Bug found + fixed. atmos azure aks update-kubeconfig --integration <name> failed with unknown integration kind: azure/aks. The pkg/auth/integrations/azure package self-registers azure/aks and azure/acr in its init(), but nothing blank-imported that package in pkg/auth/manager.go (unlike the aws and github integration packages), so init() never ran and the kinds never registered. The unit suites import the azure package directly, which registered the kinds incidentally and masked the missing production import. Fixed by adding the blank import alongside aws/github.

Integration mode — describe the cluster and write kubeconfig via the Go SDK (no az, no kubelogin):

$ atmos azure aks update-kubeconfig --integration dev/aks
✓ AKS kubeconfig: dev-aks → ~/.config/atmos/kube/config

$ export KUBECONFIG=~/.config/atmos/kube/config
$ kubectl config current-context
dev-aks

$ kubectl get pods -A
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   cilium-8trqv                      3/3     Running   0          134m
kube-system   coredns-5d474ff6db-pknhn          1/1     Running   0          132m
kube-system   metrics-server-5b879b45fc-5nxzs   2/2     Running   0          129m
...

The kubeconfig Atmos wrote drives its exec plugin through atmos azure aks token (not kubelogin), with --server-id discovered from the cluster (here the well-known AKS AAD server app):

$ kubectl config view --raw -o jsonpath='{.users[0].user.exec.command} {.users[0].user.exec.args}'
atmos [azure aks token --cluster-name aks-dev --resource-group rg-aks-cus \
       --server-id 6dae42f8-4368-4678-94ff-3960e28e3630 --subscription-id <redacted> --identity=dev]

auth exec mode — Atmos injects KUBECONFIG into the child process from the integration's Environment() (works even with auto_provision: false, which only suppresses the auto-write on login, not the env composition), so no manual export is needed:

$ atmos auth exec --identity dev -- kubectl get nodes
NAME                             STATUS   ROLES    AGE    VERSION
aks-system-64934532-vmss000000   Ready    <none>   139m   v1.35.6
aks-system-64934532-vmss000001   Ready    <none>   139m   v1.35.6

Both paths mint bearer tokens through atmos azure aks token against the Atmos-managed identity — no az CLI and no kubelogin binary. (ACR login against a live registry remains unit-test-only.)

feat(config): auto type inference + provenance/merge bug fixes @osterman (#2897)

what

  • atmos config set and atmos stack set now default --type to auto: infer from the Atmos config schema, then from the type of the value already at the path, falling back to a string (with a warning) only when neither source has an answer. atmos stack set previously never inferred at all — every value was stored as a string unless --type was passed explicitly.
  • Fixed PickProvenanceFile (shared by atmos stack set/get/delete/list and the AI/MCP tools layer) always picking the last provenance entry, which for any value defined only in an imported catalog file was a phantom Line:0 entry pointing at the wrong (importing) manifest instead of the file that actually defines the value.
  • Fixed MCPSettings.Enabled (a bool tagged omitempty) silently disappearing from the merged config when atmos.yaml and an atmos.d/ fragment both set it to different values, instead of the explicit value winning.
  • Fixed error hints containing a raw <placeholder> (e.g. pass --config <file>.) being silently stripped by the terminal markdown renderer, which parses unescaped angle brackets as inline HTML.
  • --config a.yaml,b.yaml on atmos config get/set/delete/format now warns that only the first file is targeted, instead of silently dropping the rest.
  • atmos config get on a key defined only in an atmos.d/ fragment now hints to check atmos describe config instead of just reporting "not found".
  • The unset alias (for config/stack delete) now shows up in --help output, alongside del.
  • Adds a blog post and roadmap entry for the type-inference change, and fixes a stale doc example that cited a non-existent config field.

why

  • A hands-on DX field-test pass of atmos config and atmos stack/atmos stack config surfaced these as real, reproducible bugs and gaps — silent type corruption, a provenance-resolution bug that broke edits for the standard catalog-import stack pattern, and a config value that could vanish entirely on merge.
  • These commands had effectively zero CLI-level test coverage before this PR; the fixes are backed by new regression tests reproducing each bug (including a second, independently-broken copy of the provenance bug found in the AI/MCP tools layer during the fix).

references

  • N/A
Add task-runner dependencies, freshness checks, and preconditions to custom commands and workflows @osterman (#2882)

what

  • Adds dependencies.commands/dependencies.workflows to custom commands and workflows: named, parameterized, concurrent-by-default dependency ordering across units, with automatic dedup of identical invocations.
  • Adds inputs/artifacts step fields: skip a step when its declared sources haven't changed since the last successful run (implicit when: checksum.changed), exposing checksum.changed/timestamp.changed/sources/artifacts as when: CEL facts.
  • Adds preconditions step field: skip a step when a required tool is already on PATH (implicit when: "!preconditions.success"), resolved via exec.LookPath — no shell involved. Pluralized (preconditionpreconditions) to match the block-of-checks convention already used by inputs/artifacts/dependencies.
  • Adds continue: always step field, mirroring GitHub Actions' continue-on-error: a step's own failure is forgiven, later steps still run, overall exit status unaffected.
  • Fixes type: parallel/type: matrix steps silently failing in custom commands (only workflows supported them before).
  • Adds platforms via when: CEL facts (os/arch/platform), native per-command aliases:/internal:, and a values: constraint on flags/arguments with an interactive picker.
  • Fixes Windows-specific bugs in the new step types: shell child-process argument quoting for parallel/matrix, and verbatim CmdLine construction for cmd.exe /C.
  • Fixes a cluster of concurrency and correctness bugs surfaced during implementation and review: dependency-scheduling and freshness-check race conditions, hashfile collisions, non-atomic multi-line step output, diamond-dependency de-duplication, wrong-binary resolution in workflow command dependencies, and a UnitDependencies string-shorthand schema gap.
  • Remediates 3 Dependabot security alerts surfaced while this branch was open: js-yaml, mermaid, and a nanoid infinite-loop DoS (GHSA-2v37-7h3g-55p8 / CVE-2026-67213).
  • Relocates cmd/custom_command_dependency_adapter.go and cmd/custom_command_values.go into pkg/taskgraph/adapters and pkg/flags respectively, so this logic is unit-testable in isolation instead of coupled to cmd's live command registry.
  • Adds Docusaurus docs for every new field/fact and updates the JSON Schema (atmos/manifest, config/global, stacks/stack-config) accordingly.

why

Atmos workflows and custom commands already covered most of what a task runner needs, but a handful of real gaps kept teams running go-task alongside Atmos: no dependency ordering between named commands/workflows, no up-to-date checking, no continue-on-error, no precondition shortcut, and custom commands couldn't even use parallel/matrix steps — the exact recipe the project's own go-task migration guide recommends for concurrent dependents. This closes those gaps using the existing when:/CEL condition engine and scheduler rather than inventing a second mechanism.

references

  • Blog post: website/blog/2026-08-05-taskfile-convergence.mdx
feat(provisioner): Azure (azurerm) backend auto-provisioning @aknysh (#2911)

what

Adds automatic provisioning for the azurerm Terraform state backend — the Azure counterpart to the existing S3 backend provisioner. When provision.backend.enabled: true on a component using an azurerm backend, Atmos creates the resource group (if missing), storage account, and blob container before terraform init, with opinionated secure defaults.

Previously, atmos terraform backend create returned create not implemented for backend type: azurerm and provision.backend.enabled silently skipped for Azure.

What gets created (hardcoded secure defaults)

  • Resource group in the identity's location (or reuses an existing group's location)
  • Storage account: StorageV2 / Standard_LRS, TLS 1.2 minimum, HTTPS-only, public blob access blocked
  • Entra ID hardening: shared-key access disabled when the backend sets use_azuread_auth: true
  • Blob versioning + 30-day soft delete (the S3-versioning analog)
  • Private container; Name + ManagedBy=Atmos tags

No lock resource is created — the azurerm backend serializes concurrent writes with native Azure Blob Storage blob leases (the DynamoDB / native-S3-locking analog, built into Blob Storage).

Design

  • Self-registers create/delete/exists/name into the shared backend registry via init(), so the before.terraform.init hook and atmos terraform backend create/delete pick up azurerm with no wiring changes to the hook or CLI.
  • A narrow azureBackendAPI interface hides the ARM SDK pollers behind synchronous methods for testability, mirroring the S3 client factory and the existing azurerm state-reader wrapper. A test-injectable client factory (SetAzureBackendClientFactory/ResetAzureBackendClientFactory) mirrors SetS3ClientFactory.
  • Location is sourced from the active Azure identity (or an existing resource group), never from the backend block — it is not a valid azurerm backend argument and Terraform would reject it in backend.tf.json.
  • Adds armresources + armstorage SDK deps (azcore/azidentity/azblob were already present).

Also fixes: azurerm backend init under Atmos-managed CLI auth

Field-testing the provisioner end-to-end surfaced a pre-existing bug that blocked azurerm backends whenever an Atmos profile was active. For CLI / device-code / interactive auth, Atmos exported both ARM_SUBSCRIPTION_ID and ARM_TENANT_ID to the Terraform subprocess. OpenTofu's azurerm backend authenticates via the Azure CLI and runs az account get-access-token --subscription <id> --tenant <id>, which the CLI rejects with Please specify only one of subscription and tenant, not both. It fails at argument validation — before the CLI even checks the session — so terraform init cannot list existing workspaces or read state at all:

Error: Failed to get existing workspaces: error listing blobs:
AzureCLICredential: ERROR: Please specify only one of subscription and tenant, not both

The tenant is now exported only for OIDC (service-principal / federated) auth, which needs it and does not shell out to the Azure CLI. On the CLI path only ARM_SUBSCRIPTION_ID is exported; the tenant is already fixed by the MSAL session Atmos seeds (and the active subscription), and the azurerm/azapi/azuread providers auto-detect it. oidc.go re-adds the tenant in its OIDC override, so service-principal auth is unchanged.

  • pkg/auth/cloud/azure/env.go: PrepareEnvironment gates the tenant export on UseOIDC.
  • pkg/auth/providers/azure/oidc.go: OIDC override re-adds ARM_TENANT_ID / AZURE_TENANT_ID.
  • Test updates assert the tenant is omitted on the CLI/device-code/interactive path and present for OIDC.

why

Brings Azure to feature parity with AWS for backend bootstrapping and eliminates the chicken-and-egg problem of needing remote state before Terraform can run — replacing the bespoke cold-start storage-account component teams currently hand-roll on Azure.

The CLI-auth fix is what makes the provisioned backend actually usable under Atmos-managed Azure auth: without it, terraform init against any azurerm backend fails while a profile is active.

references

  • New PRD: docs/prd/azurerm-backend-provisioner.md
  • Fix doc: docs/fixes/2026-08-11-azurerm-backend-cli-subscription-tenant-conflict.md
  • Mirrors: docs/prd/s3-backend-provisioner.md

test

  • Unit tests (pkg/provisioner/backend/azurerm_test.go, azurerm_wrappers_test.go): table-driven, mocked azureBackendAPI and Azure SDK fake servers — config extraction/precedence, use_azuread_auth parsing, full-create, resource-group reuse, existing-account warning, location-required error, every error path, existence checks, delete safety, registry wiring, and the ARM passthrough wrappers (404→exists mapping, poller completion, error propagation). 95.1% package coverage.
  • Auth fix tests (pkg/auth/cloud/azure/{env,setup}_test.go, pkg/auth/providers/azure/{cli,device_code,oidc}_test.go): both branches of the tenant gate verified — omitted for CLI/device-code/interactive, present for OIDC. env.go and oidc.go PrepareEnvironment are both 100% covered.
  • go build ./..., go vet, gofmt, and all pre-commit hooks (go-fumpt, golangci-lint, go.mod tidy) pass.

[!NOTE]
The auto-provisioned account uses secure-but-simple defaults (Standard_LRS, Microsoft-managed keys, public network access with Entra ID/RBAC gating) intended for dev/test/bootstrap — not production. For production, import the resources into a managed module (e.g. Azure/avm-res-storage-storageaccount); the provisioner is idempotent, so provision.backend.enabled: true can be left in place.

fix(ai): close DX gaps found in atmos ai field test @osterman (#2903)

what

  • Adds atmos ai skill update [name], a new command that compares each installed bundled skill's recorded version against the catalog embedded in the running binary and reinstalls only the ones that are actually outdated — closes the "no update command" gap the fixes below originally left deferred. See the blog post for the full story.
  • Enforces the compatibility.atmos version-compatibility gate for bundled and multi-skill Git package skill installs, not just single-skill Git clones (it was previously skipped entirely for those two paths).
  • Rejects unrecognized atmos ai skill install/uninstall/update --client values instead of silently no-op'ing, by extending pkg/flags's WithValidValues to work on string-slice flags generically (this also fixed a latent bug where WithValidValues was silently dead for every command that binds flags via BindFlagsToViper without calling the full Parse() pipeline).
  • Warns when --path is combined with --client/--scope/--global/--all-clients on skill install, since --path skips auto-distribution and those flags are otherwise silently ignored.
  • Gives the skill-registry-corruption error an actionable hint via the error-builder pattern instead of a bare wrapped JSON error.
  • Shows a skill's minimum required Atmos version in skill list --detailed, and flags when an installed skill has a newer catalog version available (using the same comparison update now acts on).
  • Fixes agent-skills/skills/atmos-ai/SKILL.md doc drift (it never documented skill install at all) and removes a phantom info subcommand from atmos ai skill --help.
  • Adds local-path/file:// support to skill source parsing and the downloader.
  • Documents --scope/--global precedence on skill install/uninstall/update.
  • Makes atmos ai exec/ask --session actually persist and resume conversations — previously a documented flag that was a complete no-op.
  • Resolves a session's Model from the constructed AI client instead of an independent config lookup, fixing sessions export/import for the default zero-config claude-code provider path (previously exported checkpoints for that path could never be re-imported).
  • Applies --mcp server filtering for CLI providers (claude-code/codex-cli/copilot-cli/gemini-cli) too — it was silently ignored, so all configured MCP servers were always passed through regardless of the flag.
  • Rejects invalid --format values on ai exec instead of silently falling back to text.
  • Behavior change: ai exec can now return exit code 2 for a genuine infrastructure-level tool failure (e.g. an unregistered tool) immediately, without waiting on the 25-iteration tool-call loop to exhaust as it did before.
  • Behavior change: sessions clean --older-than 0d now deletes all sessions immediately, distinguished from the flag not being passed at all (which still defaults to 30 days); negative durations are now a hard parse error instead of silently falling back to the default.
  • Remediates 7 open Dependabot alerts (2 high, 4 medium, 1 low) in transitive website dependencies: js-yaml (GHSA-5p4m-2wfm-xmqj, quadratic CPU consumption in !!omap resolution) and mermaid (5 advisories), via pnpm.overrides bumps within their existing major versions. No CodeQL alerts were open.
  • Fixes a flaky TestManager_ExportSession_WarnsOnUnimportableCheckpoint CI failure: the test asserted on raw ANSI-styled ui.Warning() output, which the formatter renders as two adjacent styled runs under CI=true — same visible text, different byte layout, so the test passed locally and failed in CI. Strips ANSI before asserting on content now.

why

  • These are all findings from a hands-on field test of the atmos ai command surface — reading the real implementation, hypothesizing plausible misuse an automated test wouldn't catch, and executing for real against isolated fixtures — rather than a spec change or feature request. Most are silent DX gaps (a flag that looks like it works but doesn't, an error with no way forward, a validation check that only applies on some of the paths that need it).
  • The two behavior changes exist because the current behavior actively undermines the documented contract: an exit code that's "practically unreachable" is useless to scripted consumers, and a duration flag that silently no-ops on 0d instead of doing what it says is a footgun in the other direction (a user who deliberately asks to delete everything gets nothing, silently).
  • atmos ai skill update exists because, once asked, leaving "no update command" as a documented gap wasn't the right call — bundled skills going stale after a binary upgrade is exactly the kind of silent drift this whole PR is about fixing elsewhere.
  • The security fix was picked up automatically after pushing this branch (GitHub reported the alerts against the default branch) and is bundled here per this repo's standing policy of fixing security alerts directly on the branch already in flight rather than opening a separate PR.

references

  • No tracked GitHub issues — everything here was discovered fresh during this field test and addressed directly in this PR, including the atmos ai skill update command that was initially scoped out and then built once asked for.
feat(ai): browsable, searchable Agent Skills Directory @osterman (#2881)

what

  • Adds a generated, browsable, searchable Agent Skills Directory at /ai/skills, replacing the hand-maintained (and drifted) skill table on the Agent Skills doc page.
  • Extends the file-browser Docusaurus plugin with category grouping, a search box, configurable card icon/CTA label, and a "Copy as Markdown" button, reused for the new skills instance.
  • The "Copy as Markdown" button (skills only) concatenates a skill's SKILL.md and every nested reference file into one clipboard-ready document, so its full context can be grabbed without installing it.
  • atmos ai skill list gains a --format flag (table/json/yaml/csv/tsv) and a Category column.
  • Adds a SkillCount component that renders the live, build-time skill count inline in prose (homepage AISection, docs), so counts can't drift out of date again.
  • Restructures the sidebar nav (Atmos AISkills category with Agent Skills, Skill Marketplace, and a link to the new directory) and cleans up a duplicate "Native CI" sidebar/doc link.
  • Adds the changelog post and roadmap entry for this feature, and fixes pre-existing EditorConfig indentation violations in several SKILL.md files surfaced by the affected-file validator.

why

  • The old skill list page was hand-maintained and had drifted to roughly half the real skill count, with stale entries pointing at skills that no longer exist. There was also no way to search, filter, or grab a skill's content without installing it.
  • Generating the directory from the skills themselves, and computing counts at build time, makes it structurally impossible for the docs to fall out of sync again.

references

  • Blog post: /blog/agent-skills-directory
  • Docs: /ai/skills, /ai/agent-skills
fix(kubernetes): single-file GitOps delivery and Kustomize metadata.name exemption @osterman (#2874)

what

  • kubernetes.gitops.provision.targets.<name> (kind: git) now supports a split tri-state: split: false writes path as a single merged multi-document YAML file instead of always treating path as a directory of auto-named files; unset infers the mode from whether path's last segment looks like a manifest filename (.yaml/.yml/.json).
  • Atmos's structural manifest validator no longer requires metadata.name on Kustomize's own Kustomization/Component objects (matched against sigs.k8s.io/kustomize/api/types's own kind/version constants), since Kustomize's own schema and field-enforcement never require one.
  • A new validate: false component-level flag opts a component out of both the apply/deploy structural auto-gate and the standalone atmos kubernetes validate command.
  • Docs: new "Generating a Kustomize component for GitOps" walkthrough, split documented on kubernetes-deploy.mdx, and the Kustomize exemption / validate: false documented on kubernetes-validate.mdx.
  • Changelog post and a new shipped roadmap milestone (with a corrected progress percentage) for the Extensibility initiative.

why

  • A git provision target's path was always treated as a directory, so configuring path: ".../kustomization.yaml" created a directory by that name containing an auto-generated file inside it, instead of the exact file Kustomize's remote-include mechanism requires.
  • The validator required metadata.name unconditionally, forcing users to add a meaningless name to Kustomize Component/Kustomization objects just to satisfy Atmos, even though Kustomize's own tooling never requires one.
  • Together these blocked a real GitOps pattern: rendering a Kustomize patch/component with Terraform-derived values (e.g. via !terraform.state) and committing it to a deployment repo as a proper kustomization.yaml for Argo CD/Flux to consume.
feat(toolchain): support Aqua `github_archive` package type @osterman (#2416)

what

  • Add github_archive package type to the Aqua-compatible toolchain registry parser and installer.
  • Resolve downloads to https://github.com/{owner}/{repo}/archive/refs/tags/{version}.tar.gz, matching upstream aquaproj/aqua semantics.
  • Hardcode tar.gz (mirroring aqua's GetFormat()); asset, url, format, and format_overrides are intentionally ignored for this type.
  • Extend resetByPkgType so version overrides that switch a tool to github_archive clear stale asset/url.
  • Add unit + registry-parsing tests plus a fixture modeled on the adr-tools example; cover validation, version-prefix handling, format-ignored semantics, URL pattern, and {{trimV .Version}}/path template idiom.
  • Add changelog blog post and a shipped milestone under the Extensibility initiative on the roadmap.

why

  • Aqua's upstream registry uses github_archive for tools shipped as repository source archives (e.g., adr-tools, tfenv, tgswitch, and many single-script projects). Without this type, those entries failed with unsupported tool type: github_archive.
  • Adding parity with aqua unblocks all such registry entries with no user-side changes — pull the upstream definition as-is and it just works.

references

🚀 Enhancements

fix(schemas): accept documented backend types and fields the manifest schema rejected @osterman (#2953)

what

  • Adds the missing consul, cos, http, kubernetes, oss, pg backend types to the stack-manifest JSON Schema's backend_type/remote_state_backend_type enums and backend_manifest.properties allow-list, across all three hand-maintained schema copies (embedded, stack-config, and the test fixture).
  • Adds several other real, documented, Go-read fields the same schema was rejecting: terraform.overrides.{hooks,generate,secrets,auth,retry,required_providers,required_version}, component-level retry: (terraform/helmfile/packer/kubernetes), component-level required_version/required_providers, source.ttl, and container_runtime.provider: "auto".
  • Removes "raw" from workflow_step.output.mode's enum — the schema accepted it but Go's validateParallelOutput never did, so it passed validation and then failed at execution time.
  • Fixes a root-schema oneOf bug that made workflows: and ordinary stack fields (vars:, settings:, etc.) mutually exclusive in one manifest, producing an uninformative (root): valid against schemas at indexes 0 and 1 error.
  • Adds a Go-level check (internal/exec/stack_processor_backend.go) that errors when backend_type/backend (or remote_state_backend_type/remote_state_backend) don't match, instead of silently resolving to an empty backend config.
  • Adds regression coverage at both the schema-unit level (pkg/datafetcher) and the CLI level (new fixture at tests/fixtures/scenarios/manifest-schema-coverage/ + tests/test-cases/manifest-schema-coverage.yaml, run through the real binary).

why

  • atmos describe stacks (and anything that calls it, including terraform plan and custom commands) hard-rejects a backend_type: http manifest even though http is a real, generic Terraform/OpenTofu backend (e.g. GitLab-managed state) that Atmos's own docs already claim to support — closes #2919.
  • This wasn't new schema drift: describe stacks has always run schema validation, but a prior unrelated fix (PR #2749, 2026-07-15) corrected a bug that had silently no-op'd that validation whenever schemas.atmos.manifest was unconfigured (the default). Once validation actually started running, this pre-existing enum gap — and, as a follow-up field test found, several siblings in the same bug class — became hard, user-visible failures.
  • Each additional field fixed here (overrides, retry, required_version/providers, source.ttl, container provider) is independently documented and already read by Go; only the embedded schema was out of sync.
  • The root oneOf fix and the backend key-mismatch check close two related, previously-silent failure modes surfaced by the same investigation, rather than leaving known gaps for the next person to rediscover.

references

  • Closes #2919
  • Supersedes #2920, which independently proposed adding http as a valid backend_type for the same issue. This PR covers that same schema change (http in backend_type/remote_state_backend_type and backend_manifest.properties) plus 5 more missing backend types, all 3 hand-maintained schema copies (#2920 updated 2 of 3), and several independent schema/validation bug classes found via a follow-up field test. Thanks to @MacherelR for the original report and fix — closing #2920 in favor of this broader pass; will fold in the HTTP-backend documentation from that PR separately.
  • Full rationale, root-cause analysis, and validation notes: docs/fixes/2026-08-11-manifest-schema-missing-backend-types.md
fix(schema): model required_providers/retry/Helm in atmos-manifest @osterman (#2950)

what

  • Add required_version, required_providers, and retry to the terraform, terraform_component_manifest, and shared overrides definitions in the atmos-manifest JSON Schema.
  • Add generate to the stack-level kubernetes definition (same drift class, found while auditing for other instances).
  • Model native Helm in the schema (helm, helm_repository, helm_components, helm_component_manifest), closing the previously-tracked topLevel:helm gap.
  • Fix pkg/datafetcher/schema_section_coverage_test.go: required_version, required_providers, and retry were explicitly exempted from the schema-coverage guard with the comment "introspected from Terraform, not authored" — factually wrong, since all three are user-authored fields with full stack-processor support. This is why the guard never caught the original gap.
  • Sync the same schema edits into tests/fixtures/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json, and extend the atmos-stacks-validation fixture with real usage of every fixed field so TestValidateStacksCmd_Success is a live regression guard for this bug class.

why

  • atmos validate stacks (broken since v1.224.0) and atmos describe stacks (broken since v1.225.0) reject required_version/required_providers under components.terraform.<name>, even though the fields are fully implemented and documented (added by #1841).
  • Root cause: required_version/required_providers were added to the legacy stack-config schema by #1841, but never ported to atmos-manifest, the schema actually enforced by validate/describe. That drift stayed invisible for months because a separate bug (fixed by #2749) had been silently skipping schema validation whenever schemas.atmos.manifest wasn't explicitly configured — the common case. Once #2749 fixed that wiring bug, the pre-existing drift became a hard regression for any user setting these fields.
  • This is a recurring class of bug, not a one-off: the same additionalProperties: false rejection pattern is the root cause behind #2919 (backend_type: http, fix open in #2920) and #2104 (depends_on_manifest.stack, fix open in #2835). Auditing for the same pattern surfaced the kubernetes.generate and native-Helm gaps fixed here.
  • The coverage-guard test exists specifically to prevent this class of drift, but the classification of required_version/required_providers/retry as non-manifest ("introspected, not authored") let this exact regression through undetected. Fixing the classification, not just the schema, closes the actual hole.

references

  • Closes #2948
  • Related: #2919 / #2920 (backend_type: http), #2104 / #2835 (depends_on_manifest.stack) — same bug class, separate PRs already open for those
fix(docker): build the arm64 image with an arm64 userland @arcaven (#2932) This makes the `linux/arm64` image you already publish run on arm64 hardware, instead of failing at startup with `exec format error`. The arm64 image has shipped an amd64 userland since it was first added, so this closes the gap between the advertised multi-arch manifest and what arm64 users actually receive. The change is one line and additive, plus a guard so it cannot silently regress.

what

  • Drop the --platform=$BUILDPLATFORM pin on the runtime FROM so the Debian
    base and all apt-installed tools follow TARGETPLATFORM.
  • Add a build-time assertion that the base architecture matches the build
    target, so a re-introduced pin fails the build instead of shipping a
    wrong-arch image.

why

  • The pinned base meant every target built on an amd64 base, so the published
    linux/arm64 image contained an amd64 userland and failed with
    exec format error on arm64 hardware. Only atmos and kustomize were
    arm64, and they were stranded in an amd64 rootfs with no aarch64 loader.
  • This has been the case since the image was first added (#627), so no released
    tag has a working arm64 image. The guard would have caught this on day one.

references

fix(git): tolerate config errors for CI git-clone bootstrap pre-Cobra @osterman (#2879)

what

  • Fixes atmos git clone failing before it ever attempts a clone in a fresh CI workspace, when a referenced config profile doesn't exist yet (e.g. ATMOS_PROFILE=github with no .atmos/profiles/ checked out) — ATMOS_CI=true had no effect on this failure.
  • Adds a combined regression test case to pkg/container's build-arg builder covering engine, driver, cache, custom dockerfile/context, and tags together in one config (previously only tested individually).

why

  • cmd/root.go's Execute() runs an initial cfg.InitCliConfig before Cobra resolves any subcommand. Only the second InitCliConfig call (inside PersistentPreRun) knew how to tolerate the CI git-clone bootstrap's expected missing config (applyCIGitCloneBootstrap). The first call's error handler had no such tolerance, so a profile not found error aborted the process before Cobra — and therefore before PersistentPreRun — ever ran, regardless of ATMOS_CI.
  • Adds isCIGitCloneBootstrapArgs (an os.Args-based equivalent of the existing Cobra-aware bootstrap check) to the pre-Cobra handler, and a new exported CIGitCloneModeRequestedFromEnv in cmd/git so both code paths defer to the same ATMOS_CI/CI-provider resolution logic.
  • The container test addition closes the one remaining gap in buildBuildArgs coverage: individual fields (driver, cache, tags, custom dockerfile/context) each had their own case, but nothing asserted they all survive together in a single build.

references

  • N/A
fix(helm): native Helm UX fixes (repo isolation, status output, default identity, namespace) @aknysh (#2941)

what

Four independent fixes to the native Helm implementation (pkg/component/helm):

  • 1. Repository config/cache isolation. newSettings now points Helm's RepositoryConfig and
    RepositoryCache at an atmos-managed XDG location (<xdg-config>/atmos/helm/repositories.yaml,
    <xdg-cache>/atmos/helm/repository) unless HELM_REPOSITORY_CONFIG / HELM_REPOSITORY_CACHE is set,
    instead of inheriting the user's global Helm config.

  • 2. Status output on apply/delete. atmos helm apply and atmos helm delete now print a one-line
    status (release name, namespace, chart) instead of succeeding silently.

  • 3. Stack default-identity resolution. atmos helm apply/diff/delete resolve the stack's
    default: true identity binding the same way atmos terraform does, so an explicit --identity is
    no longer required for cluster operations. The offline template render never triggers auth.

  • 4. Namespace for namespace-less charts. newActionContext now sets the namespace on the Helm
    EnvSettings (SetNamespace), so charts whose manifests omit metadata.namespace install into the
    component's configured namespace instead of the kubeconfig-default namespace.

why

  • 1. Because settings inherited the user's global Helm config, resolving a declared repo/name
    chart sent Helm down downloader.(*ChartDownloader).scanReposForURL, which iterates every repository
    in the user's global repositories.yaml and fails on the first one whose index is not cached
    (e.g. no cached repo found ... <repo>-index.yaml). An unrelated repository in the user's global
    config could break an atmos chart render, and setupHelmRepositories also mutated the user's global
    config. Isolation makes chart resolution depend only on the repositories the components declare, keeps
    it reproducible across workstations/CI, and mirrors how the kubeconfig is already isolated under the
    atmos XDG dir.

  • 2. A successful apply/delete produced no output, so there was no confirmation of what happened
    (release, namespace, chart) without separately querying the cluster. template/diff already emit
    their own output; apply/delete now do too.

  • 3. The helm exec path set up auth only when an explicit identity was given, so without --identity
    no auth manager was created, no KUBECONFIG was injected, and the command could not reach the cluster.
    Terraform/helmfile already auto-resolve the stack default identity; helm now matches them for cluster
    operations while keeping template fully offline. When no auth is configured, the identity stays empty
    and the ambient KUBECONFIG is used, preserving prior behavior.

  • 4. Helm derives the namespace for namespace-less objects from the settings/RESTClientGetter,
    which atmos left at the kubeconfig context default; only the install action's namespace was set. Charts
    that hardcode namespace: {{ .Release.Namespace }} worked, but charts that do not landed in default.

testing

Automated (in-code, pkg/component/helm)

  • repo_isolation_test.go - isolation is applied when the HELM_REPOSITORY_* env vars are unset;
    explicit values are respected unchanged.
  • status_output_test.go - a status line is written for apply/delete on success only (silent on error
    and for template/diff), and the message names the release and namespace.
  • default_identity_test.go - the decision logic (shouldSetupComponentAuth, operationRequiresCluster),
    plus an executor test asserting a cluster operation resolves component auth with no explicit identity
    while template stays offline.
  • namespace_test.go - newActionContext sets the settings namespace; an empty namespace leaves Helm's
    default untouched.

New functions are covered at 100%; package total is ~89.6%. gofmt and go vet are clean, and the full
module builds. TestMain initializes the data writer once for the package since apply/delete now emit
output.

Manual (against a live AKS cluster)

Built a binary and deployed two native Helm components: one local chart (files in the repo) and one chart
pulled from a public Helm repository.

  • 1. With the global Helm config holding unrelated, uncached repositories, apply previously failed on
    an unrelated repo's index; after the fix it succeeds, writes only to the atmos-managed repository config
    (which then contains only the declared repository), and leaves the user's global config untouched.
  • 2. apply and delete both print their status line.
  • 3. both charts were applied and deleted with no --identity, resolving the stack's default identity.
  • 4. the public chart (whose manifests set no namespace) installed into the configured namespace
    instead of default.

references

  • docs/fixes/2026-08-14-native-helm-ux-fixes.md
fix(secret): inherit the component's default identity for stores @jaguer0 (#2746)

what

  • atmos secret (set/get/init/validate) now inherits the component's effective identity for store-backed secrets whose store declares no explicit identity:, instead of falling back to the default AWS credential chain (→ EC2 IMDS, which fails off-EC2).

why

  • injectSecretStoreAuthResolver (cmd/secret/shared.go) called atmosConfig.Stores.SetAuthContextResolver(resolver), which passes an empty identity to every store, so an identity-less store fell back to the AWS default chain → EC2 IMDS and failed off-EC2 (e.g. no EC2 IMDS role found ... dial tcp 169.254.169.254:80: connect: host is down).
  • The terraform paths (cmd/terraform/utils.go, internal/exec/terraform_execute_helpers.go) already call SetAuthContextResolverWithDefaultIdentity; the secret CLI even computed the same DefaultIdentity (into SecretsAuth) but never applied it to the stores.
  • This aligns the code with documented behavior — website/docs/cli/configuration/secrets.mdx: "When omitted and the secret is resolved within a component scope, the component's effective identity is inherited."
  • Stores with an explicit identity, and an explicit --identity, are unaffected (defaultIdentityForStore only fills empty-identity stores). atmos terraform and atmos secret list behavior is unchanged.

Suggested label: patch (user-visible bug fix, no new surface; no blog/roadmap required).

references

  • Related: #2662 (terraform store-output hooks inherit the run's default identity — sibling fix).
  • Fix write-up: docs/fixes/2026-07-13-secret-cli-inherit-default-identity.md
fix(merge): resolve deferred YAML functions losing data on merge (#2888) @osterman (#2892)

what

Fixes #2888 — deferred YAML functions silently losing data on merge

  • Every production call site of ApplyDeferredMerges passed processor = nil, so deferred YAML
    functions (!template, !terraform.output, !terraform.state, !store, !exec, !env) were
    never actually resolved-and-merged — they silently lost data whenever a concrete value at another
    config layer collided with them. On top of that, !labels/!tags/!labels.keys/!labels.values
    weren't in the defer list at all, which is the literal scenario reported in the issue.
  • Adds a real Stage 3 resolution pass (internal/exec/deferred_contexts.go, plus changes across
    internal/exec/stack_processor_*.go, internal/exec/yaml_processor.go,
    internal/exec/yaml_func_tags.go, pkg/merge/deferred.go, pkg/merge/merge_yaml_functions.go)
    that resolves deferred functions per-invocation (auth- and template-context-aware) and deep-merges
    the result against any concrete override at the same path — including the mirror-precedence
    direction (a concrete value at a lower-precedence layer than the function), which the original
    design didn't handle.
  • Fixes a nondeterministic parent/child collision found while field-testing: ApplyDeferredMerges
    now processes deferred paths ancestor-before-descendant, so a descendant leaf can never be
    clobbered by a later wholesale replace of its ancestor map (see
    docs/fixes/2026-08-07-deferred-merge-nested-function-collision.md).

Fixes a double-execution regression introduced by the Stage 3 pass

  • Reviewing the Stage 3 wiring surfaced a behavior regression: with --process-functions=true, the
    document-wide ProcessCustomYamlTags pass already resolves each surviving function, and Stage 3
    then re-resolved every deferred path unconditionally — so each deferred function ran twice
    per component. Harmless for pure/cached functions (!template, !terraform.output/state,
    !labels, !tags, !env), but !exec (uncached — runs the shell again) and !store
    (an extra backend read) were executed twice. Confirmed live: a non-colliding vars.foo: !exec
    ran the shell on this branch vs on main.
  • Fix (pkg/merge/merge_yaml_functions.go): for a single-contribution (no-collision) deferred path
    whose value is already resolved in the result, ApplyDeferredMerges now reuses that value instead
    of re-invoking the processor. Tightly guarded so genuine collisions (len > 1) still fully
    resolve-and-merge — the #2888 fix is untouched. See
    docs/fixes/2026-08-13-deferred-merge-double-execution.md.

Housekeeping

  • Introduces named types StackComponentDeferredContexts and AllStacksDeferredContexts in place
    of the raw map[string]map[string][...]ComponentDeferredContexts signatures threaded through the
    stack processor (readability only; no behavior change).
  • Also bundled in this branch (unrelated to #2888, surfaced during field-test CI runs): transient-error
    retry logic for the Aqua registry and GitHub releases/rate-limit fetches
    (pkg/toolchain/registry/*), and a stack-completion fix so completion lists all project stacks
    including local (cmd/emulator/completions.go).

why

  • vars.tags: !labels (and other deferred functions) silently lost data when another config layer
    set a conflicting value at the same path — a correctness bug with no error or warning, so it was
    hard to detect in real stacks.
  • The double-execution fix prevents side-effecting/uncached functions (!exec, !store) from
    running twice, which could surprise users with duplicated side effects or extra load.
  • Per this repo's bug-fixing workflow, regression tests were written and confirmed failing first,
    then the fixes were implemented and verified against them (including live before/after runs of a
    real !exec fixture and end-to-end assertions through ExecuteDescribeComponent).

references

Fix const variable interpolation in Terraform module sources @gitbluf (#2914)

What

Updates terraform-config-inspect to support static (const = true) variable
interpolation in Terraform module.source values.

Adds regression coverage for:

  • Successfully describing a component with source = "./mods/${var.org}".
  • Preserving real HCL syntax failures when loading Terraform components.
  • Returning parsed Terraform configuration as *tfconfig.Module in the OpenTofu
    interpolation test.

Why

Atmos previously failed while parsing valid Terraform 1.15+
configurations that interpolate a static variable in module.source:

variable "org" {
  const   = true
  type    = string
  default = "myorg"
}

module "greeting" {
  source = "./mods/${var.org}"
}

The previous terraform-config-inspect version evaluated module.source without an HCL
evaluation context and returned Variables not allowed before Terraform or OpenTofu was
invoked.

Fixes: #2913

fix(terraform): prevent concurrent output corruption @zack-is-cool (#2898)

What

Prevent concurrent Terraform runs from interleaving provisioner and lifecycle UI with component-prefixed output.

Why

JIT provisioning, backend provisioning, post-init provider locking, and clear or spin step hooks could bypass the scheduler's concurrent-output suppression. Terminal control sequences could corrupt output from other components.

Validation

  • go build ./...
  • atmos lint --changed
  • Focused scheduler, hooks, runner-step, provisioner, source, workdir, and Terraform-init tests.
  • Full internal/exec suite completed in a clean worktree.
  • Full CLI suite completed with an extended timeout. Remaining failures require external GitHub access for tenv, a non-linked Git worktree for one sandbox test, and an environment without inherited GitLab tokens.
fix(scaffold): preserve source in scaffold config @jorrite (#2869)

what

  • Fix atmos scaffold generate/atmos init recording a dangling, already-deleted temp-directory path in spec.source of .atmos/scaffold.yaml whenever the template source is remote (git::... or a bare https://... URL).
  • In the filepkg/generator/source/resolver.go: in resolveRemote(), set conf.Source = src (the original source string the caller passed in) after loading the template configuration from the temporary download directory, instead of leaving Configuration.Source as whatever LoadConfigurationFromDir was given (the temp dir itself).
  • pkg/generator/source/resolver_test.go: added/extended tests asserting Configuration.Source holds the original source for both local and remote paths, plus a dedicated regression test (TestResolve_RemoteRecordsOriginalSource) that fails on the pre-fix code and passes after.
  • No change to local source handling (resolveLocal), which already recorded the correct value.

why

  • For a remote scaffold source, resolveRemote() downloads the template into os.MkdirTemp("", "atmos-scaffold-"), then loaded the config with that temp dir passed in as the "source" — so Configuration.Source, and
    therefore the persisted spec.source, ended up holding something like /var/folders/xx/.../atmos-scaffold-1234567890. That directory is removed by cleanup() immediately after the command finishes, so the recorded provenance is a dangling reference to nothing as soon as generation completes — useless for anything that might want to read it back later (e.g. a future --update/re-resolve flow), and directly contradicts SaveProjectRecord's own doc comment: "spec.source and spec.baseRef record provenance for future updates."
  • Local sources (a relative/absolute path, or file://...) were correct only by accident of not having a temp-dir indirection step in resolveLocal, not because anything special-cased provenance for them.
  • Reproduced directly: atmos scaffold generate "git::https://.../scaffold-template.git" ./out --defaults, then cat ./out/.atmos/scaffold.yaml shows a /var/folders/...//tmp/... path for spec.source, and that path no longer exists on disk.

references

  • No upstream GitHub issue — checked issue search and the web for cloudposse/atmos + spec.source/scaffold, nothing matched as of 2026-08.
fix: preserve trailing newlines in text-based 3-way merges @jorrite (#2891)

what

  • Fix atmos scaffold generate --update unconditionally stripping the trailing newline from every file it 3-way-merges, whether or not the file actually changed.
  • pkg/generator/merge/text_merger.go: TextMerger.Merge() now appends one newlineSeparator ("\n") to each of ours/base/theirs before handing them to diff3.Merge, so diff3's guaranteed loss of exactly one trailing newline cancels out and the original count survives.
  • pkg/generator/merge/text_merger_test.go: consolidated the trailing-newline regression coverage into a single table-driven test, TestTextMerger_TrailingNewlinePreservation, asserting exact byte-for-byte output across a no-op merge (0/1/2/3 trailing newlines, plus an internal blank line) and a genuine template change (theirs with 0/1/2 trailing newlines).
  • No change to conflict detection, threshold behavior, or ConflictStrategy handling — out of scope, and unaffected since the appended newline is identical across all three inputs.

why

  • TextMerger.Merge() delegates the actual 3-way merge to epiclabs-io/diff3, which reads each of base/ours/theirs line-by-line via bufio.Scanner (ScanLines, Go's standard-library default split function) and rejoins the merged lines with strings.Join(lines, "\n").
  • ScanLines strips every line's terminator — including the last — and gives no way to tell afterward whether the original input ended with a trailing newline or not. Concretely: for content ending in N trailing newlines, the round-trip through GetLines + Join always reconstructs exactly N-1 (it loses exactly one, regardless of how many there were; for N = 0 there was nothing to lose in the first place). Verified directly: generating a file with 3 trailing newlines and running --update with nothing changed on the template side reproducibly comes back with 2.
  • Appending one newline to each input before the merge bumps every input's count to at least 1, so that guaranteed loss of exactly one cancels out and the original count is preserved — for both the no-op case and genuine changes, since whichever side's content ends up dominating a given region carries its own (now-restored) newline count through, independent of the others.
  • This must be applied to all three inputs, not just theirs: appending it only to theirs makes an otherwise-identical ours/theirs pair (a very common no-op shape) differ by one trailing newline as far as diff3 is concerned, which turns a no-op into a spurious detected change/conflict instead of fixing anything.

references

fix(version): exclude draft GitHub releases from Version Tracker resolution @osterman (#2900)

what

  • pkg/github.GetReleases now excludes draft GitHub releases unconditionally, alongside the existing prerelease filter. This fixes the Version Tracker's github-releases datasource resolver, atmos version list, and GetReleaseVersions, which all share this function.
  • Removes the deprecated atmos version track render subcommand. It was marked Deprecated/Hidden in the same commit that introduced it and has never had a non-deprecated existence in any release, so no migration path is needed. The shared renderTemplate helper moves into apply.go, its only remaining consumer.
  • Fixes pre-existing EditorConfig indentation drift (3-space list/fence indents instead of the required 2-space multiple) in docs/prd/atmos-version-management.md, surfaced once the file was touched by this branch's --affected validation.
  • Expands the version.files and !version function docs with worked before/after examples for the marker and github-actions file managers (including SHA pinning) and adds Helm/Container-component examples alongside the existing Terraform one.

why

  • atmos version track with datasource: github-releases and desired: latest could resolve to an unpublished draft release instead of the actual latest published release, whenever the GitHub token had repo write access (e.g. secrets.GITHUB_TOKEN in a repo's own CI). Reproduced against cloudposse/atmos itself: atmos version track lock resolved to v1.226.0, which gh release view v1.226.0 --repo cloudposse/atmos --json isDraft confirmed was a draft, when the real latest published release was v1.225.0. Unlike prerelease, there's no legitimate case for ever resolving to a draft, so it's excluded unconditionally rather than gated behind a new opt-in policy field.
  • atmos version track render was superseded by apply/the file-managers architecture within its own introducing PR and has been carried as dead weight across multiple releases; removing it avoids maintaining a command with no live users.
  • The docs updates make the Version Tracker's file-manager and !version behavior easier to learn from concrete examples rather than a single terse case.
fix(config): honor --config across internal reloads and multi-file merges @osterman (#2875)

what

  • Internal reloads of the CLI config (many call sites across internal/exec, pkg/vendoring, cmd/, etc. calling InitCliConfig(schema.ConfigAndStacksInfo{}, false)) now fall back to parsing --config/--config-path/--base-path from os.Args/env instead of silently discarding the selection made at startup.
  • A second --config file that sets a conflicting value for an array-typed key (e.g. stacks.included_paths) no longer aborts stack discovery for entries that still legitimately match; a real "nothing matched at all" case now returns a distinct error instead.
  • atmos config get now reports the effective, fully-merged configuration for the invocation (all --config files, --config-path dirs, and profiles applied) instead of reading a single physical file.
  • VendorDirAbsolutePath/WorkflowsDirAbsolutePath are now precomputed once (mirroring the existing top-level base_path resolution), so vendor/workflow path joins no longer re-derive a possibly still-relative BasePath.

why

  • atmos --config <file> terraform plan/test was failing with failed to find import even though atmos --config <file> list stacks worked with the identical flag, because a downstream InitCliConfig re-invocation lost the --config selection mid-command.
  • Splitting config across two --config files with a conflicting array value made stacks.included_paths unusable for stack discovery, while atmos config get misleadingly reported the config as unchanged.

references

Closes #2867
Closes #2868

fix(steps): resolve relative paths against step.WorkingDirectory @osterman (#2880)

what

  • Fix type: archive, file, workdir, junit, and container build step handlers to resolve relative source/destination/path/files/context/dockerfile fields against step.WorkingDirectory instead of the Atmos process's own cwd.
  • Add a shared BaseHandler.ResolveInWorkingDirectory helper (pkg/runner/step/handler_base.go) used by all five handlers; container build additionally anchors Dockerfile to the resolved Context, matching Docker's own convention.
  • Add regression tests for each fixed handler plus a hooks-integration test (TestStepEngineRunsArchiveTypeWithRelativeWorkingDirectory) reproducing the original bug end-to-end.
  • Update two pre-existing container tests that had hardcoded the old (buggy) relative-path behavior to assert the corrected absolute-path behavior.

why

  • type: archive steps run as component lifecycle hooks ignored step.WorkingDirectory, even though the hooks engine (pkg/hooks/step_engine.go) already correctly computes and sets it to the resolved component path before dispatch — the field was just never read back out by the handler.
  • Auditing for the same defect class turned up four more handlers (file, workdir, junit, container build) with the identical bug: relative paths resolved via template substitution only, then silently anchored to process cwd instead of the step's configured working directory.

references

fix(auth): cover legacy ARM audience and seed refresh token in Azure CLI cache @aknysh (#2890)

what

  • Store the seeded Azure management access token in the Azure CLI MSAL cache with all ARM scope forms in its target field — the modern scope (https://management.azure.com/.default) plus the legacy audience forms (https://management.core.windows.net/.default and the double-slash variant), with matching forms for the US Government and China clouds (new LegacyManagementScopes field on CloudEnvironment).
  • Copy the account's refresh token from the Atmos realm MSAL cache (~/.azure/atmos/<realm>/msal_token_cache.json) into the Azure CLI cache after login (new CopyAtmosRefreshTokensInto; UpdateAzureCLIFiles gains a realm parameter). Skipped for service principals, empty realms, or unmatched home account IDs.
  • Regression tests written first to reproduce both failures, now pinning the fix (pkg/auth/providers/azure/token_audience_test.go).
  • Fix doc: docs/fixes/2026-08-06-azure-cli-cache-legacy-audience-refresh-token.md.

why

  • After atmos auth login, Terraform providers that authenticate via AzureCLICredential request an ARM token for the legacy audience https://management.core.windows.net/ (the azidentity/azapi default). The cache write-back only seeded the modern scope, so MSAL's cache lookup missed and azapi-based modules (all modern Azure Verified Modules) failed mid-apply with AzureCLICredential: ERROR: Can't find token from MSAL cache — while azurerm resources in the same apply succeeded. Observed in a real cold-start apply of a state backend component.
  • MSAL matches a requested scope as a subset of a cache entry's space-separated target, and ARM accepts both audiences interchangeably, so a single entry carrying every form satisfies every lookup.
  • No refresh token was seeded at all, so once the access tokens expired (~1h) every az-side lookup failed the same way. Atmos authenticates with the Azure CLI's own public client ID, so the refresh token in the Atmos realm cache is directly usable by az — seeding it lets az self-mint tokens for any audience and survive access-token expiry.
  • Until now the workaround was to run a real az login alongside atmos auth login, defeating the purpose of single-command auth.

Manually verified end-to-end on a real Azure tenant:

  1. Logged out completely and wiped all caches: az logout, az account clear, removed ~/.azure/msal_token_cache.json and ~/.azure/atmos/ (confirmed with az account show failing).
  2. Ran atmos auth login alone — no az login at any point.
  3. Confirmed the refresh token was copied into the Azure CLI cache: jq '.RefreshToken | length' ~/.azure/msal_token_cache.json returned 1 (previously 0).
  4. Requested a token for the legacy ARM audience — the exact request azidentity/azapi make: az account get-access-token --resource https://management.core.windows.net/ succeeded (previously failed with Can't find token from MSAL cache). Its expiry matched the login session's, proving MSAL served it from the seeded multi-audience entry via subset matching rather than minting a new token.
  5. Ran atmos terraform plan on an azapi-heavy component (the exact field failure): refresh and plan completed clean with no MSAL errors.

references

fix(kubernetes): close gaps found field-testing Kustomize GitOps delivery @osterman (#2905)

what

Field-tested the Kustomize/git-delivery GitOps pipeline shipped in #2874 (real k3s cluster, real local git remotes, real fixtures — not mocks) and fixed the gaps found:

  • validate: "false" (a quoted YAML string, an easy typo) was silently ignored by atmos kubernetes validate/apply/deploy, leaving validation enabled with no warning. It now fails closed with a clear error.
  • provision.targets.<name>.split had no type enforcement in the runtime-embedded JSON Schema (pkg/datafetcher/schema/atmos/manifest/1.0.json) — only in the docs-facing copy (stacks/stack-config/1.0.json) — so split: "yes" passed atmos validate stacks and was silently dropped at runtime, falling back to path-based auto-inference. The embedded schema is now synced, and the git target's parseConfig also fails closed as defense in depth (since kubernetes validate/apply/deploy don't go through the stack-config schema path).
  • Flipping a git delivery target between directory and single-file mode now emits a warning before the unconditional RemoveAll that replaces whatever currently exists at the managed path.
  • The managed git workdir cache never reconciled with a changed git.repositories.<name>.uri — an already-cloned repo kept using its original remote forever. reconcile now syncs the local remote URL to the configured URI before fetching.
  • The DNS-1123 invalid-name error embeds a regex containing [, ], (, ) with no spaces; rendered as plain markdown it was both mangled (brackets collide with link syntax) and hard-wrapped mid-token. It's now backtick-fenced as a code span, so it renders verbatim.
  • Fixed a copy-pasted config example in the atmos-git skill doc: it showed a nested signing: {mode: auto} instead of the real flat signing: auto string field — copying it verbatim fails to parse.
  • Documented that atmos kubernetes validate --server fails on a manifest set that creates its own namespace and delivers into it in the same batch (inherent to Kubernetes server-side dry-run semantics — each object's dry-run is evaluated independently against already-persisted state), even though apply/deploy of the identical objects succeeds.

Also includes an unrelated, incidental fix: bumped js-yaml/mermaid pnpm overrides in website/ to close 7 open Dependabot alerts (triggered by this repo's post-push security-remediate automation).

why

A /field-test pass is a hands-on DX pass that builds real fixtures and runs the actual CLI against them, specifically to catch "looks fine in review, breaks or misleads a real user" gaps that unit tests (which mostly exercise fake clients and hand-built inputs) don't cover. Every fix here was independently reproduced live before being fixed, and re-verified live after. The pass also confirmed several things work correctly as documented (directory/single-file delivery, the Kustomize metadata.name exemption, validate:false + --server interaction, offline validation gating delivery) — those aren't included here since nothing needed to change for them.

references

  • Follow-up to #2874 (Kustomize/git-delivery GitOps support)
fix(ci): Docker build image mirrors and concurrent output race @osterman (#2884)

what

  • Bump cloudposse/github-action-docker-build-push from v3.0.0 to v3.1.0 in the release docker job (.github/workflows/build.yml).
  • Explicitly override the action's binfmt-image input to mirror.gcr.io/tonistiigi/binfmt:qemu-v7.0.0.
  • Hold the shared output lock for an entire flush (not per line) in LinePrefixWriter (pkg/io/line_prefix_writer.go), so concurrent Terraform node writers can't interleave a line mid-block.

why

  • The release Docker build job was failing due to rate limiting when pulling its buildx builder (moby/buildkit) and QEMU binfmt images from public.ecr.aws.
  • v3.1.0 of the action switches the buildx builder's default image to the Google mirror (mirror.gcr.io/moby/buildkit), fixing that pull. The action's binfmt-image input still defaults to public.ecr.aws/eks-distro-build-tooling/binfmt-misc even at v3.1.0 (no upstream fix yet), so it's overridden here directly to the equivalent Google-mirrored tonistiigi/binfmt image, which publishes the same qemu-v7.0.0 tag. Verified live: both mirror.gcr.io/tonistiigi/binfmt:qemu-v7.0.0 and docker.io/tonistiigi/binfmt:qemu-v7.0.0 resolve to the same digest and pull successfully; mirror.gcr.io also falls through to Docker Hub origin on any cache miss, so it's never less reliable than a direct Docker Hub pull.
  • Separately, the macOS Acceptance Tests job was failing TestExecuteTerraformConcurrentHooksUseNodeWriters (pkg/scheduler/adapters) with a real, reproducible race: LinePrefixWriter.writeLine acquired/released the shared output mutex per line, so a single Write() call that produced multiple lines (e.g. a hook's buffered \r-then-\n progress update) could have a different node's writer interleave a line in between, corrupting concurrent Terraform output. Reproduced with go test -race -count=200 before the fix (intermittent failures) and confirmed 200/200 clean after.
  • Both fixes address CI reliability issues discovered while investigating unrelated failures on this branch; neither changes the shipped Atmos CLI's behavior for end users.

references

  • Upstream fix: cloudposse/github-action-docker-build-push v3.1.0
  • CI failure: Acceptance Tests (macos), job 92489708710

🤖 Automatic Updates

chore(deps): update github/codeql-action action to v4.37.7 @[renovate[bot]](https://github.com/apps/renovate) (#2952)

Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.

fix(deps): update module github.com/google/go-containerregistry to v0.21.9 @[renovate[bot]](https://github.com/apps/renovate) (#2921) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
build(deps): bump the website group in /website with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) (#2945) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
build(deps): bump gopkg.in/ini.v1 from 1.67.2 to 1.67.3 @[dependabot[bot]](https://github.com/apps/dependabot) (#2944) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
build(deps): bump the cicd group with 12 updates @[dependabot[bot]](https://github.com/apps/dependabot) (#2946) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
build(deps): bump github.com/getsentry/sentry-go from 0.46.2 to 0.48.0 @[dependabot[bot]](https://github.com/apps/dependabot) (#2943) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
fix(deps): update github.com/epiclabs-io/diff3 digest to 3b16698 @[renovate[bot]](https://github.com/apps/renovate) (#2917) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.
fix(deps): update kubernetes monorepo to v0.36.3 @[renovate[bot]](https://github.com/apps/renovate) (#2918) Automated dependency update. Changelog trimmed to keep the aggregated release-notes draft under GitHub's 125,000-character limit. See the PR commits for details.

Don't miss a new atmos release

NewReleases is sending notifications on new releases.