refactor(lint): go-native Mage tooling + goimports→gci formatter speedup Erik Osterman (Cloud Posse) (@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'scustom-gcl/lintroller/gomodcheck/changedsubcommands now each
delegate to a one-linego 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-revbranching for the pre-commit hook) is fully ported to
magefiles/mage_lint_golangci_run.goand deleted..pre-commit-config.yaml'sgolangci-lint/gomodcheckhooks now call
go tool mage lint:precommit/go tool mage lint:goModCheck..golangci.ymlgetsbuild-tags: [mage]so the new build tooling is linted too, with scoped
exclusions forforbidigo/lintroller(dev tooling outside the Atmos CLI/UI runtime — same
precedent already used fortools/gomodcheck/main.go)..github/workflows/codeql.yml's custom-gcl build step also moved ontogo tool mage..golangci.yml'sformatterssection swapsgoimportsforgci, 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.gciis a stock golangci-lint v2 formatter, so
no.custom-gcl.ymlplugin change is needed.CLAUDE.mdand thelint-fix/test-coverage-fix
agent docs are updated to namegciinstead ofgoimports.
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 viaos.Stat().ModTime()comparisons, and also
fixes the per-worktree cache isolation to setTMP/TEMPon Windows (Go'sos.TempDir()
ignoresTMPDIRthere), which the bash version never handled. gomodcheckpreviously had two different implementations (a staleness-cached binary in the
atmos-command path, a plaingo runin the pre-commit-hook path). Unified ontogo run -C tools/gomodcheck . <go.mod>for both —go runalready benefits fromGOCACHE, so the cached
binary bought little for the extra code.goimportswas a measurable bottleneck in local and CI lint runs;gciperforms 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
customGCLstaleness branches (missing binary / config newer / plugin source newer)
manually triggered and confirmed correct. -
lint:precommitfail-fast path: deleted./custom-gcl, confirmed the same error message,
nonzero exit, and — the most important regression check — that./custom-gclis not
created as a side effect. -
Staged-patch branch and
--new-from-revbranch 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-commitframework (binary-missing
failure path and binary-present passing path). -
atmos lint lintroller/atmos lint gomodcheck/atmos lint custom-gclall verified through the
realatmosbinary with correct exit-code propagation. -
atmos test(full suite) passes. -
goimports→gciswap 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 formattersconfirmsgciis active andgoimportsdisabled after the swap. -
This PR's own commits went through the new pre-commit hook wiring live (
go-fumpt,
golangci-lintviago 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 Erik Osterman (Cloud Posse) (@osterman) (#2923)
what
- New tutorial
website/docs/tutorials/centralized-auth-config.mdx: centralize an organization's Atmosauth:config in one private repo andimport:it into every project, with side-by-side AWS/Azure/GCP examples. - Fixed
CustomGitDetector.resolveTokento fall back togh auth token(GitHub CLI) for privategit::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.ttlcaching across every remote import form. It previously covered onlygit::imports that use a subdirectory; it now also covers plain remote URLs andgit::imports without a subdirectory, without touching the sharedpkg/cachepackage's behavior for its other (unrelated) consumers. - Supporting docs:
website/docs/cli/configuration/imports.mdx(caching section), changelog postwebsite/blog/2026-08-11-remote-import-github-auth-and-caching.mdx(written in ASD-STE100 style), a roadmap milestone, and fix-log records underdocs/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 Erik Osterman (Cloud Posse) (@osterman) (#2942)
what
- Container components, one-shot
atmos container runcontainers, and stack-scoped workflowtype: container, action: runsteps 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 networkdocker composecreates 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), fixingconnection refusedwhen reproducing a socket-mounted job container locally. - Fixes the Native CI
atmos terraform testsummary 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 Erik Osterman (Cloud Posse) (@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'sTestCLICommandsnow deterministically assigns each CLI test case to a shard by hashing its name, gated byATMOS_TEST_SHARD/ATMOS_TEST_SHARD_COUNTenv vars (a no-op locally when unset -atmos test --fullstill runs everything)..github/workflows/test.yml'stestjob matrix expands toflavor x shard(1..10).TestTerraformRegistryCachenow runs once per OS (shard 1 only, guarded bymatrix.shard == 1) instead of implicitly once per job. Added atest-requiredaggregator job (mirrors the existingk3s-requiredpattern) so branch protection can key off one stable check name regardless of shard count.scripts/collect-coverage.shwrites to a configurableCOVERAGE_OUTpath and pins-covermode=atomicexplicitly. Thecoveragejob now downloads all 10 Linux shard coverage files and hands them tocodecov-actionin a single call so Codecov aggregates line hits server-side, instead of uploading one Linux-onlycoverage.out.
why
- The acceptance suite's runtime was dominated by ~388 sequential CLI-driven subtests in the
testspackage (not.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 John C. Bland II (@johncblandii) (#2938)
## what- Remove
@cloudposse/approversas 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_reviewenforced, any PR touching a matching file demands an approval
from theapproversteam in addition to engineering review. The bareREADME.mdpattern 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
approversrequirement so engineering review alone suffices
references
- #2934 (currently blocked on this requirement)
chore: upgrade actions to Node 24 runtime (SHA-pinned) John C. Bland II (@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 usesinstall-mode: nonewith a custom-built golangci-lint from a Nov-2025 (v2-era)
commit, which v9 supports
- Re-pin the
cloudposse/.githubshared-workflow refs (shared-go-auto-release.yml,
shared-release-branches.yml) from8244c7c9 # mainto current main49ac8cd5 # 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
toruns.using: node20 - Verified: every changed SHA matches its upstream tag / branch head, this repo's own
verify-sha-pinningtest suite passes (28/28) over the modified tree, and actionlint is clean
references
still on Node 20
bobheadxi/deployments@648679e8 # v1— latest release is still node20; no node24 version to pin
https://github.com/bobheadxi/deploymentscloudposse/github-action-seek-deployment@9c18326d # 0.1.1— node20; runtime bump tracked in
cloudposse/github-action-seek-deployment#29 (needs a maintainer with push access)
feat(profiles): interactive multi-select for bare --profile Erik Osterman (Cloud Posse) (@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
NoOptDefValbare-flag mechanism — previously hardcoded toStringFlagonly — to also work forStringSliceFlag, across the flag registry, parser, and preprocessing pipeline. - Adds a
ProfileSelectordependency-injection seam inpkg/config, fulfilled bypkg/flagsatinit()time, so the interactive picker can be wired in without creating an import cycle (pkg/flagsandpkg/profileboth already importpkg/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
--profilewas aStringSliceFlag, and that flag type never supported the bare-flag sentinel pattern--identityalready used — soatmos auth login -i <identity> --profile(expecting an interactive prompt like-igives) instead errored with a confusing, low-level pflag message.- Users shouldn't need to memorize exact profile names or run
atmos profile listfirst just to activate one.
references
N/A
feat(vendor): add --stack/--labels flags, fix --tags selector bugs Erik Osterman (Cloud Posse) (@osterman) (#1889)
what
- Add
--stack/-sand--labelsselector flags toatmos vendor pull: vendor every component
declared in a stack (or matching stackmetadata.labels) that has its owncomponent.yaml,
bypassingvendor.yamlentirely for installation. - Extend
--tagsacross all five vendor subcommands (pull,diff,clean,update,verify)
to compose as an independent filter with--componentor--stack/--labels, instead of being
mutually exclusive with them — narrows whichever base selector resolved byvendor.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
--componentnow reports "not defined" instead of a misleading tags-mismatch
message when--tagsalso happens to match nothing. - Repeated
-c/--componentonvendor pullis now rejected instead of silently keeping only
the last value (--componentis now a slice, matchingvendor update's flag). --stack/--labelsnow warns when a resolved component also has avendor.yamlentry,
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-gitsymlink/path
traversal,dompurifyXSS,nanoidDoS); the remaining 2 (image-sizeDoS) 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--componentinvocations, 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*.go→pkg/vendor/). While this branch was in flight,origin/main
independently shipped a more complete rewrite of the same internals intopkg/vendoring/
(lockfile, SBOM/provenance, native component-updater PR workflow — #2756). That refactor has
been dropped from this PR to avoid duplicating it;--stack/--labels/--tagsare implemented
directly on top of main's currentpkg/vendoring/internal/execarchitecture. docs/fixes/2026-08-07-vendor-selector-flag-consistency.md— the--tagscomposition 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 Jorrit Elfferich (@jorrite) (#2928)
what
- Adds
spec.files[].matrixfor dynamic per-combination file generation in scaffold templates: a file entry declaringmatrix:expands into one generated file per combination of one or more axes, reusing the exact shape the workflowmatrix: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
collectKeystemplate function for computed axes:collectKeys(m)returnsm's sorted top-level keys;collectKeys(m, "nestedKey")collectsnestedKey's own keys from every value inm, flattened and deduplicated. Registered under its own name so it doesn't shadow Sprig's ownkeys, 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>intarget:, inwhen:(via thematrixCEL variable), and in the file's own rendered content. - New
examples/scaffolding-matrixexample (one concept: a multiselect field driving a single matrix axis), keepingexamples/scaffoldingitself matrix-free. Comprehensive testing (all three axis kinds, plus deliberately-wrong inputs) moved to dedicatedtests/fixtures/scenarios/scaffold-*fixtures rather than piggybacking on the end-user example. - Updates the
atmos-scaffold/atmos-templatesagent skills, the CLI reference docs, and the file-browser plugin so all three are current withmatrix/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 Mikhail Shirkov (@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/gkewith requiredproject_id,location, and clustername - describes the exact
projects/{project}/locations/{location}/clusters/{name}resource through the native GKE API using Atmos-issuedGCPCredentials - reuses the shared kubeconfig writer for merge, replace, error, no-op, and cleanup behavior
- adds
atmos gcp gke tokenas the kubeconfig exec plugin without persisting bearer tokens - exposes
KUBECONFIGandKUBE_CONFIG_PATHthrough the Auth identity environment - authenticates the upstream chain for provider-backed
gcp/projectidentities - 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 planandhelm diff - explicit
--against=release helm apply/helm deployhelm 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: trueArchitecture 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 execprovisioned kubeconfig without preparatorygcloudcommands - 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
gcloudandgke-gcloud-auth-pluginabsent fromPATH
Validation
go test ./pkg/component/helm -count=1go test ./pkg/auth/... -count=1go test ./internal/exec ./pkg/datafetcher -run 'Auth|GKE|GlobalAuth|Schema' -count=1git 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 Erik Osterman (Cloud Posse) (@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 Erik Osterman (Cloud Posse) (@osterman) (#2894)
what
- Adds
atmos toolchain update [tool...]to move a pinned tool to its newest available version and reinstall it, with--dry-runand bounded--max-concurrency. Tools pinned topr:/sha:/ref:are skipped with an explanation instead of silently left alone. - Fixes
which/execresolving the wrong version (last token instead of the default first token) on a multi-version.tool-versionsline, which caused false "not installed" errors. - Fixes
setappending instead of replacing the default version, contradicting its documented behavior. - Fixes
add/installsilently 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 towarddependencies.tools/atmos version track. - Fixes
atmos version track add/setcorrupting any value containing<,>, or&(ajson.MarshalHTML-escaping bug), which broke the exact~>/>=constraint syntax the toolchain docs recommend. - Fixes
atmos toolchain versions --helpsilently rendering the wrong command's help and exiting 0 instead of erroring; fixed globally in root help routing (atmos <cmd> <bogus-subcommand> --helpnow errors for every command tree). Removes the staletoolchain-versionsandtoolchain-aliasesdocs/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
updateToolVersionsFilewriting to the hardcoded default.tool-versionspath instead of the configured one. - Adds a changelog post and roadmap entry for the new
updatecommand.
why
- A field test of
atmos toolchainsurfaced 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 inwhich/exec, which led to finding the rest of the bugs above along the way — a documentedsetbehavior 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 letatmos toolchain versions --helpsilently 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 Jorrit Elfferich (@jorrite) (#2925)
what
- Adds a
--merge-driverflag (auto/text) toatmos scaffold generate --updateandatmos init --update, alongside the existing--merge-strategyflag. auto(default) is today's existing behavior: pick the merger by file extension (YAML-aware for.yaml/.yml, line-oriented text otherwise).textforces 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 Michael Pursifull (@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.mdalready selected vz formacos-15-inteland records qemu failing there onusernet unable to resolve IP for SSH forwardingbefore 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-minutebrew install qemucap) 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 completedif, which reads theifstatement's own zero when no branch executes, so the function returned success after every attempt had failed and the job died later atdocker versionwith a misleading error. The status is now captured in theelsebranch. docker pull rancher/k3s:latestmoves 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 statusandlimactl listcapped 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 Erik Osterman (Cloud Posse) (@osterman) (#2790)
what
- Adds
atmos azure aks token,atmos azure aks update-kubeconfig, andatmos azure acr login, mirroring the existingatmos aws eks/atmos aws ecrintegrations. - Generalizes
pkg/auth/cloud/kube.KubeconfigManagerfrom 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/.Registryschema structs (renamed fromEKSCluster/ECRRegistrytoCluster/Registry) sospec.cluster/spec.registryare reused verbatim acrossaws/eks+azure/aksandaws/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
kubectland Docker credentials in one step viaatmos auth login. Azure had the same auth foundation (providers, identities) but no equivalent for AKS/ACR, so Azure users still needed theazCLI — and for AAD-enabled clusters, the separatekubeloginbinary — 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 tokeninstead ofkubelogin; ACR login is a plain OAuth2 token exchange, matching whataz acr logindoes 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.6Both 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 Erik Osterman (Cloud Posse) (@osterman) (#2897)
what
atmos config setandatmos stack setnow default--typetoauto: 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 setpreviously never inferred at all — every value was stored as a string unless--typewas passed explicitly.- Fixed
PickProvenanceFile(shared byatmos stack set/get/delete/listand the AI/MCP tools layer) always picking the last provenance entry, which for any value defined only in an imported catalog file was a phantomLine:0entry pointing at the wrong (importing) manifest instead of the file that actually defines the value. - Fixed
MCPSettings.Enabled(abooltaggedomitempty) silently disappearing from the merged config whenatmos.yamland anatmos.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.yamlonatmos config get/set/delete/formatnow warns that only the first file is targeted, instead of silently dropping the rest.atmos config geton a key defined only in anatmos.d/fragment now hints to checkatmos describe configinstead of just reporting "not found".- The
unsetalias (forconfig/stack delete) now shows up in--helpoutput, alongsidedel. - 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 configandatmos stack/atmos stack configsurfaced 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 Erik Osterman (Cloud Posse) (@osterman) (#2882)
what
- Adds
dependencies.commands/dependencies.workflowsto custom commands and workflows: named, parameterized, concurrent-by-default dependency ordering across units, with automatic dedup of identical invocations. - Adds
inputs/artifactsstep fields: skip a step when its declared sources haven't changed since the last successful run (implicitwhen: checksum.changed), exposingchecksum.changed/timestamp.changed/sources/artifactsaswhen:CEL facts. - Adds
preconditionsstep field: skip a step when a required tool is already onPATH(implicitwhen: "!preconditions.success"), resolved viaexec.LookPath— no shell involved. Pluralized (precondition→preconditions) to match the block-of-checks convention already used byinputs/artifacts/dependencies. - Adds
continue: alwaysstep 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: matrixsteps silently failing in custom commands (only workflows supported them before). - Adds
platformsviawhen:CEL facts (os/arch/platform), native per-commandaliases:/internal:, and avalues: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 verbatimCmdLineconstruction forcmd.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
UnitDependenciesstring-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.goandcmd/custom_command_values.gointopkg/taskgraph/adaptersandpkg/flagsrespectively, so this logic is unit-testable in isolation instead of coupled tocmd'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 Andriy Knysh (@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=Atmostags
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/nameinto the shared backend registry viainit(), so thebefore.terraform.inithook andatmos terraform backend create/deletepick upazurermwith no wiring changes to the hook or CLI. - A narrow
azureBackendAPIinterface hides the ARM SDK pollers behind synchronous methods for testability, mirroring the S3 client factory and the existingazurermstate-reader wrapper. A test-injectable client factory (SetAzureBackendClientFactory/ResetAzureBackendClientFactory) mirrorsSetS3ClientFactory. - Location is sourced from the active Azure identity (or an existing resource group), never from the
backendblock — it is not a validazurermbackend argument and Terraform would reject it inbackend.tf.json. - Adds
armresources+armstorageSDK deps (azcore/azidentity/azblobwere 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:PrepareEnvironmentgates the tenant export onUseOIDC.pkg/auth/providers/azure/oidc.go: OIDC override re-addsARM_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, mockedazureBackendAPIand Azure SDK fake servers — config extraction/precedence,use_azuread_authparsing, 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.goandoidc.goPrepareEnvironmentare 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, soprovision.backend.enabled: truecan be left in place.
fix(ai): close DX gaps found in atmos ai field test Erik Osterman (Cloud Posse) (@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.atmosversion-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 --clientvalues instead of silently no-op'ing, by extendingpkg/flags'sWithValidValuesto work on string-slice flags generically (this also fixed a latent bug whereWithValidValueswas silently dead for every command that binds flags viaBindFlagsToViperwithout calling the fullParse()pipeline). - Warns when
--pathis combined with--client/--scope/--global/--all-clientsonskill install, since--pathskips 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 comparisonupdatenow acts on). - Fixes
agent-skills/skills/atmos-ai/SKILL.mddoc drift (it never documentedskill installat all) and removes a phantominfosubcommand fromatmos ai skill --help. - Adds local-path/
file://support to skill source parsing and the downloader. - Documents
--scope/--globalprecedence onskill install/uninstall/update. - Makes
atmos ai exec/ask --sessionactually persist and resume conversations — previously a documented flag that was a complete no-op. - Resolves a session's
Modelfrom the constructed AI client instead of an independent config lookup, fixingsessions export/importfor the default zero-configclaude-codeprovider path (previously exported checkpoints for that path could never be re-imported). - Applies
--mcpserver 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
--formatvalues onai execinstead of silently falling back totext. - Behavior change:
ai execcan 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 0dnow 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!!omapresolution) andmermaid(5 advisories), viapnpm.overridesbumps within their existing major versions. No CodeQL alerts were open. - Fixes a flaky
TestManager_ExportSession_WarnsOnUnimportableCheckpointCI failure: the test asserted on raw ANSI-styledui.Warning()output, which the formatter renders as two adjacent styled runs underCI=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 aicommand 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
0dinstead 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 updateexists 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 updatecommand that was initially scoped out and then built once asked for.
feat(ai): browsable, searchable Agent Skills Directory Erik Osterman (Cloud Posse) (@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-browserDocusaurus 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.mdand every nested reference file into one clipboard-ready document, so its full context can be grabbed without installing it. atmos ai skill listgains a--formatflag (table/json/yaml/csv/tsv) and a Category column.- Adds a
SkillCountcomponent that renders the live, build-time skill count inline in prose (homepageAISection, docs), so counts can't drift out of date again. - Restructures the sidebar nav (
Atmos AI→Skillscategory 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.mdfiles 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 Erik Osterman (Cloud Posse) (@osterman) (#2874)
what
kubernetes.gitops.provision.targets.<name>(kind: git) now supports asplittri-state:split: falsewritespathas a single merged multi-document YAML file instead of always treatingpathas a directory of auto-named files; unset infers the mode from whetherpath's last segment looks like a manifest filename (.yaml/.yml/.json).- Atmos's structural manifest validator no longer requires
metadata.nameon Kustomize's ownKustomization/Componentobjects (matched againstsigs.k8s.io/kustomize/api/types's own kind/version constants), since Kustomize's own schema and field-enforcement never require one. - A new
validate: falsecomponent-level flag opts a component out of both the apply/deploy structural auto-gate and the standaloneatmos kubernetes validatecommand. - Docs: new "Generating a Kustomize component for GitOps" walkthrough,
splitdocumented onkubernetes-deploy.mdx, and the Kustomize exemption /validate: falsedocumented onkubernetes-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
pathwas always treated as a directory, so configuringpath: ".../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.nameunconditionally, forcing users to add a meaningless name to KustomizeComponent/Kustomizationobjects 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 properkustomization.yamlfor Argo CD/Flux to consume.
feat(toolchain): support Aqua `github_archive` package type Erik Osterman (Cloud Posse) (@osterman) (#2416)
what
- Add
github_archivepackage 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 upstreamaquaproj/aquasemantics. - Hardcode
tar.gz(mirroring aqua'sGetFormat());asset,url,format, andformat_overridesare intentionally ignored for this type. - Extend
resetByPkgTypeso version overrides that switch a tool togithub_archiveclear staleasset/url. - Add unit + registry-parsing tests plus a fixture modeled on the
adr-toolsexample; cover validation, version-prefix handling, format-ignored semantics, URL pattern, and{{trimV .Version}}/pathtemplate idiom. - Add changelog blog post and a shipped milestone under the Extensibility initiative on the roadmap.
why
- Aqua's upstream registry uses
github_archivefor tools shipped as repository source archives (e.g.,adr-tools,tfenv,tgswitch, and many single-script projects). Without this type, those entries failed withunsupported 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 Erik Osterman (Cloud Posse) (@osterman) (#2953)
what
- Adds the missing
consul,cos,http,kubernetes,oss,pgbackend types to the stack-manifest JSON Schema'sbackend_type/remote_state_backend_typeenums andbackend_manifest.propertiesallow-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-levelretry:(terraform/helmfile/packer/kubernetes), component-levelrequired_version/required_providers,source.ttl, andcontainer_runtime.provider: "auto". - Removes
"raw"fromworkflow_step.output.mode's enum — the schema accepted it but Go'svalidateParallelOutputnever did, so it passed validation and then failed at execution time. - Fixes a root-schema
oneOfbug that madeworkflows:and ordinary stack fields (vars:,settings:, etc.) mutually exclusive in one manifest, producing an uninformative(root): valid against schemas at indexes 0 and 1error. - Adds a Go-level check (
internal/exec/stack_processor_backend.go) that errors whenbackend_type/backend(orremote_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 attests/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, includingterraform planand custom commands) hard-rejects abackend_type: httpmanifest even thoughhttpis 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 stackshas 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 wheneverschemas.atmos.manifestwas 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
oneOffix 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
httpas a validbackend_typefor the same issue. This PR covers that same schema change (httpinbackend_type/remote_state_backend_typeandbackend_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 Rémy Macherel (@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 Erik Osterman (Cloud Posse) (@osterman) (#2950)
what
- Add
required_version,required_providers, andretryto theterraform,terraform_component_manifest, and sharedoverridesdefinitions in theatmos-manifestJSON Schema. - Add
generateto the stack-levelkubernetesdefinition (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-trackedtopLevel:helmgap. - Fix
pkg/datafetcher/schema_section_coverage_test.go:required_version,required_providers, andretrywere 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 theatmos-stacks-validationfixture with real usage of every fixed field soTestValidateStacksCmd_Successis a live regression guard for this bug class.
why
atmos validate stacks(broken since v1.224.0) andatmos describe stacks(broken since v1.225.0) rejectrequired_version/required_providersundercomponents.terraform.<name>, even though the fields are fully implemented and documented (added by #1841).- Root cause:
required_version/required_providerswere added to the legacystack-configschema by #1841, but never ported toatmos-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 wheneverschemas.atmos.manifestwasn'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: falserejection 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 thekubernetes.generateand 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/retryas non-manifest ("introspected, not authored") let this exact regression through undetected. Fixing the classification, not just the schema, closes the actual hole.
references
fix(docker): build the arm64 image with an arm64 userland Michael Pursifull (@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=$BUILDPLATFORMpin on the runtimeFROMso the Debian
base and all apt-installed tools followTARGETPLATFORM. - 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/arm64image contained an amd64 userland and failed with
exec format erroron arm64 hardware. Onlyatmosandkustomizewere
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
- closes #2931
fix(git): tolerate config errors for CI git-clone bootstrap pre-Cobra Erik Osterman (Cloud Posse) (@osterman) (#2879)
what
- Fixes
atmos git clonefailing before it ever attempts a clone in a fresh CI workspace, when a referenced config profile doesn't exist yet (e.g.ATMOS_PROFILE=githubwith no.atmos/profiles/checked out) —ATMOS_CI=truehad no effect on this failure. - Adds a combined regression test case to
pkg/container's build-arg builder coveringengine,driver,cache, customdockerfile/context, andtagstogether in one config (previously only tested individually).
why
cmd/root.go'sExecute()runs an initialcfg.InitCliConfigbefore Cobra resolves any subcommand. Only the secondInitCliConfigcall (insidePersistentPreRun) 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 aprofile not founderror aborted the process before Cobra — and therefore beforePersistentPreRun— ever ran, regardless ofATMOS_CI.- Adds
isCIGitCloneBootstrapArgs(anos.Args-based equivalent of the existing Cobra-aware bootstrap check) to the pre-Cobra handler, and a new exportedCIGitCloneModeRequestedFromEnvincmd/gitso both code paths defer to the sameATMOS_CI/CI-provider resolution logic. - The container test addition closes the one remaining gap in
buildBuildArgscoverage: 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) Andriy Knysh (@aknysh) (#2941)
what
Four independent fixes to the native Helm implementation (pkg/component/helm):
-
1. Repository config/cache isolation.
newSettingsnow points Helm'sRepositoryConfigand
RepositoryCacheat an atmos-managed XDG location (<xdg-config>/atmos/helm/repositories.yaml,
<xdg-cache>/atmos/helm/repository) unlessHELM_REPOSITORY_CONFIG/HELM_REPOSITORY_CACHEis set,
instead of inheriting the user's global Helm config. -
2. Status output on apply/delete.
atmos helm applyandatmos helm deletenow print a one-line
status (release name, namespace, chart) instead of succeeding silently. -
3. Stack default-identity resolution.
atmos helm apply/diff/deleteresolve the stack's
default: trueidentity binding the same wayatmos terraformdoes, so an explicit--identityis
no longer required for cluster operations. The offlinetemplaterender never triggers auth. -
4. Namespace for namespace-less charts.
newActionContextnow sets the namespace on the Helm
EnvSettings(SetNamespace), so charts whose manifests omitmetadata.namespaceinstall 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 downdownloader.(*ChartDownloader).scanReposForURL, which iterates every repository
in the user's globalrepositories.yamland 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, andsetupHelmRepositoriesalso 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/diffalready 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, noKUBECONFIGwas 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 keepingtemplatefully offline. When no auth is configured, the identity stays empty
and the ambientKUBECONFIGis 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 hardcodenamespace: {{ .Release.Namespace }}worked, but charts that do not landed indefault.
testing
Automated (in-code, pkg/component/helm)
repo_isolation_test.go- isolation is applied when theHELM_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
whiletemplatestays offline.namespace_test.go-newActionContextsets 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 ofdefault.
references
docs/fixes/2026-08-14-native-helm-ux-fixes.md
fix(secret): inherit the component's default identity for stores Juan A. (@jaguer0) (#2746)
what
atmos secret(set/get/init/validate) now inherits the component's effective identity for store-backed secrets whose store declares no explicitidentity:, instead of falling back to the default AWS credential chain (→ EC2 IMDS, which fails off-EC2).
why
injectSecretStoreAuthResolver(cmd/secret/shared.go) calledatmosConfig.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 callSetAuthContextResolverWithDefaultIdentity; the secret CLI even computed the sameDefaultIdentity(intoSecretsAuth) 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 (defaultIdentityForStoreonly fills empty-identity stores).atmos terraformandatmos secret listbehavior 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) Erik Osterman (Cloud Posse) (@osterman) (#2892)
what
Fixes #2888 — deferred YAML functions silently losing data on merge
- Every production call site of
ApplyDeferredMergespassedprocessor = 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-wideProcessCustomYamlTagspass 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-collidingvars.foo: !exec
ran the shell 2× on this branch vs 1× onmain. - Fix (
pkg/merge/merge_yaml_functions.go): for a single-contribution (no-collision) deferred path
whose value is already resolved in the result,ApplyDeferredMergesnow 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
StackComponentDeferredContextsandAllStacksDeferredContextsin place
of the rawmap[string]map[string][...]ComponentDeferredContextssignatures 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
includinglocal(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!execfixture and end-to-end assertions throughExecuteDescribeComponent).
references
- Closes #2888
Fix const variable interpolation in Terraform module sources Marko Petrovic (@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.Modulein 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/execsuite 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 Jorrit Elfferich (@jorrite) (#2869)
what
- Fix
atmos scaffold generate/atmos initrecording a dangling, already-deleted temp-directory path inspec.sourceof.atmos/scaffold.yamlwhenever the template source is remote (git::...or a barehttps://...URL). - In the file
pkg/generator/source/resolver.go: inresolveRemote(), setconf.Source = src(the original source string the caller passed in) after loading the template configuration from the temporary download directory, instead of leavingConfiguration.Sourceas whateverLoadConfigurationFromDirwas given (the temp dir itself). pkg/generator/source/resolver_test.go: added/extended tests assertingConfiguration.Sourceholds 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 intoos.MkdirTemp("", "atmos-scaffold-"), then loaded the config with that temp dir passed in as the "source" — soConfiguration.Source, and
therefore the persistedspec.source, ended up holding something like/var/folders/xx/.../atmos-scaffold-1234567890. That directory is removed bycleanup()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 contradictsSaveProjectRecord'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 inresolveLocal, not because anything special-cased provenance for them. - Reproduced directly:
atmos scaffold generate "git::https://.../scaffold-template.git" ./out --defaults, thencat ./out/.atmos/scaffold.yamlshows a/var/folders/...//tmp/...path forspec.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 Jorrit Elfferich (@jorrite) (#2891)
what
- Fix
atmos scaffold generate --updateunconditionally 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 onenewlineSeparator("\n") to each ofours/base/theirsbefore handing them todiff3.Merge, sodiff3'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
ConflictStrategyhandling — out of scope, and unaffected since the appended newline is identical across all three inputs.
why
TextMerger.Merge()delegates the actual 3-way merge toepiclabs-io/diff3, which reads each ofbase/ours/theirsline-by-line viabufio.Scanner(ScanLines, Go's standard-library default split function) and rejoins the merged lines withstrings.Join(lines, "\n").ScanLinesstrips 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 throughGetLines+Joinalways 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--updatewith 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 totheirsmakes an otherwise-identicalours/theirspair (a very common no-op shape) differ by one trailing newline as far asdiff3is concerned, which turns a no-op into a spurious detected change/conflict instead of fixing anything.
references
- Closes #2887.
fix(version): exclude draft GitHub releases from Version Tracker resolution Erik Osterman (Cloud Posse) (@osterman) (#2900)
what
pkg/github.GetReleasesnow excludes draft GitHub releases unconditionally, alongside the existing prerelease filter. This fixes the Version Tracker'sgithub-releasesdatasource resolver,atmos version list, andGetReleaseVersions, which all share this function.- Removes the deprecated
atmos version track rendersubcommand. It was markedDeprecated/Hiddenin the same commit that introduced it and has never had a non-deprecated existence in any release, so no migration path is needed. The sharedrenderTemplatehelper moves intoapply.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--affectedvalidation. - Expands the
version.filesand!versionfunction docs with worked before/after examples for themarkerandgithub-actionsfile managers (including SHA pinning) and adds Helm/Container-component examples alongside the existing Terraform one.
why
atmos version trackwithdatasource: github-releasesanddesired: latestcould 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_TOKENin a repo's own CI). Reproduced againstcloudposse/atmositself:atmos version track lockresolved tov1.226.0, whichgh release view v1.226.0 --repo cloudposse/atmos --json isDraftconfirmed was a draft, when the real latest published release wasv1.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 renderwas superseded byapply/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
!versionbehavior easier to learn from concrete examples rather than a single terse case.
fix(config): honor --config across internal reloads and multi-file merges Erik Osterman (Cloud Posse) (@osterman) (#2875)
what
- Internal reloads of the CLI config (many call sites across
internal/exec,pkg/vendoring,cmd/, etc. callingInitCliConfig(schema.ConfigAndStacksInfo{}, false)) now fall back to parsing--config/--config-path/--base-pathfromos.Args/env instead of silently discarding the selection made at startup. - A second
--configfile 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 getnow reports the effective, fully-merged configuration for the invocation (all--configfiles,--config-pathdirs, and profiles applied) instead of reading a single physical file.VendorDirAbsolutePath/WorkflowsDirAbsolutePathare now precomputed once (mirroring the existing top-levelbase_pathresolution), so vendor/workflow path joins no longer re-derive a possibly still-relativeBasePath.
why
atmos --config <file> terraform plan/testwas failing withfailed to find importeven thoughatmos --config <file> list stacksworked with the identical flag, because a downstreamInitCliConfigre-invocation lost the--configselection mid-command.- Splitting config across two
--configfiles with a conflicting array value madestacks.included_pathsunusable for stack discovery, whileatmos config getmisleadingly reported the config as unchanged.
references
fix(steps): resolve relative paths against step.WorkingDirectory Erik Osterman (Cloud Posse) (@osterman) (#2880)
what
- Fix
type: archive,file,workdir,junit, and containerbuildstep handlers to resolve relativesource/destination/path/files/context/dockerfilefields againststep.WorkingDirectoryinstead of the Atmos process's own cwd. - Add a shared
BaseHandler.ResolveInWorkingDirectoryhelper (pkg/runner/step/handler_base.go) used by all five handlers; containerbuildadditionally anchorsDockerfileto the resolvedContext, 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: archivesteps run as component lifecycle hooks ignoredstep.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, containerbuild) 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 Andriy Knysh (@aknysh) (#2890)
what
- Store the seeded Azure management access token in the Azure CLI MSAL cache with all ARM scope forms in its
targetfield — the modern scope (https://management.azure.com/.default) plus the legacy audience forms (https://management.core.windows.net/.defaultand the double-slash variant), with matching forms for the US Government and China clouds (newLegacyManagementScopesfield onCloudEnvironment). - 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 (newCopyAtmosRefreshTokensInto;UpdateAzureCLIFilesgains arealmparameter). 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 viaAzureCLICredentialrequest an ARM token for the legacy audiencehttps://management.core.windows.net/(the azidentity/azapi default). The cache write-back only seeded the modern scope, so MSAL's cache lookup missed andazapi-based modules (all modern Azure Verified Modules) failed mid-apply withAzureCLICredential: ERROR: Can't find token from MSAL cache— whileazurermresources 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 byaz— seeding it letsazself-mint tokens for any audience and survive access-token expiry. - Until now the workaround was to run a real
az loginalongsideatmos auth login, defeating the purpose of single-command auth.
Manually verified end-to-end on a real Azure tenant:
- Logged out completely and wiped all caches:
az logout,az account clear, removed~/.azure/msal_token_cache.jsonand~/.azure/atmos/(confirmed withaz account showfailing). - Ran
atmos auth loginalone — noaz loginat any point. - Confirmed the refresh token was copied into the Azure CLI cache:
jq '.RefreshToken | length' ~/.azure/msal_token_cache.jsonreturned1(previously0). - Requested a token for the legacy ARM audience — the exact request
azidentity/azapimake:az account get-access-token --resource https://management.core.windows.net/succeeded (previously failed withCan'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. - Ran
atmos terraform planon anazapi-heavy component (the exact field failure): refresh and plan completed clean with no MSAL errors.
references
- Follow-up to #2861 and #2862 (Azure auth +
azure/interactiveprovider, shipped in v1.225.0) - azidentity legacy ARM audience default: https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity
- MSAL cache scope (subset) matching: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
fix(kubernetes): close gaps found field-testing Kustomize GitOps delivery Erik Osterman (Cloud Posse) (@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 byatmos kubernetes validate/apply/deploy, leaving validation enabled with no warning. It now fails closed with a clear error.provision.targets.<name>.splithad 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) — sosplit: "yes"passedatmos validate stacksand was silently dropped at runtime, falling back to path-based auto-inference. The embedded schema is now synced, and the git target'sparseConfigalso fails closed as defense in depth (sincekubernetes validate/apply/deploydon'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
RemoveAllthat 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.reconcilenow 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-gitskill doc: it showed a nestedsigning: {mode: auto}instead of the real flatsigning: autostring field — copying it verbatim fails to parse. - Documented that
atmos kubernetes validate --serverfails 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 thoughapply/deployof 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 Erik Osterman (Cloud Posse) (@osterman) (#2884)
what
- Bump
cloudposse/github-action-docker-build-pushfrom v3.0.0 to v3.1.0 in the releasedockerjob (.github/workflows/build.yml). - Explicitly override the action's
binfmt-imageinput tomirror.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 frompublic.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'sbinfmt-imageinput still defaults topublic.ecr.aws/eks-distro-build-tooling/binfmt-misceven at v3.1.0 (no upstream fix yet), so it's overridden here directly to the equivalent Google-mirroredtonistiigi/binfmtimage, which publishes the sameqemu-v7.0.0tag. Verified live: bothmirror.gcr.io/tonistiigi/binfmt:qemu-v7.0.0anddocker.io/tonistiigi/binfmt:qemu-v7.0.0resolve to the same digest and pull successfully;mirror.gcr.ioalso 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.writeLineacquired/released the shared output mutex per line, so a singleWrite()call that produced multiple lines (e.g. a hook's buffered\r-then-\nprogress update) could have a different node's writer interleave a line in between, corrupting concurrent Terraform output. Reproduced withgo test -race -count=200before 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.