Skip to content

Add atmos describe affected CLI command. Update docs website - #274

Merged
Andriy Knysh (aknysh) merged 49 commits into
masterfrom
add-describe-affected-cli-command
Dec 15, 2022
Merged

Andriy Knysh (aknysh) merged 49 commits into
masterfrom
add-describe-affected-cli-command

Conversation

@aknysh

@aknysh Andriy Knysh (aknysh) commented Dec 14, 2022 •

Copy link
Copy Markdown
Member

what

why

  • The command atmos describe affected produces a list of the affected Atmos components and stacks given two Git commits. The command compares the final component sections (after all imports and deep-merging) for all Atmos components in all stacks, and produces a list of affected (changed) components in the stacks. The command also checks the changed files b/w the two commits and checks if the Terraform/Helmfile component folders are changed.

For the first commit, the command assumes that the repo root is a Git checkout (and throws an error if the repo is not a Git repository, does not have .git folder).

The second commit is specified on the command line using the --ref and --sha flags.
The --ref flag supports all standard Git References (https://git-scm.com/book/en/v2/Git-Internals-Git-References).

the ref will be the default branch (e.g. main) and the commit SHA will point to the HEAD of the branch.

atmos describe affected  --verbose=true

Cloning repo 'https://github.com/cloudposse/atmos' into the temp dir '/var/folders/g5/lbvzy_ld2hx4mgrgyp19bvb00000gn/T/16710538942745756531'

Checking out the HEAD of the default branch

Enumerating objects: 4138, done.
Counting objects: 100% (1080/1080), done.
Compressing objects: 100% (538/538), done.
Total 4138 (delta 611), reused 854 (delta 482), pack-reused 3058

Checked out Git ref: refs/heads/master

examples

atmos describe affected
atmos describe affected --verbose=true
atmos describe affected --ref refs/heads/main
atmos describe affected --ref refs/heads/main --format json
atmos describe affected --ref refs/tags/v1.16.0 --file affected.yaml --format yaml
atmos describe affected --ref refs/heads/my-new-branch
atmos describe affected --sha 3a5eafeab90426bd82bf5899896b28cc0bab3073 --file affected.json
atmos describe affected --sha 3a5eafeab90426bd82bf5899896b28cc0bab3073

test

If the current local branch changes a component (vars or env section) in any YAML stack config files that defines configuration for the component (either inline or via imports), the command shows all the affected components/stacks and the affected section:

atmos describe affected --ref refs/heads/main
[
   {
      "stack": "tenant2-ue2-staging",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant2-ue2-prod",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant2-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-staging",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-test-1",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-prod",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   }
]

If the current local branch changes any base component (including the abstract ones), the command shows all the affected real components including all the components derived from the changed base component. For example, if we changed the variables for the test/test-component (which is a base for many derived components), the command shows the affected base and all the derived components:

atmos describe affected --ref refs/heads/main
[
  {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-2",
      "affected": "stack.vars"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.vars"
   }
]

If the current local branch changes the env section (ENV vars) for any base or derived components, the command shows the affected real components with the affected_section: "env":

atmos describe affected --ref refs/heads/main
[
  {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant2-ue2-dev",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant2-ue2-staging",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant1-ue2-staging",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant2-ue2-prod",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant1-ue2-prod",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   },
   {
      "stack": "tenant1-ue2-test-1",
      "component_type": "terraform",
      "component": "test/test-component-override-3",
      "affected": "stack.env"
   }
]

If the current local branch changes a Terraform or Helmfile component (in components/terraform and components/helmfile folders), the command shows all the affected Atmos components/stacks and in the affected attribute it shows "affected": "terraform" if the Terraform code for the component has been changed, or "affected": "helmfile" if the Helmfile code for the component has been changed. For example, if we change the examples/complete/components/terraform/infra/vpc/main.tffile, it will affect all the Atmos components that provision the thevpc` Terraform component:

atmos describe affected --verbose=true
Cloning repo 'https://github.com/cloudposse/atmos' into the temp dir '/var/folders/g5/lbvzy_ld2hx4mgrgyp19bvb00000gn/T/16710736261366892599'

Checking out the HEAD of the default branch

Enumerating objects: 4215, done.
Counting objects: 100% (1157/1157), done.
Compressing objects: 100% (576/576), done.
Total 4215 (delta 658), reused 911 (delta 511), pack-reused 3058

Checked out Git ref: refs/heads/master

Local repo HEAD: 7d37c1e890514479fae404d13841a2754be70cbf refs/heads/add-describe-affected-cli-command
Remote repo HEAD: 40210e8d365d3d88ac13c0778c0867b679bbba69 refs/heads/master

Changed files:
cmd/describe_affected.go
examples/complete/Dockerfile
examples/complete/components/terraform/infra/vpc/main.tf
go.mod
go.sum
internal/exec/describe_affected.go
internal/exec/describe_component.go
internal/exec/describe_stacks.go
internal/exec/utils.go
internal/exec/vendor_utils.go
pkg/config/schema.go
pkg/describe/describe_affected_test.go
website/.nvmrc
website/docs/cli/commands/describe/describe-affected.md

Affected components and stacks:

[
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-dev",
      "component_type": "terraform",
      "component": "vpc/new",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-test-1",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-test-1",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-prod",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-prod",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-prod",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-prod",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-staging",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant1-ue2-staging",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-dev",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-dev",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-staging",
      "component_type": "terraform",
      "component": "vpc",
      "affected": "component"
   },
   {
      "stack": "tenant2-ue2-staging",
      "component_type": "terraform",
      "component": "infra/vpc",
      "affected": "component"
   }
]

image

@mcalhoun Matt Calhoun (mcalhoun) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <erik@cloudposse.com>
@aknysh
Andriy Knysh (aknysh) temporarily deployed to preview December 15, 2022 17:41 — with GitHub Actions Inactive

@Benbentwo Ben (Benbentwo) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@aknysh
Andriy Knysh (aknysh) merged commit c90dcb8 into master Dec 15, 2022
@aknysh
Andriy Knysh (aknysh) deleted the add-describe-affected-cli-command branch December 15, 2022 19:03
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 8, 2026
- go-git/go-git/v5: v5.19.1 -> v5.19.2 (GHSA alerts #270 high, #271
  medium; patched in 5.19.2)
- dompurify (website, transitive): pnpm override ^3.4.12 -> ^3.4.13
  (GHSA alert #272 medium; the existing override itself was below the
  patched version)
- nanoid (website, transitive): pnpm override ^3.3.15 -> ^3.3.17
  (GHSA alerts #274, #273 high; same issue -- prior override pinned
  below both patches)

Not fixed: image-size (alerts #276, #275, high) -- GitHub reports no
patched version exists yet for either advisory (first_patched_version
is null on both). Nothing to bump to; revisit once upstream ships a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 8, 2026
- github.com/go-git/go-git/v5: 5.19.1 -> 5.19.2, fixing a symlink
  traversal in worktree operations (GHSA-hc8v-wwc9-vgxm, high) and a
  path traversal in reference name handling (GHSA-qgq7-7hm3-q39j,
  medium). Alerts #270, #271.
- dompurify (website, transitive via pnpm override): 3.4.12 -> 3.4.13,
  fixing an IN_PLACE sanitization XSS via detached subtree hook
  removal (GHSA-55q2-fjhq-7xh7, medium). Alert #272.
- nanoid (website, transitive via pnpm override, both the direct
  override target and postcss's own dependency): 3.3.15/3.3.16 ->
  3.3.18, fixing two infinite-loop DoS issues with negative/zero size
  (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8, high). Alerts #273, #274.

Not fixed: #275/#276 (npm image-size <= 2.0.2, high) -- no patched
version exists yet upstream (first_patched_version is null on both
advisories); nothing to bump to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 8, 2026
Bump to patched versions, all within their existing major line (not
blocked by dependabot.yml's semver-major ignore):

- github.com/go-git/go-git/v5 v5.19.1 -> v5.19.2 (#270 high, #271
  moderate), pulled transitively via `go get` + `go mod tidy`;
  regenerated NOTICE
- dompurify 3.4.12 -> 3.4.13 (#272 moderate) via pnpm override
- nanoid 3.3.15 -> 3.3.18 (#273, #274 both high) via pnpm override;
  added a second override key matching postcss's own `^3.3.16`
  dependency range, which the existing `nanoid@3.3.3` key didn't catch

image-size (#275, #276, both high) has no patched version published
upstream yet -- not fixable.
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 8, 2026
- github.com/go-git/go-git/v5: v5.19.1 -> v5.19.2, fixing GHSA-hc8v-wwc9-vgxm
  (symlink traversal in worktree operations) and the companion malicious
  reference-name advisory (#270, #271). Patch-level bump, no API changes.
- website: dompurify pnpm override ^3.4.12 -> ^3.4.13, fixing an IN_PLACE
  hook removal XSS (#272).
- website: nanoid pnpm overrides bumped to ^3.3.17 (added a second
  selector, nanoid@^3.3.16, to also catch postcss's own nanoid range,
  which the existing nanoid@3.3.3 selector didn't match), fixing two
  indefinite-loop DoS advisories (#273, #274). The nanoid 5.x line was
  already patched by the existing override.
- NOTICE regenerated for the go-git/dompurify/nanoid version bumps.

Not fixed: image-size (#275, #276, both DoS via infinite loop) has no
patched release yet as of this commit -- GitHub's advisories list
"<= 2.0.2" as vulnerable with no first_patched_version. Left as-is
pending an upstream fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 8, 2026
- github.com/go-git/go-git/v5: 5.19.1 -> 5.19.2 (GHSA-hc8v-wwc9-vgxm/#270
  symlink traversal in worktree ops, GHSA-qgq7-7hm3-q39j/#271 path
  traversal via malicious reference names). Patch release, no API changes.
- dompurify: 3.4.12 -> 3.4.13 (GHSA-55q2-fjhq-7xh7/#272 IN_PLACE hook
  removal leaves a detached subtree executable).
- nanoid: broadened the 3.x pnpm override from an exact "3.3.3" pin to
  "nanoid@^3" so it also catches postcss's independent nanoid dependency
  (previously unpinned, resolving to a still-vulnerable 3.3.16), bumped to
  ^3.3.17 (GHSA-2v37-7h3g-55p8/#273, GHSA-28wg-ghj8-5hjv/#274).

image-size (GHSA-w3rx-r6r6-pgpr/#275, GHSA-5p2g-fcmc-qvqq/#276) has no
patched release yet (first_patched_version: null; confirmed against the
npm registry, latest is still 2.0.2) -- not fixable until upstream ships
one.

Regenerated NOTICE and website/pnpm-lock.yaml; verified the vulnerable
nanoid@3.3.16 resolution is gone post-regeneration. go build, go test for
every package importing go-git directly, and patch-scoped lint all pass.

Also fixes an EditorConfig indentation issue (list-continuation lines
need 2-space multiples) in the previous commit's fix-log doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request Aug 10, 2026
- github.com/go-git/go-git/v5 5.19.1 -> 5.19.2 (GHSA-hc8v-wwc9-vgxm
  #270, GHSA-qgq7-7hm3-q39j #271), pulling in patch bumps to
  golang.org/x/{mod,net,text,tools} via go mod tidy
- nanoid pnpm override widened from a pinned 3.3.3->^3.3.15 mapping to
  a ^3->^3.3.17 range so every 3.x requester resolves past both
  vulnerable versions (GHSA-28wg-ghj8-5hjv #274, GHSA-2v37-7h3g-55p8
  #273); previously two different 3.x versions were resolving
  simultaneously because the override only matched exact-version
  requests
- dompurify pnpm override 3.4.12 -> 3.4.13 (GHSA-55q2-fjhq-7xh7 #272)

image-size alerts #275/#276 (GHSA-5p2g-fcmc-qvqq, GHSA-w3rx-r6r6-pgpr)
have no first_patched_version yet in any release line -- left open,
nothing to bump to.

Verified via `go build ./...`, targeted go-git-consumer package tests,
and `pnpm run build`; NOTICE regenerated (version-string changes only,
no license changes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Andriy Knysh (aknysh) pushed a commit to zack-is-cool/atmos that referenced this pull request Aug 10, 2026
* fix(ai): close DX gaps found in atmos ai field test

Fixes 16 findings from a hands-on field test of `atmos ai` commands:

skill install/uninstall:
- Enforce the compatibility.atmos version gate for bundled and
  multi-skill Git package installs, not just single-skill Git clones
- Reject unrecognized --client values instead of silently no-op'ing
  (extends pkg/flags WithValidValues to string-slice flags generically)
- Warn when --path is combined with --client/--scope/--global, since
  --path skips auto-distribution and those flags are otherwise ignored
- Give the registry-corruption error an actionable hint via the
  error-builder pattern
- Show a skill's minimum required Atmos version in `skill list --detailed`
  and flag when an installed skill has a newer catalog version available
- Fix `agent-skills/skills/atmos-ai/SKILL.md` doc drift (never documented
  `skill install`) and remove a phantom `info` subcommand from --help
- Add local-path/file:// support to skill source parsing and the
  downloader
- Document --scope/--global precedence

ask/exec/sessions/MCP:
- Make `exec`/`ask --session` actually persist and resume conversations
  (previously a complete no-op despite being a documented flag)
- Resolve a session's Model from the constructed AI client instead of a
  raw config lookup, fixing sessions export/import for the default
  zero-config claude-code provider path
- Apply --mcp server filtering for CLI providers too (was silently
  ignored, all configured servers were always passed through)
- Reject invalid --format values instead of silently falling back to text

Two intentional behavior changes:
- `ai exec` can now return exit code 2 for a genuine infrastructure-level
  tool failure (e.g. an unregistered tool) without waiting on the
  25-iteration tool-call loop to exhaust
- `sessions clean --older-than 0d` now deletes all sessions immediately,
  distinguished from the flag not being passed at all (which still
  defaults to 30 days); negative durations are now a hard parse error
  instead of silently falling back to the default

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 7 Dependabot alerts in website deps

Bumps pnpm overrides for two transitive website dependencies to their
patched versions:
- js-yaml 3.15.0 -> 3.15.1, 4.3.0 -> 4.3.1 (GHSA-5p4m-2wfm-xmqj,
  quadratic CPU consumption in !!omap resolution, high severity,
  alerts cloudposse#268/cloudposse#269)
- mermaid 11.16.0 -> 11.16.1 (GHSA-rhh3-jpg6-66xh, GHSA-c4c3-pg64-4m4v,
  GHSA-6x64-9x62-f2gx, GHSA-3rrr-jr9j-h3q3, GHSA-2v8p-3f2j-5mp7,
  alerts cloudposse#263-cloudposse#267)

Both are within-major-version patches (not blocked by dependabot.yml's
major-bump ignore policy). No CodeQL alerts were open. Verified with a
full `pnpm run build` in website/ — succeeds, no new broken links.
NOTICE is unchanged (no license-set change from these bumps).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): fix flaky CI assertion in session export warning test

TestManager_ExportSession_WarnsOnUnimportableCheckpoint asserted on the
raw ui.Warning() output, including ANSI styling. Under CI=true (the
real GitHub Actions env, confirmed by reproducing locally with CI=true)
the formatter emits the message across two adjacent styled runs, which
split the literal substring "not be re-importable" the test was
checking for -- the visible text was identical, only the styling
boundary differed from a local run. Passed locally, failed in CI.

Strip ANSI codes and collapse whitespace before asserting on captured
UI output, so the test checks content, not rendering byte-layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(ai): add `atmos ai skill update` to refresh outdated bundled skills

Closes the gap from finding cloudposse#11 of the field-test fix pass: bundled skills
are static copies made at install time, so upgrading the atmos binary alone
never refreshed a skill you'd already installed, even when the new release
shipped improved skill content. `atmos ai skill list --detailed` already
surfaced an "update available" hint; nothing acted on it.

`atmos ai skill update [name]` 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:

- With no <name>, updates every installed bundled skill that has a newer
  version available (a single confirmation, not one per skill). Skills
  already current are left untouched and reported separately, so it's safe
  to run repeatedly.
- An outdated skill is reinstalled the same way `install <name> --force`
  would install it, reusing all the same client-distribution/scope flags
  and logic (--client, --all-clients, --scope, --global, --path).
- Skills installed from GitHub aren't covered yet (no cheap way to check
  their upstream version without a fetch); update returns a clear error
  pointing at `install <source> --force` as the manual path for those.

Both `atmos ai skill list`'s "update available" indicator and the new
`update` command now share one `marketplace.SkillVersionOutdated` helper
instead of duplicating the comparison, so they can never disagree.

Docs, blog post, and roadmap updated; the `atmos ai skill --help` screengrab
regenerated (--filter'd to just that one cast). Relabeling this PR
patch -> minor since this is new user-visible functionality, which requires
both per this repo's release-doc policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): add fix records for this session's four PR changes

Per the fix-log skill: one record each for the 16-finding atmos ai
field-test fix pass, the website Dependabot remediation, the flaky
CI test-assertion fix, and the new atmos ai skill update command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(flags): validate env-sourced flag values, not just CLI-set ones

ValidateFlagValues only checked cmd.Flags().Changed(), so an invalid
value supplied purely via a bound environment variable (e.g.
ATMOS_AI_SKILL_CLIENT=bogus) silently bypassed validation -- a skill
install/update would report success while distributing to zero
clients. Now validates whenever Viper reports the value as actually
set (CLI or env), not just CLI-changed.

Found via a field-test pass on the atmos ai skill update /
pkg/flags changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): surface batch failures in `atmos ai skill update`/install

UpdateAllBundled/InstallAllBundled's batch path logged a warning per
failed skill but always returned nil, so a real per-skill failure
(e.g. a reinstall that can't remove the existing install) was
reported as overall success with exit code 0 -- the only signal was
an easily-missed log line, invisible entirely at logs.level: Error.

Adds a distinct outcomeRejected for expected compatibility/structure
skips (already covered by an existing test), keeping it separate
from genuine outcomeFailed so only real failures propagate an error.

Found via a field-test pass on the atmos ai skill update changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): resolve anonymous chat session Model via client.GetModel()

The anonymous-session branch (no --session given) still called
getModelFromConfig -- a raw config lookup with no default fallback --
instead of client.GetModel(), unlike the named-session branch fixed
in a prior commit. A zero-config claude-code chat with no --session
got a blank Model on its session record, breaking re-import (which
requires a non-empty Model).

Found via a field-test pass on the atmos ai session DX fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): warn when --session is ignored because sessions are disabled

prepareSession returned (nil, nil) identically for "no --session
given" and "--session given but ai.sessions.enabled is false",
so a user could run `exec --session foo` across multiple invocations
believing conversation context was being carried over, with nothing
persisted and no indication why -- only discoverable indirectly via
`sessions list` erroring "sessions are not enabled".

Found via a field-test pass on the atmos ai session DX fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): fix concurrent session storage access failing with SQLITE_BUSY

Two processes opening the same new session database at once (e.g.
concurrent `atmos ai exec --session <same-name>` invocations) could
crash with SQLITE_BUSY: PRAGMA journal_mode = WAL briefly needs
exclusive access to switch modes, and busy_timeout was set after it
in the pragma list, so a connection that lost the race had no busy
timeout in effect yet. Reorders busy_timeout first and adds a bounded
retry loop around pragma execution, since the pure-Go sqlite driver
(modernc.org/sqlite) doesn't consistently honor busy_timeout for that
specific one-time mode switch.

Found via a field-test pass on the atmos ai session DX fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 5 Dependabot alerts (go-git, dompurify, nanoid)

- github.com/go-git/go-git/v5: 5.19.1 -> 5.19.2, fixing a symlink
  traversal in worktree operations (GHSA-hc8v-wwc9-vgxm, high) and a
  path traversal in reference name handling (GHSA-qgq7-7hm3-q39j,
  medium). Alerts cloudposse#270, cloudposse#271.
- dompurify (website, transitive via pnpm override): 3.4.12 -> 3.4.13,
  fixing an IN_PLACE sanitization XSS via detached subtree hook
  removal (GHSA-55q2-fjhq-7xh7, medium). Alert cloudposse#272.
- nanoid (website, transitive via pnpm override, both the direct
  override target and postcss's own dependency): 3.3.15/3.3.16 ->
  3.3.18, fixing two infinite-loop DoS issues with negative/zero size
  (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8, high). Alerts cloudposse#273, cloudposse#274.

Not fixed: cloudposse#275/cloudposse#276 (npm image-size <= 2.0.2, high) -- no patched
version exists yet upstream (first_patched_version is null on both
advisories); nothing to bump to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): fix flaky merge-queue failure in describe-affected base test

TestSetDescribeAffectedFlagValueInCliArgs_BaseResolution's "CI
auto-detect when enabled and no explicit base" subtest cleared
GITHUB_ACTIONS/CI but not GITHUB_EVENT_PATH. When this test suite
runs locally or under a regular pull_request CI trigger, no merge_group
event file is present, so resolveMergeGroupBase falls back to the
simulated GITHUB_BASE_REF the subtest sets, and the test passes.

But when this test actually runs inside a real merge_group-triggered
workflow (i.e. a PR going through the GitHub merge queue), the runner's
real GITHUB_EVENT_PATH points at a genuine merge_group event payload
with a real base_sha -- and resolveMergeGroupBase correctly prefers
that real payload data over GITHUB_BASE_REF, per its documented
fallback order. The subtest never accounted for this, so it fails with
an empty Ref field specifically -- and only -- in real merge-queue
runs, not locally or in regular PR CI. Reproduced locally by pointing
GITHUB_EVENT_PATH at a synthetic merge_group payload; fixed by
explicitly clearing GITHUB_EVENT_PATH so the subtest is hermetic
regardless of the ambient CI context it happens to execute in.

This was pre-existing on main (from PR cloudposse#2395) and surfaced when PR
cloudposse#2903 hit the merge queue for the first time; not a regression from
this branch's own changes, but fixed here directly per repo convention
for CI-blocking issues found on an open PR's branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
zack-is-cool pushed a commit to zack-is-cool/atmos that referenced this pull request Aug 10, 2026
…udposse#2880)

* fix(steps): resolve relative paths against step.WorkingDirectory

The archive, file, workdir, junit, and container build step handlers
resolved relative source/destination/path/glob/context fields via
template substitution only, then let filesystem calls resolve them
against the Atmos process's own cwd instead of step.WorkingDirectory.
This surfaced most visibly for `type: archive` hooks, since the hooks
engine correctly defaults working_directory to the component path but
the handler never read it back.

Add a shared BaseHandler.ResolveInWorkingDirectory helper that anchors
a relative resolved value to step.WorkingDirectory (falling back to
process cwd when unset, matching prior behavior), and apply it across
the five affected handlers. container_build.go additionally anchors
Dockerfile to the resolved Context rather than WorkingDirectory
directly, matching Docker's own convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): add fix record for step WorkingDirectory bug

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(runner/step): close coverage gaps in ResolveInWorkingDirectory and loadReport

Codecov flagged handler_base.go and junit.go below the 85% patch-coverage
threshold. Add a case that exercises the relative (non-template)
WorkingDirectory branch in resolveWorkingDirectory, and a junit test that
triggers a WorkingDirectory template-resolution error from inside
loadReport's per-pattern loop, distinct from the already-covered `files`
template error path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(steps): anchor container bake/cache paths and preserve workflow error context

A field test of the WorkingDirectory fix found the container_build.go handler
was only partially fixed: build.bake.file/bake.files and cache.from/cache.to
type: local src/dest still resolved against the Atmos process's own cwd
instead of step.WorkingDirectory, reproduced live as a silent build against
the wrong bake file. Both now route through ResolveInWorkingDirectory like
context/dockerfile already do.

Also root-caused and fixed a separate defect surfaced while verifying error
output: buildWorkflowStepError dual-wrapped step errors with fmt.Errorf before
building the final error, and cockroachdb/errors treats that Go 1.20
multi-error shape as an opaque leaf node, silently dropping any hints/context
a handler attached deeper in the chain. Switched to WithCause, which extracts
them eagerly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(steps): anchor container bake/cache paths and preserve workflow error context

Address CodeRabbit findings on PR cloudposse#2880:
- Remove the host-specific /tmp/atmos-field-test/ path from the fix doc,
  replacing it with a worktree-relative fixture reference.
- Split TestBuildWorkflowStepError and TestBuildWorkflowStepErrorPreservesInnerHintsAndContext
  out of workflow_utils_test.go into a focused workflow_step_error_test.go,
  keeping step-error tests co-located and out of the oversized (pre-existing)
  workflow_utils_test.go file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(runner/step): close container_build.go coverage gaps from bake/cache anchoring

Codecov flagged container_build.go's patch coverage below 85% after the
bake/cache anchoring follow-up. Add regression tests for the three
previously-uncovered error branches: resolveBakeFiles propagating a
per-entry template failure, anchorCacheLocalPaths' own resolve failure, and
that failure propagating out through resolveBuildCache. handler_base.go's
remaining gap (os.Getwd/filepath.Abs failing inside resolveWorkingDirectory)
is left uncovered deliberately -- it requires corrupting the process's own
working directory to reach, has no DI seam, and matches this repo's existing
untested pattern for the identical os.Getwd/filepath.Abs fallback in
container_run.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(docs): exclude reproducible-builds.org from link checking (CI connection refused)

CI's Check Markdown Links job failed on docs/prd/archive-step.md's citation
of the SOURCE_DATE_EPOCH origin -- the CI runner's outbound request was
refused while the page returns 200 OK outside CI. Follows the repo's
existing precedent for excluding CI-hostile hosts (docs.docker.com,
otelic.com, taskfile.dev, etc.) in lychee.toml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(hooks): anchor bare-relative working_directory to the component

An explicit working_directory: on a kind: step/kind: steps hook was always
resolved against the Atmos process's own cwd, regardless of shape. Following
docs/prd/base-path-resolution-semantics.md's existing Dot/Bare convention: a
dot-prefixed value (., .., ./x, ../x) keeps resolving against the process
cwd; a bare relative value (x, x/y) now resolves against the component's
own working directory instead -- the same directory ComponentPath(ctx)
already computes for the unset-default case, so this stays compatible with
provisioned working directories and metadata.component aliasing for free.
Workflows and custom commands are unaffected -- they have no "current
component" concept, so bare-relative values there still resolve against the
process cwd exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 7 open Dependabot alerts

Bump pnpm.overrides floors for transitive npm dependencies flagged by
Dependabot (all patch/minor bumps within the same major, not blocked by
dependabot.yml's major-version ignore policy):

- js-yaml 3.15.0 -> 3.15.1 (cloudposse#269, GHSA-5p4m-2wfm-xmqj: quadratic CPU
  consumption in !!omap resolution, high)
- js-yaml 4.3.0 -> 4.3.1 (cloudposse#268, same advisory, 4.x line, high)
- mermaid 11.16.0 -> 11.16.1 (cloudposse#267, cloudposse#266, cloudposse#265, cloudposse#264, cloudposse#263: five advisories
  ranging low-medium)

NOTICE unchanged (no license changes). Verified: pnpm install regenerated
website/pnpm-lock.yaml with the bumped versions resolved, atmos fix lint
(patch-scoped, no Go files touched) is a no-op, and cd website && npm run
build succeeds with no new broken links.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): fix markdownlint MD018 in working-directory fix record

Address CodeRabbit finding on PR cloudposse#2880: a paragraph wrap left "cloudposse#2880):" at
the start of a line, which markdownlint's atx-heading rule (MD018) flags as
a heading missing a space after the hash. Rewrap so the line doesn't start
with a bare hash-prefixed token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(runner/step): close remaining working_directory anchoring gaps

Found by field-testing the working_directory fix against real hooks,
workflows, and a live docker build instead of just unit tests:

- workdir step: `source` (local relative path) now anchors to
  `working_directory` via a new `sourceprov.WithBaseDir` option,
  matching `path`'s existing anchoring instead of silently falling
  back to the process cwd.
- workflow-level `working_directory:` default now reaches extended
  step types (archive/file/junit/workdir/container), not just
  shell/exec/atmos steps, which silently ignored it before.
- step-level `working_directory` now expands a leading `~`, matching
  the tilde expansion --chdir already gets.
- Corrected agent-skills/atmos-steps and the workflow/hooks docs,
  which claimed a single blanket CWD-vs-base_path rule that doesn't
  match actual per-surface behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(steps): document handler-field and unset-hook working_directory contracts

Addresses CodeRabbit feedback on PR cloudposse#2880: state that relative handler
fields (source, destination, path, files, context) resolve against the
already-resolved working_directory, that a container Dockerfile
resolves relative to context rather than working_directory directly,
and that an unset kind: step hook working_directory (not just a bare
value) anchors to the component's own working directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(runner/step): isolate CI base-resolution test from ambient GITHUB_EVENT_PATH

TestSetDescribeAffectedFlagValueInCliArgs_BaseResolution's merge_group
subtest only overrode GITHUB_ACTIONS/GITHUB_EVENT_NAME/GITHUB_BASE_REF,
so it depended on $GITHUB_EVENT_PATH being unset in the ambient
environment. That's true for local/regular CI runs, but not when the
test itself runs inside a real GitHub Actions merge_group job (i.e.
the merge queue): the real event payload's merge_group.base_sha then
takes precedence over the simulated GITHUB_BASE_REF, resolving to a
SHA instead of the "refs/remotes/origin/main" ref the test asserts.

This was failing the merge queue for this PR and, independently, for
cloudposse#2900 and cloudposse#2903 as well -- confirmed pre-existing and unrelated to any
of their changes. Fixed by explicitly forcing GITHUB_EVENT_PATH="" so
the subtest deterministically exercises the fallback path it's meant
to test, regardless of the real job's event context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 5 of 7 open Dependabot alerts

- github.com/go-git/go-git/v5: v5.19.1 -> v5.19.2, fixing GHSA-hc8v-wwc9-vgxm
  (symlink traversal in worktree operations) and the companion malicious
  reference-name advisory (cloudposse#270, cloudposse#271). Patch-level bump, no API changes.
- website: dompurify pnpm override ^3.4.12 -> ^3.4.13, fixing an IN_PLACE
  hook removal XSS (cloudposse#272).
- website: nanoid pnpm overrides bumped to ^3.3.17 (added a second
  selector, nanoid@^3.3.16, to also catch postcss's own nanoid range,
  which the existing nanoid@3.3.3 selector didn't match), fixing two
  indefinite-loop DoS advisories (cloudposse#273, cloudposse#274). The nanoid 5.x line was
  already patched by the existing override.
- NOTICE regenerated for the go-git/dompurify/nanoid version bumps.

Not fixed: image-size (cloudposse#275, cloudposse#276, both DoS via infinite loop) has no
patched release yet as of this commit -- GitHub's advisories list
"<= 2.0.2" as vulnerable with no first_patched_version. Left as-is
pending an upstream fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
Andriy Knysh (aknysh) added a commit to zack-is-cool/atmos that referenced this pull request Aug 11, 2026
…lution (cloudposse#2900)

* fix(version): exclude draft GitHub releases from Version Tracker resolution

github-releases datasource resolution could pick an unpublished draft
release instead of the latest published one when the GitHub token had
repo write access (e.g. secrets.GITHUB_TOKEN in CI). GetReleases now
filters out drafts unconditionally, alongside the existing prerelease
filter, fixing the Version Tracker resolver, `atmos version list`, and
GetReleaseVersions at the shared root cause.

* fix(version): remove the deprecated `atmos version track render` command

render.go and apply.go were added in the same commit that introduced
the Version Tracker (cloudposse#2664); render was marked Deprecated/Hidden from
day one and has never had a non-deprecated existence in any release,
so it never needs a migration path. Relocates the shared renderTemplate
helper into apply.go (its only remaining consumer) and removes the
now-dead manager.RenderFile helper and render-specific sentinel errors.

Also fixes pre-existing EditorConfig indentation drift in
docs/prd/atmos-version-management.md (3-space list/fence indents under
numbered items instead of the required 2-space multiple), surfaced by
this branch's `--affected` validation once the file was touched.

* docs(version): expand version.files and !version function examples

Adds worked before/after examples for the marker and github-actions
file managers (including SHA pinning) to the version.files reference,
and adds Helm/Container-component tabs to the !version function docs
alongside the existing Terraform example, so each supported component
type has a concrete resolution example rather than relying solely on
the Terraform case.

* test(github): assert filterDrafts output tags and order, not just length

Length-only assertions let filterDrafts silently drop or reorder the
wrong releases; assert each result's tag against the expected order.

* docs(version): refine file-manager and !version examples

- Replace before/after tabs in files.mdx with one tab per distinct
  scenario (single tool, multiple tools, YAML, custom match pattern,
  version bump, pinned to SHA), and show both trailing and standalone
  marker comment forms for the match= example.
- Use current tool versions in examples instead of stale ones.
- Rename the "Non-Dockerfile Format" tab to "YAML" to name what the
  example is instead of what it isn't.
- Clarify in version.mdx that !version only resolves inside stack
  manifests, not in atmos.yaml or arbitrary YAML files.

* docs(version): address CodeRabbit review findings on PR cloudposse#2900

- Qualify the GITHUB_TOKEN permission claim in the draft-exclusion
  fix log: repo CI only sees drafts when its token permissions grant
  read access to contents, not unconditionally.
- Fix the pinned-SHA example in files.mdx: the SHA labeled v6.1.0 was
  actually actions/checkout's v5.0.0 release commit. Swapped in the
  correct SHA for the v6.1.0 tag.

* fix(security): remediate 7 Dependabot alerts in website deps

Bump pnpm overrides to patched versions, all within their existing
major line (not blocked by dependabot.yml's semver-major ignore):

- js-yaml 3.15.0 -> 3.15.1 (GHSA-5p4m-2wfm-xmqj, high)
- js-yaml 4.3.0 -> 4.3.1 (high)
- mermaid 11.16.0 -> 11.16.1 (4 advisories: 1 low, 3 moderate)

* fix(ci): isolate merge_group base-resolution test from ambient GITHUB_EVENT_PATH

TestSetDescribeAffectedFlagValueInCliArgs_BaseResolution's merge_group
subtest only stubbed GITHUB_EVENT_NAME/GITHUB_BASE_REF, leaving the
real runner's GITHUB_EVENT_PATH in place. Under an actual
merge_group-triggered job (i.e. the merge queue) that path points to a
real payload carrying merge_group.base_sha, so resolution correctly
takes the SHA branch and leaves describe.Ref empty -- failing the
test's Ref assertion even though production behavior is correct. This
is why the PR passed as a normal PR check but failed once queued.

Clear GITHUB_EVENT_PATH explicitly, matching the pattern already used
by sibling tests in this file and in pkg/ci/providers/github and
pkg/validation for the same reason.

* fix(security): remediate 5 CodeQL/Dependabot alerts

Bump to patched versions, all within their existing major line (not
blocked by dependabot.yml's semver-major ignore):

- github.com/go-git/go-git/v5 v5.19.1 -> v5.19.2 (cloudposse#270 high, cloudposse#271
  moderate), pulled transitively via `go get` + `go mod tidy`;
  regenerated NOTICE
- dompurify 3.4.12 -> 3.4.13 (cloudposse#272 moderate) via pnpm override
- nanoid 3.3.15 -> 3.3.18 (cloudposse#273, cloudposse#274 both high) via pnpm override;
  added a second override key matching postcss's own `^3.3.16`
  dependency range, which the existing `nanoid@3.3.3` key didn't catch

image-size (cloudposse#275, cloudposse#276, both high) has no patched version published
upstream yet -- not fixable.

* fix(ci): widen Windows Acceptance tests timeout, root-caused via runner log

Windows acceptance tests timed out at exactly the 60m step budget
(started 19:04:21, `go test` itself finished 20:03:56 with every
package printing `ok`, endgroup at 20:04:21, force-cancelled 17s
later at 20:04:38). No test failure -- just CI-to-CI variance on a
budget with no headroom, the same flake pattern already documented
here from an earlier 45m->60m bump.

Give windows-latest its own 80m step budget (matrix-conditional
expression; macOS stays at 60m, where it comfortably finishes in
~35m today) and raise the job-level ceiling from 120m to 140m to
preserve the same headroom-above-step-budget-sum margin the existing
comment establishes for Linux and macOS.

---------

Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
Michael Pursifull (arcaven) pushed a commit to arcaven/atmos that referenced this pull request Aug 19, 2026
…cloudposse#2879)

* test(container): cover combined buildx driver/cache/tags/context args

Strengthens pkg/container's pure arg-building test with a case combining
engine, driver, cache, custom dockerfile/context, and tags in a single
config, closing the one remaining gap versus per-field-only coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(git): tolerate config errors for CI git-clone bootstrap pre-Cobra

atmos git clone in a fresh CI workspace (no atmos.yaml yet, e.g. a profile
referenced by CI config) failed with "profile not found" before ever
attempting the clone, and ATMOS_CI=true had no effect. Execute() runs an
initial cfg.InitCliConfig before Cobra resolves any command; only the
second, PersistentPreRun-scoped InitCliConfig call knew how to tolerate
the CI bootstrap clone's expected missing config (applyCIGitCloneBootstrap),
so the first call's error aborted the process before that check could run.

Add isCIGitCloneBootstrapArgs, an os.Args-based equivalent of the existing
cmd-aware bootstrap check, so the pre-Cobra handler recognizes the same
no-argument `atmos git clone` shape and defers to the same
ATMOS_CI/CI-provider resolution (via the new exported
CIGitCloneModeRequestedFromEnv) before Cobra ever parses the command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): record CI git-clone bootstrap profile fix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(git): parse CI bootstrap flags with real pflag arity, not a heuristic

The pre-Cobra CI git-clone bootstrap check (added in the prior commit)
disqualified the bootstrap on any bare, non-"-"-prefixed token, including a
space-separated flag value like the "0" in `--depth 0`. That misread a
value-taking flag's argument as a positional repo name/URI, so the exact
reported reproduction (`atmos git clone --ci --depth 0` in a fresh CI
workspace) still failed on "profile not found".

Replace the heuristic with CIGitCloneBootstrapRequestedFromRawArgs, which
parses the clone-specific args against a throwaway command carrying the
real clone flag set (a fresh newCloneParser() instance, never the shared
singleton) via actual pflag parsing, then defers to the existing
CICloneBootstrapRequested. This also lets an explicit --ci/--ci=false in
the raw args be honored before Cobra resolves the command, which the
removed env-only CIGitCloneModeRequestedFromEnv could not do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): update CI git-clone bootstrap fix record for pflag rewrite

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(schema): decode with: into Build/Run/Push/Inspect for custom commands

Fixes cloudposse#2876. A custom command's `type: container` step with
a `with:` block (engine, driver, cache, tags, etc.) silently dropped
everything, falling back to a bare `docker build -f Dockerfile .`, when
loaded from a commands.yaml merged into atmos.yaml's Viper config tree.

Root cause: `with:` is polymorphic -- decoded into Build/Run/Push/Inspect
for `type: container` steps, or the generic With map otherwise -- but that
promotion lives entirely in Task.UnmarshalYAML/WorkflowStep.UnmarshalYAML
(go-yaml's yaml.Unmarshaler interface), invoked only when something calls
yaml.Node.Decode directly (e.g. standalone workflows/*.yaml files via
pkg/utils.UnmarshalYAMLFromFile). Custom commands merged into atmos.yaml
decode via Viper's mapstructure pipeline (TasksDecodeHook ->
decodeTaskFromMap), which never invokes yaml.Unmarshaler and had no
equivalent promotion, so `with:` only ever reached the raw generic map.

decodeTaskFromMap now pulls `with:` out before the mapstructure decode and
replays the same polymorphic decode via decodeStepWith, round-tripping the
value through YAML so both code paths share one implementation and can't
drift apart.

Reproduced through the real production paths per the bug report's request:
config loaded via InitCliConfig (pkg/config), and the full custom command
executed via RootCmd through a fake logging docker executable (cmd/) --
not by manually constructing schema.Task/WorkflowStep/ContainerBuildStep
literals, which would have bypassed the actual decode bug. Added a
complementary test proving workflow-file and custom-command steps decode
with: identically, per the report's public-contract requirement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir): sanitize nested component names in BuildPath

A component name containing "/" (e.g. a nested layout like ecs/cluster)
made workdir.BuildPath produce a real extra subdirectory instead of a
single path segment, since the name was interpolated into "<stack>-<name>"
without escaping and then filepath.Join'd. That put the nested component's
workdir one level deeper than a flat component's at the same stack.

Any path computed relative to the workdir -- most visibly a relative
`backend.local.path` template like `../../../.context/tfstate/...` --
therefore climbed to a different real ancestor for the nested component
than for the flat one, silently writing state under a different root
(<repo>/.workdir/.context/... instead of <repo>/.context/...) even though
both components used the identical backend config.

Sanitize the component name the same way internal/exec/terraform_generate_
backends.go already does for backend template context: replace "/" with
"-" before building the workdir directory name. BuildPath is the single
formula reused by the source provisioner and by internal/terraform_backend's
JIT-workdir state lookup, so both pick up the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): record custom-command with: and workdir path-depth fixes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(terraform/output): retarget containment-guard test at stack traversal; surface cached output lookups

BuildPath now sanitizes "/" out of component names, so the containment
guard test's traversal-via-component vector no longer escapes BasePath.
Retarget it at the stack argument, which isn't sanitized the same way and
still needs the guard. Also make cache-hit output lookups emit the same
visible "Fetching ..." notification a real fetch would, instead of only a
Debug-level log, so a second output lookup on an already-cached component
isn't silently invisible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(terraform/output): strip ANSI before asserting cache-hit visibility; correct fix-log formatter name

CI forces color output (CI=true), which makes the markdown-based UI
renderer split "Fetching vpc_id ..." into multiple ANSI-styled runs right
at the literal underscore, without dropping or reordering any visible
characters. Strip ANSI before the assert.Contains checks, matching the
ansi.Strip convention already used elsewhere in the test suite.

Also correct the fix-log's "gofmt" validation bullet to "gofumpt", the
formatter this repo actually mandates and runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(claude): deny gofmt in Claude Code permissions

Repo mandates gofumpt, not gofmt (CLAUDE.md, .golangci.yml). Denying the
raw command prevents Claude Code from running gofmt directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: close Codecov patch-coverage gaps on PR cloudposse#2879

Adds behavior-focused tests for the 5 lines Codecov flagged as uncovered
on this branch's added code: isCIGitCloneBootstrapArgs's len(args) < 1
guard, decodeTaskFromMap/decodeStepWithFromMapValue's three error-wrap
branches (invalid container action, yaml.Marshal failure via a
yaml.Marshaler that errors, yaml.Unmarshal failure via a dangling YAML
alias), and resolveOutputFromCache's cache-miss and getOutputVariable-
error branches. No production code changes; no assertions weakened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): record terraform/output CI fixes; correct gofmt->gofumpt typo

Two fixes from this branch (cache-hit output lookups now visible;
containment-guard test retargeted at the still-open stack-traversal
vector after the workdir fix closed the component-name one) had no
docs/fixes/ record. Also corrects a stale "gofmt" mention in the
git-clone-ci-bootstrap doc to "gofumpt", matching the correction already
applied to the container with-block doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(registry): widen timing margin in provider-mirror concurrency test

Fixes a flaky Windows Acceptance Tests failure: resolving 10 platforms
took 784ms against a 750ms threshold, even though that's nowhere near the
1.5s serial floor the test guards against. Raises the bound to 4/5 of the
serial floor (1200ms) for headroom against normal CI timing variance,
Windows runners especially. No production code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir): sanitize nested component names in createWorkdirDirectory

createWorkdirDirectory duplicated the unsanitized stack-componentName
formula that BuildPath was already fixed to sanitize, so a local
(non-source) component with provision.workdir.enabled: true and a
nested name still got a workdir one level deeper than a flat sibling,
silently shifting where relative backend.local.path state resolves.
Now delegates to BuildPath so both formulas can't drift apart again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(provisioner): guard path traversal in source-vendoring fallback

DetermineTargetDirectory's non-workdir fallback (the default vendoring
path when provision.workdir.enabled is unset) joined the component
base path with the raw component name with no containment check, so a
component named with ../ segments could vendor outside
components/terraform/. Adds the same absolutize-and-prefix containment
guard already used by the two other BuildPath-derived callers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cli): decode and execute custom-command step-level container: overrides

Sibling gap to the with: block fix (docs/fixes/2026-08-05-custom-
command-container-with-block-dropped.md): a custom command step's
container: override went through three independent failures. The
bare boolean opt-out (container: false) broke InitCliConfig for the
whole atmos.yaml because decodeTaskFromMap never round-tripped
container: through YAML the way with: now does. The mapping form
decoded fine but was never consulted at execution time -- the
custom-command step loop always ran type: shell steps on the host.
And once both of those were fixed, container: false still ran the
step inside a container because cloneCommand's JSON round-trip
silently dropped WorkflowContainer.Enabled (json:"-"), inverting the
opt-out.

Fixes all three: decodeTaskContainerFromMapValue mirrors the with:
fix's round-trip for container:, cmd/cmd_utils.go's step loop now
reuses the same pkg/workflow/container.go session logic the
workflow-file path already uses, and WorkflowContainer gained
MarshalJSON/UnmarshalJSON so Enabled survives a JSON round-trip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(container): pass restart/healthcheck through to ephemeral run steps

ContainerRunStep.Restart/.HealthCheck decoded fine but EphemeralConfig
(the runtime config for type: container, action: run steps) had no
such fields, and buildRunConfig never populated them -- unlike the
persistent-component path, which already wires the same settings.
Adds the fields to EphemeralConfig and populates them via the
existing (previously unused for this path) RestartPolicyFromStep/
HealthCheckFromStep helpers, so --restart/--health-* flags now reach
the real docker/podman invocation for step-based container runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(config): surface swallowed import errors and hide-nothing validation

Two DX gaps where already-computed diagnostic detail never reached
the default log level: a YAML syntax error in an import:-loaded
commands file was silently swallowed (Debug-only), leaving only a
generic "Unknown command" with no hint a config file failed to parse;
and container-step validation (missing required field, invalid pull:
value) already computed the field/step/type and the bad value but
only exposed them via --verbose or dropped them entirely.

LocalAdapter now pairs its existing log.Debug with a ui.Warning
naming the file and parse error. ValidateRequired's default message
now names the field; invalidContainerField now echoes the actual
invalid value typed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 7 Dependabot alerts in website dependencies

Bumps three pnpm.overrides pins to their patched releases, all within
the major-version line dependabot.yml's ignore policy allows:

- js-yaml 3.15.0 -> 3.15.1 (GHSA-5p4m-2wfm-xmqj, alert cloudposse#269)
- js-yaml 4.3.0 -> 4.3.1 (GHSA-5p4m-2wfm-xmqj, alert cloudposse#268)
- mermaid 11.16.0 -> 11.16.1 (GHSA-rhh3-jpg6-66xh cloudposse#267,
  GHSA-c4c3-pg64-4m4v cloudposse#266, GHSA-6x64-9x62-f2gx cloudposse#265,
  GHSA-3rrr-jr9j-h3q3 cloudposse#264, GHSA-2v8p-3f2j-5mp7 cloudposse#263)

Verified via `pnpm run build` in website/; NOTICE unchanged (no
license changes from these patch bumps).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(schema): reject unknown fields in container step with:/driver: blocks

The JSON Schema for a container step's with: block already documented
it as "validated by the step handler at run time," but nothing
fulfilled that promise for unknown keys -- yaml.Node.Decode (used by
both the workflow-file and custom-command loading paths) has no
strict/KnownFields mode, so a typo'd field like `platforms:` was
silently dropped with no error. This masked a real pre-existing test
bug: TestWorkflowStep_DecodeWith's push-action case used `tag: v1`
instead of the actual `tags: []string` field and passed anyway.

decodeYAMLInto and ContainerDriverConfig.UnmarshalYAML now decode
through a stream-level yaml.Decoder with KnownFields(true) (the only
place go-yaml exposes strict decoding) instead of plain node.Decode,
scoped narrowly to the typed container structs -- the generic with:
map[string]any fallback other step types use is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 5 Dependabot alerts, 2 unpatched and deferred

- github.com/go-git/go-git/v5 5.19.1 -> 5.19.2 (GHSA-hc8v-wwc9-vgxm
  cloudposse#270, GHSA-qgq7-7hm3-q39j cloudposse#271), pulling in patch bumps to
  golang.org/x/{mod,net,text,tools} via go mod tidy
- nanoid pnpm override widened from a pinned 3.3.3->^3.3.15 mapping to
  a ^3->^3.3.17 range so every 3.x requester resolves past both
  vulnerable versions (GHSA-28wg-ghj8-5hjv cloudposse#274, GHSA-2v37-7h3g-55p8
  cloudposse#273); previously two different 3.x versions were resolving
  simultaneously because the override only matched exact-version
  requests
- dompurify pnpm override 3.4.12 -> 3.4.13 (GHSA-55q2-fjhq-7xh7 cloudposse#272)

image-size alerts cloudposse#275/cloudposse#276 (GHSA-5p2g-fcmc-qvqq, GHSA-w3rx-r6r6-pgpr)
have no first_patched_version yet in any release line -- left open,
nothing to bump to.

Verified via `go build ./...`, targeted go-git-consumer package tests,
and `pnpm run build`; NOTICE regenerated (version-string changes only,
no license changes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd): update stale path assertions in container build argv test

TestCustomCommandContainerBuildPassesWithBlockToDocker asserted the
buildx argv contained bare "Dockerfile" and "app" strings. Since
main's cloudposse#2880 (resolve relative paths against step.WorkingDirectory),
context: and dockerfile: values in a with: block are correctly
resolved to absolute paths before reaching docker, so the argv now
contains the full resolved paths instead of the bare relative
strings. Docker still receives the same file -- update the
assertions to check for the resolved absolute paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(provisioner): pass OutputWriters in nested-workdir Provision test

Merging origin/main's fix(terraform): prevent concurrent output
corruption (cloudposse#2898) added a fourth provisioner.OutputWriters parameter
to (*Service).Provision. Every call site main's own history knew
about was updated by that commit, but this branch's own
TestServiceProvision_NestedComponentName_SanitizesLikeBuildPath
(added by an earlier, unrelated fix on this branch) didn't exist in
main's history, so git's auto-merge had nothing to reconcile it
against and left the stale three-argument call in place -- a compile
failure only visible once this branch actually merges main, which is
exactly what GitHub Actions' implicit PR-merge checkout does on every
CI run regardless of whether this branch has locally merged yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(cmd): assert cache/driver flag values, not just presence

TestCustomCommandContainerBuildPassesWithBlockToDocker only checked
that --cache-from/--cache-to/--builder appeared somewhere in the
argv, not that they carried the configured registry ref, mode=max, or
driver. The driver: block also provisions a real Buildx builder via a
separate `docker buildx create` invocation (pkg/container/docker.go's
ensureBuilder) that the fake runtime already recorded but the test
never inspected. Adds table-driven flag-value assertions covering
both the create and build invocations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd): normalize marker path for cross-platform shell command

markerPath comes from t.TempDir(), which contains backslashes on
Windows. The step command string goes through Atmos's mvdan/sh
interpreter, which treats \ as an escape character, so the
redirection target would resolve to a mangled path. Convert to
slashes and quote the path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(provisioner): sanitize backslash in workdir component names

BuildPath sanitized "/" in a component name to prevent it from
adding a real directory level, but not "\", which is Windows' actual
path separator. A crafted or copy-pasted component name containing
"\" (e.g. "..\\..\\evil") would let filepath.Join/Clean treat it as
real ".."-traversal segments on that platform, escaping the intended
workdir root. Sanitize "\" identically and unconditionally on every
platform, matching the existing "/" handling, so a given component
name's workdir path also stays identical across OSes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(runner): make step-name assertion independent, cover empty type

The default-message test used step name "run" and field "run.image",
so the step-name assertion could pass even if ValidateRequired
dropped the step name entirely, since "run.image" also contains
"run". Converted to a table with a distinct step name/field per case
and no shared substrings, and added a step.Type == "" case to cover
the previously-untested branch that omits the "(type ...)" clause.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(schema): reject unknown fields in container: override blocks

WorkflowContainer.UnmarshalYAML's mapping branch used plain
value.Decode, so a typo'd field (e.g. `imgae` instead of `image`) in
a workflow-level or step-level container: block was silently
discarded rather than rejected -- the same class of gap already fixed
for with: blocks in decodeYAMLInto. Use the existing
decodeYAMLKnownFields helper instead, and add regression coverage for
both the workflow-file (yaml.Unmarshal) and custom-command
(mapstructure + TasksDecodeHook) decode paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixtures): sync workdir-nested fixture with path-safety fixes

The README still documented the pre-fix behavior for the
app/local-nested "known bug" scenario and the ../escape-test-nowd
path-traversal probe, which are both now fixed on this branch.
Re-verified both scenarios for real (atmos terraform apply/source
pull against the fixture) and updated the manual-testing steps and
expected output to match: the nested local component now sanitizes
to a sibling workdir, and the unguarded probe now fails with
ErrPathTraversal instead of vendoring outside components/terraform/.
Updated the .gitignore comment on the now-defensive-only
escape-test-nowd entry accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): correct formatter name, markdown syntax, typos, spelling

- gofmt -> gofumpt (the repository-required formatter)
- double-backtick delimiter for a code span containing a literal
  backtick, which single backticks can't escape in Markdown
- add `text` language identifiers to unlabeled fenced output blocks
- "on a already-parsed" -> "on an already-parsed"
- "macos" -> "macOS" in a CI platform list

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): correct macOS spelling in CI platform list

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(provisioner): use filepath.Rel for component base path containment

The naive absBase+separator prefix check breaks when componentBasePath
resolves to a filesystem root ("/" on Unix, "C:\" on Windows): absBase
already ends in the separator there, so the literal absBase+sep prefix
("//" or "C:\\") never matches any real descendant, rejecting every
valid target with ErrPathTraversal. filepath.Rel doesn't have this
edge case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(schema): wrap WorkflowContainer JSON decode error

UnmarshalJSON returned the raw json.Unmarshal error directly instead
of wrapping it with a static error from errors/errors.go, so callers
couldn't classify a WorkflowContainer JSON decode failure the way
they already can for its YAML counterpart. Wrap with the existing
ErrInvalidWorkflowContainer sentinel, matching UnmarshalYAML.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixtures): sync stack-manifest comments with path-safety fixes

The comments on the three path-traversal probes in this fixture's
dev.yaml still described pre-fix behavior (determineSourceTargetDirectory
having no containment guard, DetermineTargetDirectory's non-workdir
fallback having no sanitization, createWorkdirDirectory reimplementing
an unsanitized formula) even though all three are now fixed on this
branch. Updated to describe the current, correct expected behavior,
matching the README sync in the prior commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(provisioner,cmd): close symlink containment gap and route script steps through container overrides

- pkg/provisioner/source: validateWithinComponentBasePath now resolves
  symlinks in the existing portion of target/base paths before checking
  containment, closing a bypass where a symlink under componentBasePath
  pointing outside it passed the old lexical-only check.
- cmd/cmd_utils: type: script custom-command steps now route through
  StepContainerOverride/RunStepContainerOverride like type: shell steps
  already do, instead of silently ignoring a step-level container: override.
- tests/fixtures: drop the POSIX-only /dev/stderr log destination from the
  source-provisioner-workdir-nested fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd): propagate Cobra cancellation to custom-command step execution

executor.Execute and both RunStepContainerOverride call sites in
executeCustomCommand used context.Background() instead of cmd.Context(),
so a Ctrl-C on the top-level invocation couldn't cancel an extended step
handler or a container runtime operation. Derive one executionCtx from
cmd.Context() (falling back to context.Background() for direct-test
invocations), mirroring the existing depCtx pattern used for dependency
graph execution in the same function.

Addresses CodeRabbit review comment on PR cloudposse#2879.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): add missing comma after "e.g." in fix-log doc

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd,schema): address CodeRabbit findings on PR cloudposse#2879

Completes Cobra-cancellation propagation to the remaining
context.Background() call sites in executeCustomCommand, wraps
with:/container: decode failures with their static sentinel errors so
errors.Is works regardless of which decode step fails, and extends
cmd.NewTestKit(t) to restore RootCmd.Commands() between tests so custom
commands registered by one test no longer leak into the next. Also fixes
comment/doc-accuracy nits (godot periods, exported test-double doc
comments, and two docs/fixes/*.md wording corrections).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(workdir): document BuildPath's separator sanitization in its doc comment

CodeRabbit nitpick on PR cloudposse#2879: the doc comment didn't mention that both
"/" and "\" are replaced with "-", only the inline comment did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(cmd): add regression test for RootCmd.Commands() restoration

Addresses CodeRabbit findings on PR cloudposse#2879: a dedicated table-driven test
was missing for restoreRootCmdCommands (confirmed failing pre-fix,
passing post-fix), plus a comment period and an inconsistent
path-traversal probe description in a test fixture README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir,schema): close BuildPath collision/traversal gaps, stop mutating caller task maps

Addresses CodeRabbit findings on PR cloudposse#2879:

- workdir.BuildPath encoded "/" and "-" identically, so components named
  e.g. "app/local" and "app-local" collided on the same workdir, sharing
  files, metadata, and Terraform state. Encode "/" and "\" as "--" instead.
- BuildPath validated the component name but never the stack name, both of
  which come from user-controlled YAML; a crafted stack value could escape
  BasePath. Move a shared containment check into BuildPath itself so all
  six call sites get it, instead of the two ad hoc copies that existed
  before (and the four call sites that had none).
- decodeTaskFromMap deleted "with"/"container" keys in place, which could
  mutate the caller's own map (e.g. Viper's live config tree) when earlier
  normalization steps returned it unchanged instead of a copy.

Also converts a hard-coded JIT-workdir test case to table-driven per a
separate nitpick on the same PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(fixes): fix EditorConfig indentation in workdir fix-log doc

The numbered list's continuation lines used 3-space indentation
(aligned under the "1. " marker), which fails the repo's
indent_size=2 EditorConfig rule (want multiple of 2). CI's
"Run pre-commit hooks" job caught this. Switched to the 2-space
continuation indentation already used elsewhere in docs/fixes/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir,cmd): make BuildPath's encoding fully injective, restore removed RootCmd commands

Addresses a second CodeRabbit review pass on PR cloudposse#2879:

- The prior "/" -> "--" fix for workdir.BuildPath's component-name
  collision was still not injective: a component literally named
  "app--local" collided with "app/local" (both encode to "app--local").
  Replaced it with a fully injective scheme -- escape the literal hyphen
  ("-" -> "-h") before encoding separators ("/" or "\" -> "-s") -- so "-"
  never appears unescaped in the output and no two distinct component
  names can produce the same encoded path segment. This changes the
  on-disk workdir directory name for existing hyphenated components;
  updated every hardcoded expected-path assertion this touched across
  six packages, several switched to compute the expected path via the
  real BuildPath instead of a hand-rolled formula so they can't go stale
  the same way again.
- restoreRootCmdCommands (added in an earlier pass on this PR) only
  removed commands added to RootCmd after a NewTestKit snapshot; a
  command present in the snapshot but removed mid-test (via
  RootCmd.RemoveCommand) stayed gone for every later test. It now also
  re-adds any snapshot command whose Parent() is no longer RootCmd.

Also fixes a godot comment-period nit and stale sanitized-name comments
in a fixture referencing the earlier "--" encoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: update hardcoded workdir names for BuildPath's injective encoding

Fixes CI failures on all three platforms (linux/windows/macos) from the
previous commit: several tests hardcoded the pre-encoding workdir
directory name (e.g. "dev-vpc-remote-workdir", "dev-null-label-exports",
"test-producer-from-source") instead of the new "-h"/"-s" escaped form
BuildPath now produces for hyphenated component names. These packages
weren't covered by the test sweep before that commit landed:
pkg/ci/plugins/terraform and the tests/ acceptance suite's JIT-source and
source-provisioner-workdir tests.

The pkg/git TestDefaultBranchAndGitHubRepository failure on the Windows
run is unrelated -- a transient runner permission error on
C:/Users/runneradmin/.gitconfig, not a code issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir): give backslash its own escape token, route CleanWorkdir through BuildPath

Addresses a third CodeRabbit review pass on PR cloudposse#2879:

- escapeComponentNameForPath aliased "/" and "\" to the same "-s" token,
  so "ecs/cluster" and `ecs\cluster` still collided. The aliasing was
  meant to keep a component name's encoding OS-independent, but that's
  already guaranteed by the encoding being pure Go string processing
  (never delegated to path/filepath) -- so backslash now gets its own
  token ("-b") at no cost to that property.
- CleanWorkdir had its own separate, unsanitized stack+"-"+component
  formula, never routed through BuildPath. It already couldn't find
  workdirs for "/"-containing components; the encoding fixes on this PR
  widened that to ordinary hyphenated components too. Now delegates to
  BuildPath like every other consumer.
- TestCustomCommandStepContainerFalseOptOutRunsOnHost only proved the
  host command ran, not that docker was never invoked. Now installs the
  fake container runtime and asserts its argument log was never created.
- Two other fix-log docs and this PR's own fix-log doc still described
  earlier, now-superseded encoding examples; updated for consistency.
- Replaced a slash-delimited path literal with filepath.Join in a test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir): handle filesystem-root basePath, de-tautologize workdir_path_test oracles

containWithinBase's absBase+separator prefix check rejected every legitimate
workdir path when basePath resolved to a filesystem root (absBase already
ends in the separator there, so absBase+sep doubled up). Switch to
filepath.Rel, mirroring pkg/provisioner/source/source.go's isWithinBase.

Also replace pkg/component/workdir_path_test.go's BuildPath-derived expected
paths with independent hand-computed literals: BuildAndResolveWorkdirPath
calls the same BuildPath internally, so a setup+assertion pair that both
called BuildPath would silently agree on a wrong path if the encoding ever
regressed again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: cover BuildPath's error-propagation branches across workdir consumers

Adds behavioral tests for the (string, error) BuildPath signature change:
stack-name path-traversal now returns errUtils.ErrPathTraversal instead of
silently resolving, and every caller's new `if err != nil` branch needs its
own test to prove it actually forwards/wraps that error rather than
swallowing it. Also covers resolveExistingSymlinks's non-ENOENT propagation
and buildWorkdirPath's empty-BasePath default, both left untested by the
original patch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir,cmd,tests): contain stack traversal to component-type root, fix bootstrap flag inheritance, fix mock fixture marker

pkg/provisioner/workdir/types.go: BuildPath validated the derived path against
basePath only, but stack (unlike component) was never escaped before being
folded into workdirName -- a stack like "../../components" resolves inside
basePath while still escaping .workdir/<componentType>. Now also validates
against the canonical per-component-type workdir root.

cmd/git/bootstrap.go: CIGitCloneBootstrapRequestedFromRawArgs's throwaway
Cobra tree only registered clone-specific flags, so an inherited global flag
like --config made clone.ParseFlags reject the args and silently report no
CI-bootstrap request. Now registers the real global persistent flags before
parsing.

tests/fixtures/.../components/terraform/mock/main.tf: was byte-identical to
the vendored source-modules/mock/main.tf, including its "vendored" header --
the local component is never vendored, so its component_type marker couldn't
distinguish which module actually produced a given state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir,cmd/git): address PR cloudposse#2879 CodeRabbit round and fix local-backend state loss on re-provision

CodeRabbit review round on PR cloudposse#2879 (verified against current code, not just
the diff it saw):

- pkg/provisioner/source/source.go: attach underlying filesystem errors to
  symlink-resolution failures instead of discarding them.
- pkg/provisioner/workdir/types.go: BuildPath rejects a stack name containing
  "/" or "\" instead of only checking containment after the fact, closing a
  workdir-collision gap (e.g. stack "team/../prod" aliasing stack "prod").
- pkg/provisioner/workdir/clean.go, cmd/terraform/workdir/workdir_helpers.go:
  CleanWorkdir/GetWorkdirInfo/DescribeWorkdir now honor atmos_component
  overrides via BuildPath. The CLI-wired DefaultWorkdirManager had its own
  separate, never-updated path formula that couldn't find any hyphenated
  component's real workdir at all -- fixed too.
- pkg/provisioner/workdir/workdir.go: best-effort migration of a workdir
  found at the pre-escaping path onto the new encoded one, so upgrading
  doesn't orphan existing local state.
- cmd/git/bootstrap.go: a malformed `atmos git clone --depth not-a-number`
  no longer gets masked by an unrelated config/profile error; Cobra's own
  flag-parsing error now surfaces as intended.

Also fixes a real, separate bug found while testing the above: workdir sync
was deleting local-backend Terraform state (terraform.tfstate) on every
re-provision, since only provider lock files and the workspace-specific
terraform.tfstate.d/ were protected from the sync's delete-orphaned-files
pass. A local-backend component's state was silently gone after the second
run. shouldSkipSyncFile now also protects terraform.tfstate,
terraform.tfstate.backup, and .terraform.tfstate.lock.info.

See docs/fixes/2026-08-17-pr2879-coderabbit-round-workdir-and-bootstrap-fixes.md
and docs/fixes/2026-08-17-workdir-sync-deletes-local-backend-state.md for
full details, and website/blog/2026-08-17-container-config-validation-and-workdir-path-encoding.mdx
for the user-facing changelog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workdir): reject only '.'/'..' segments in stack, not every '/'

CI caught a real regression from the previous commit's validateStackForPath:
rejecting any stack value containing "/" broke cmd/terraform/migrate's own
test fixtures (stack "deploy/test") and, transitively, three
tests/cli_workdir_test.go fixtures for hyphenated component names.

Only a literal "." or ".." path segment is an actual collision/traversal
risk (filepath.Join's implicit Clean() can fold it away, aliasing e.g. stack
"team/../prod" onto stack "prod"). A plain "/" without such a segment, like
"deploy/test", is a real, already-supported nesting convention with no
traversal risk -- it just becomes a real subdirectory, exactly as it always
has. Narrowed the check accordingly.

Also fixed tests/cli_workdir_test.go's testWorkdirShow/testWorkdirDescribe/
testWorkdirCleanSpecific fixtures, which hand-rolled a pre-escaping workdir
path for a hyphenated component name instead of computing it via BuildPath
-- the same class of drift the prior commit's CleanWorkdir fix addressed.
testWorkdirShow/testWorkdirDescribe had been silently masking their own
breakage via a weak assert.Contains check that passed on error output too;
tightened to require.NoError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(cmd): extract shared container-override step helper

The "shell" and "script" custom-command step cases each built an identical
workflowPkg.ContainerStepParams and called RunStepContainerOverride, differing
only in the workflowStep and the display command. Extracted into a shared
runContainerOverrideStep closure.

No behavior change: TestCustomCommandStepContainerOverrideRunsInsideContainer
(shell), its _ScriptType variant, and TestCustomCommandStepContainerFalseOptOutRunsOnHost
all still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd,workdir,terraform): address PR cloudposse#2879 CodeRabbit round 3 findings

Propagate executionCtx to non-TTY shell steps and the atmos step type so
Ctrl-C/prompt cancellation actually stops an in-flight custom-command step
instead of letting it run to completion. Make migrateLegacyWorkdir fail
closed on a rename error instead of silently creating a fresh empty
workdir over an orphaned legacy directory that may hold real Terraform
state. Make ExtractComponentPath propagate a BuildPath rejection instead
of falling back to the source component directory, which could point
Terraform at the wrong workdir on a rejected (traversal/invalid) stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd): make workdir clean/describe/show honor atmos_component overrides

resolveComponentConfig passed the caller's already-loaded (processStacks=false)
AtmosConfiguration into ExecuteDescribeComponent, which only does its own full
stack-processing init when passed nil. That branch never ran, so component
resolution always failed silently and every clean/describe/show call fell back
to treating the component as its own instance name -- the exact failure mode
atmos_component-override support exists to prevent. It now builds its own
fully-processed config from the same CLI flag overrides (base-path, config,
config-path, profile) the caller already derived, so overrides actually
resolve. Adds three regression tests that execute the real command path
against a real stack fixture and assert the manager receives the resolved
atmos_component, replacing gomock.Any() assertions CodeRabbit flagged as too
permissive to catch this. Also fixes two stale doc/comment references from the
same review round (obsolete BuildPath encoding description; a stale fixture
path in a test comment).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cmd,provisioner): address PR cloudposse#2879 CodeRabbit round 4 findings

Fixes four independent path-identity gaps in the vendoring/workdir subsystem:
DetermineTargetDirectory's default vendoring target permitted resolving to
the shared component-type directory itself (component name "." or
"child/.."); shouldSkipSyncFile protected local-backend state files by
basename, over-broadly excluding nested source files with the same name;
migrateLegacyWorkdir could rename the wrong identity's directory since its
legacy-name formula isn't injective across stack/component; and
validateStackForPath's segment split silently dropped empty segments from a
leading or repeated "/", letting a stack name alias another's workdir path.
Also fixes a test helper that suppressed all panics instead of only the
expected one, and corrects three stale doc references from earlier rounds.
Skipped one invalid finding (adding perf.Track to pkg/schema/workflow.go
would create an import cycle with pkg/perf).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
zack-is-cool pushed a commit to zack-is-cool/atmos that referenced this pull request Sep 8, 2026
…gs (cloudposse#2899)

* docs(atmos-migration): add mise/aqua migration references, fix kubectl alias

Add from-mise.md and from-aqua.md scenario-keyed migration guides to the
atmos-migration skill, wired in via SKILL.md and AGENTS.md.

Field-testing these docs against real fixtures found two bugs, fixed here:
kubectl's Aqua registry owner/repo is kubernetes/kubectl, not
kubernetes-sigs/kubectl (the wrong alias broke `atmos toolchain install`
for both docs' flagship Shape A example), and the `mise use` CLI mapping
pointed at `atmos toolchain add` (append-only) instead of `atmos toolchain
set` (which actually changes the default version).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(atmos-migration): fix list-continuation indentation to satisfy EditorConfig

from-mise.md and from-aqua.md used 3-space continuation under numbered
list items, matching this skill's other reference docs but failing this
repo's EditorConfig rule (multiple of 2). Shift continuation blocks
(including nested YAML/text fences) to 4-space uniformly; no content
changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(toolchain): default use_lock_file to true via editions

toolchain.lock.yaml pins resolved tool versions and checksums for
reproducible installs, but writing it was opt-in and undocumented
(use_lock_file defaulted to false). Field-testing the mise/aqua migration
docs found their "the lockfile writes automatically" claim didn't hold in
practice for exactly this reason.

Flip the default via the editions system (pkg/edition/journal.go) rather
than a bare default change, so projects pinned to an edition before this
change keep the old opt-in behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(toolchain,ui): set now sets the default version; fix markdown text loss

atmos toolchain set claimed to set a tool's default version but called
the same append-only helper as add, so it never reordered .tool-versions,
and used the resolved owner/repo form to write the file instead of the
name the caller passed, silently duplicating entries for aliased tools
(e.g. "jq") instead of updating them in place. Fixed by calling
AddToolToVersionsAsDefault with the original tool name, matching add.go's
already-correct, already-tested pattern.

Separately, set's own success message silently dropped the tool@version
text it reported (e.g. "Set  in .tool-versions"). Root cause: goldmark's
GFM autolink pass mistakes word@version text for an email address; the
strict-linkify extension correctly un-links it but replaced it with an
ast.KindString node glamour's ANSI renderer has no render case for, so
the text vanished. Any ui.* formatted message with this shape hit the
same bug. Fixed by rebuilding a source-backed ast.Text node instead.

Both were found while field-testing the mise/aqua migration skill docs,
which document `set` as the command a mise-style "make this version
active" migration needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(blog,roadmap): announce toolchain.use_lock_file default flip

Required for the minor label on this PR: a blog post and roadmap entry
for the toolchain.lock.yaml-by-default change in the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): update stale toolchain_set_test.go for set.go's new write behavior

CI acceptance tests (linux, macos) failed: TestToolchainSetTool_Execute
asserted the old behavior where SetToolVersion wrote .tool-versions under
the resolved canonical owner/repo form. That behavior was intentionally
changed in the prior commit (matching AddToolVersion's already-correct
pattern) but this MCP/AI-tool wrapper's test, in a different package,
wasn't updated at the same time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: regenerate golden snapshots for toolchain.use_lock_file default

CI acceptance tests (linux, macos, windows) failed: TestCLICommands'
describe-config-family snapshots didn't account for the
toolchain.use_lock_file default flip landing earlier in this branch --
atmos describe config now resolves and renders that field (and the
previously all-zero-value toolchain block, no longer empty) differently
than what the committed .golden files expected.

Regenerated via `go test ./tests -run 'TestCLICommands/...' -regenerate-snapshots`
per this repo's golden-snapshot policy; diff is exactly the expected
use_lock_file: false -> true change, no unrelated output shifted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(migration): pin registry refs, document edition-gated lockfile, fix mise mappings

Addresses CodeRabbit review on PR cloudposse#2899:
- from-aqua.md: carry the aqua.yaml ref: pin into the converted
  toolchain.registries[] entries (public and custom registry examples) instead
  of dropping it -- an unpinned registry, like an unpinned branch ref, can
  change what gets installed without any change to atmos.yaml.
- from-aqua.md: document that automatic toolchain.lock.yaml management depends
  on the project's edition; projects pinned before 2026-08-05 need
  toolchain.use_lock_file: true explicitly.
- from-mise.md: mise prune has no direct Atmos equivalent (it prunes unused
  versions only); atmos toolchain clean remains the mise implode mapping.
- SKILL.md: add .mise/config.toml to the mise routing entry so repos using
  that path still route to from-mise.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(toolchain): promote a version tracked under a different alias/canonical key

AddToolToVersionsAsDefault (via addToolToVersionsInternal) returned early
whenever findDuplicateKey (formerly wouldCreateDuplicate) matched, even when
the caller wanted the version promoted to the default position. When a
version was already tracked under a *different* key than the caller passed
-- the alias vs. its canonical owner/repo form, or vice versa -- promoting it
silently did nothing instead of reordering it within its existing key.

Same-key updates (e.g. "jq 1.9.0 1.7.1" -> promote 1.7.1) were unaffected:
findDuplicateKey never matches within the same key, so AddVersionToTool's
existing reorder loop already handled that case correctly. Added a
regression test for both the same-key case (documenting the pre-existing
correct behavior) and the cross-key case (reproducing and fixing the bug).

findDuplicateKey/aliasConflictsWithFullName/fullNameConflictsWithAlias now
return the conflicting key instead of a bool, so the caller can promote
within it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(ui/markdown): assert duplicate package-ref labels render twice

TestCustomRenderer_Render_PackageRefLinkify only exercised two *different*
package-ref labels in one message, so an implementation that dedupes by label
and keeps only the first occurrence would still pass. Add a case with the
same reference repeated and assert the rendered occurrence count, not just
presence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(blog,roadmap): scope lockfile reproducibility claim to platform

The toolchain lockfile pins the resolved artifact per platform, so two
different operating-system/architecture combinations can legitimately resolve
to different artifacts for the same declared version. Reword the "byte-for-
byte the same artifact" claim in the blog post and roadmap entry to be scoped
to installs on the same OS/architecture, matching what the lockfile actually
guarantees.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(security): remediate 5 of 7 open Dependabot alerts

- go-git/go-git/v5: v5.19.1 -> v5.19.2 (GHSA alerts cloudposse#270 high, cloudposse#271
  medium; patched in 5.19.2)
- dompurify (website, transitive): pnpm override ^3.4.12 -> ^3.4.13
  (GHSA alert cloudposse#272 medium; the existing override itself was below the
  patched version)
- nanoid (website, transitive): pnpm override ^3.3.15 -> ^3.3.17
  (GHSA alerts cloudposse#274, cloudposse#273 high; same issue -- prior override pinned
  below both patches)

Not fixed: image-size (alerts cloudposse#276, cloudposse#275, high) -- GitHub reports no
patched version exists yet for either advisory (first_patched_version
is null on both). Nothing to bump to; revisit once upstream ships a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [autocommit] formatting fixes

* docs(atmos-migration): correct shim guidance -- Atmos does support toolchain proxies

Both from-mise.md and from-aqua.md claimed Atmos "does not use shims" at
all. That's inaccurate: toolchain.proxies is a real, documented,
Aqua-style shim mechanism (website/docs/cli/configuration/toolchain/proxies.mdx)
-- Atmos creates a command-name link that lazily installs its pinned tool
on first use, same as an Aqua shim. The real distinction from mise/Aqua is
that it's opt-in per command (an explicit proxies: entry), not automatic
for every tool in .tool-versions. Corrected both gotcha sections to
describe the actual behavior and link to the proxies reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(atmos-migration): remove broken tree/<ref> segment from registry source URLs

Confirmed live: Atmos's custom-registry resolver treats everything after
github.com/<owner>/<repo> in `source` as a literal file path. Embedding
tree/<ref> (e.g. .../aqua-registry/tree/main/pkgs) produces a 404 against
raw.githubusercontent.com -- verified directly with curl and by tracing
debug logs through an actual `atmos toolchain install`.

This was worse than a simple broken example: when a custom registry fails
to resolve a tool, Atmos silently falls back to the public Aqua registry
with no warning. A tool that also exists publicly installs anyway, from
the wrong source, so the bug was invisible for common tools and only
surfaced as an outright failure for a tool that exists *only* in the
custom registry -- exactly the from-aqua.md Shape B example
(myorg/internal-tool). Documented this fallback behavior as a new gotcha
with a way to verify a custom registry is actually being used.

Fixed all 4 affected source: values (from-aqua.md Shape A, Shape B's two
registries, from-mise.md Shape A) to keep source at the repository/subpath
and the ref in the separate `ref:` field, per CodeRabbit's finding on
PR cloudposse#2899 -- verified the fix resolves directly via the configured
registry (no fallback) before applying.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): retry transient Windows go-test unlinkat race in acceptance shards

Acceptance Tests (windows, shard 10/10) failed with every package already
reporting ok; the actual failure was go's own "unlinkat ...: The process
cannot access the file because it is being used by another process"
cleanup diagnostic for its temp test binary, a documented Windows race
(commonly Defender's real-time scanner), not a test regression.
commandRunner.run now retries on that exact diagnostic only, leaving any
real test failure to fail immediately as before.

* fix: address CodeRabbit review feedback on PR cloudposse#2899

- agent-skills/atmos-migration/from-mise.md: correct the `mise env`
  mapping -- `atmos toolchain env` only exports PATH, not `[env]`
  table entries, which migrate to a stack or command `env:` block
  instead. `atmos env` is unrelated: it only reads atmos.yaml's own
  global `env:` section.
- docs/fixes/2026-08-20-windows-go-test-unlinkat-retry.md: fix MD040
  (missing fenced-code language), and document the retry-scope
  follow-up below.
- internal/ci/acceptance/command.go: wrap run()'s error with a static
  errCommandFailed sentinel instead of a bare dynamic fmt.Errorf, and
  scope the transient Windows unlinkat retry to actual `go test`
  invocations only. It previously also retried `go tool covdata` and
  precompiled *.test.exe/cmd.test binaries run directly, none of which
  can hit the real race -- retrying them on a coincidental stderr
  match risked rerunning a command with real side effects or masking
  an unrelated failure. Replaced the added bool parameter (which broke
  revive's argument-limit) with an explicit runOptions{dir, env,
  retryTransient} struct, threaded through run.go and coverage.go's
  call sites.
- internal/ci/acceptance/command_test.go: give the two counter-file
  setup t.Fatal(err) calls operation-specific messages, and add
  TestRunDoesNotRetryWhenNotRetryTransient covering the narrowed scope.

The Mergify "PR has conflicts" comment is stale -- gh pr view confirms
mergeable: MERGEABLE after the last merge/push.

* fix(ci): bound stderr retention in acceptance command retry matching

commandRunner.run buffered all stderr into stderrCapture for every
command, even when retryTransient is false and the buffer is never
inspected (short-circuited by ||) -- a verbose test binary could grow
that buffer unbounded and exhaust CI worker memory.

Replace stderrCapture with transientErrorDetector, a bounded (4KB
trailing-window) io.Writer with a sticky matched flag that drops its
window once matched. Non-retryable calls now forward stderr directly
with no capture at all; retryable calls are capped regardless of
output volume. Addresses CodeRabbit PR cloudposse#2899 review comment.

* fix(ci): raise terraform-registry-cache windows timeout to 45m

GitHub Job ID 99475416637 was cancelled by its own 30m job timeout even
though TestTerraformRegistryCache had already passed. Get dependencies,
the test step, and the post-job Go cache save were each independently
~10x slower than a normal run -- the signature of a degraded/throttled
runner that day, not a code regression. This job already had its
timeout raised 20->30 once for the same pattern; 30m still wasn't
enough headroom for an occasional fully-degraded run, so raise it to
45m. See docs/fixes/2026-08-31-terraform-registry-cache-windows-runner-degradation.md.

* chore: gitignore tools/gomodcheck's compiled binary

tools/gomodcheck/.gomodcheck was the only tool binary under tools/*/
missing a .gitignore entry -- tools/lintroller/.lintroller already has
the equivalent pattern for its sibling.

* fix(ci): retry go mod download before go run in website workflows

website-deploy-preview failed with "stream error: ... INTERNAL_ERROR;
received from peer" across many unrelated modules during
`go run . stack schema ...` -- a transient proxy.golang.org mid-stream
reset, the same class already fixed for `go mod download` in
magefiles/build.go, but website-preview-build.yml and
website-deploy-prod.yml run bare `go run .` with no module cache
warm-up and no retry protection at all.

Add a go-mod-download-retry composite action (3 attempts, 15s backoff,
mirroring the existing convention) and wire it in after "Set up Go" in
both workflows, before their go run steps.

* docs(changelog-skill): require grounding blog openers in the real reason

Rule 1a: the opening problem statement must come from the actual PR/
issue/commit reason, not an invented scenario, and stated at the
scope it actually applies to (not narrowed to one migration path when
the gap is category-general). Both failure modes found in review:
2026-08-06-toolchain-lockfile-default.mdx (invented) and
2026-08-05-taskfile-convergence.mdx (over-narrowed).

* fix(tests): retry Floci endpoint health check instead of checking once

TestAzureSecretsFlociE2E failed with "Floci HTTP endpoint is not
reachable at http://localhost:4577" -- the TCP dial succeeded but the
HTTP GET timed out, a startup race where the socket accepts
connections before the app inside is ready to respond. CI's service
containers have no health-check configured, so requireFlociEndpoint's
single 2s check was the only readiness gate, far stricter than the 90s
flociStartupTimeout the local testcontainers auto-start path already
grants for this exact scenario.

requireFlociEndpoint now polls via a new pollUntil helper for up to
flociStartupTimeout instead of checking once. Added unit tests for
pollUntil directly.

* docs(blog): rewrite toolchain lockfile post, retime to 2026-09-01

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(tests): stop pollUntil from overrunning its deadline

Check the deadline before invoking fn (not just after) and cap the
retry sleep to the remaining time, so a slow fn call near the deadline
can no longer push the total run time well past the intended timeout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(blog): clarify toolchain lockfile backfill for already-installed tools

atmos toolchain install skips tools already on disk, so it won't add a
lock entry for them. Point readers to atmos toolchain lock (or
--reinstall) to backfill, and scope the byte-for-byte reproducibility
claim to platforms with a matching lock entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: atmos-pro[bot] <173522224+atmos-pro[bot]@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
preview — da4ca8bd Deployed Dec 15, 2022 by aknysh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor New features that do not break anything

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants