Skip to content

fix(hooks): resolve component path from metadata.component target - #2802

Merged
Andriy Knysh (aknysh) merged 11 commits into
mainfrom
osterman/atmos-issue-2799
Jul 26, 2026
Merged

Andriy Knysh (aknysh) merged 11 commits into
mainfrom
osterman/atmos-issue-2799

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

what

  • Hooks (infracost, trivy, checkov, kics, tflint, kind: command, kind: git, kind: step/kind: steps) now resolve $ATMOS_COMPONENT_PATH — and the hook subprocess's own working directory — from the component's resolved metadata.component target or provisioned workdir, instead of the stack-facing component/alias name.
  • Path resolution for all hook kinds is consolidated behind one exported hooks.ComponentPath(), extended to cover every provisionable component type (terraform/helmfile/packer/ansible/kubernetes/helm).
  • kind: git hooks with no repository: now use the resolved component directory (not the repo root) as their Git workdir, so commit.paths are component-relative like other hook kinds.
  • kind: step/kind: steps hooks default working_directory to the resolved component path when not explicitly set — except type: atmos steps, which re-invoke the atmos binary itself and must keep inheriting the ambient working directory so the nested run resolves the project's atmos.yaml/stacks, not the target component's own directory.
  • A component with a JIT source: and hooks configured now has its source provisioned before those hooks fire (before.terraform.* is a "run before Terraform" event, not a "run before provisioning" event) — except before.terraform.init, which is the source provisioner's own lifecycle event and must keep running first. This only triggers for components that actually have hooks; every terraform subcommand already provisions its own component independently in RunE, so gating on hooks.HasHooks() avoids a second, racing provisioning attempt for components with no hooks at all.
  • When the resolved component directory doesn't exist on disk (component never provisioned, or an early failure before Terraform ever reaches it), the hook subprocess now falls back to the ambient process working directory instead of refusing to start — needed for when: always after-hooks that must still fire and report the failure.
  • A failed JIT provisioning attempt no longer leaves its target directory behind (surfaced by review: a test promised this but didn't assert it, and asserting it failed). vendorToTarget created the target directory before downloading, so a failed download left an empty component directory — enough for path resolution to treat the component as existing — and a partial copy failure would leave a non-empty one that needsProvisioning treats as fully provisioned, silently skipping re-provisioning on the next run. The provisioner now removes the directory only when this attempt created it; a pre-existing directory is left untouched (covered by positive/negative unit tests in pkg/provisioner/source).
  • Multi-component invocations (--all/--affected/--components/--query/--tags/--labels) no longer fail with "component is required": the metadata-resolution step is skipped for the global before/after hook (no single component is resolved yet at that point) and is best-effort everywhere else, so an intentionally invalid stack/component test scenario is rejected by Terraform's own validation with its normal message, not by hook-context preparation with a different one.
  • Added a fix-log record (docs/fixes/2026-07-24-hook-component-path-metadata-component.md) and updated the custom-hooks, git-ops, and hooks-step-types PRDs to document all of the above.
  • Fixed a pre-existing Windows-only test bug (TestComponentPathFor_Fallbacks) surfaced by this change: a hand-rolled filepath.Separator-prefixed "absolute" path isn't actually absolute per Go's filepath.IsAbs on Windows, so u.GetComponentPath's Abs() call legitimately expanded it with the current drive letter, breaking the test's literal expectation. Rooted the fixture paths in a genuine t.TempDir() instead.
  • Fixed pre-existing 3-space (odd) markdown indentation in docs/prd/git-ops.md that failed editorconfig-checker's indent_size=2 rule once this PR made the file "affected" by CI's affected-file validation.
  • Remediated 2 open Dependabot alerts flagged on this branch: github.com/google/cel-go v0.26.0 → v0.29.0 (GHSA-gcjh-h69q-9w9g), and the website's transitive postcss pin bumped from ^8.5.10 to ^8.5.18 (resolves to 8.5.23, GHSA-r28c-9q8g-f849). NOTICE and website/pnpm-lock.yaml regenerated accordingly.

why

  • For a component using the shared-module / "abstract component" pattern (metadata.component: nat-gateway on a component named e.g. nat-gateway-alias), hooks pointed at a directory that doesn't exist on disk (.../nat-gateway-alias instead of .../nat-gateway). For kind: infracost this failed silently — it just logged "Could not autodetect any projects" and reported $0.00, looking like a normal (if boring) result instead of a broken cost-estimation run. atmos describe component already resolved metadata.component correctly; hooks just weren't sharing that resolution.
  • Making that resolution (and JIT provisioning) happen ahead of hook execution surfaced several ordering assumptions across the rest of the hook/lifecycle system that weren't true before — multi-component bulk runs, type: atmos steps, before.terraform.init's own provisioner event, and when: always fallback hooks on early failures — each of which needed its own narrow fix to keep working the way it did before this PR, without losing the original fix.
  • The security fixes close a high-severity path-traversal issue in postcss (arbitrary .map file disclosure) and a medium-severity private-field-exposure issue in cel-go, both flagged by Dependabot on this branch; folding them in here keeps this PR mergeable without a separate open vulnerability.

references

Summary by CodeRabbit

  • Bug Fixes
    • Terraform lifecycle hooks now resolve component metadata consistently (including alias-based selections) and carry the resolved component context into hook execution.
    • Hook subprocesses, Git current-repository actions, and step hooks run from the resolved component directory; ATMOS_COMPONENT_PATH now matches that resolved location.
    • Components with configured source: are provisioned (best-effort) before applicable before.terraform.* hooks; provisioning failures are non-blocking.
    • Step hooks default working_directory to the component directory while preserving explicit values and ambient behavior for Atmos steps.
  • Documentation
    • Updated hook-path, working-directory, and Terraform timing rules; clarified Git and env var behavior.
  • Tests
    • Expanded coverage for metadata resolution, JIT source: provisioning, and working-directory defaults.

…component target

Hooks (infracost, trivy, checkov, kics, tflint, kind: command, kind: git,
kind: step/steps) resolved $ATMOS_COMPONENT_PATH and their own working
directory from the stack-facing component name instead of the resolved
metadata.component target or provisioned workdir, so hooks on aliased
components (the shared-module pattern) pointed at a directory that doesn't
exist on disk.

- Thread ProcessStacks-resolved component info into the hook execution
  context in prepareHookContext.
- Export hooks.ComponentPath() as the single resolution point for all
  hook kinds, extended to cover every provisionable component type.
- Set cmd.Dir on the hook subprocess so relative-path tools are also
  anchored to the resolved component directory, not just the env var.
- Default kind: step/steps working_directory to the same resolved path
  when not explicitly set.

Closes #2799
- github.com/google/cel-go v0.26.0 -> v0.29.0 (GHSA-gcjh-h69q-9w9g,
  medium): json:"-" struct fields were exposed to CEL expressions via
  dyn(obj)["-"] indexing and JSON struct conversion. Minor-version bump
  within a 0.x module, not blocked by the gomod semver-major ignore
  rule. go.mod/go.sum updated via `go get` + `go mod tidy`; NOTICE
  regenerated.
- postcss (transitive, via website/pnpm-lock.yaml) 8.5.16 -> 8.5.23
  (GHSA-r28c-9q8g-f849, high): path traversal via sourceMappingURL
  auto-loading could disclose arbitrary .map file contents. Bumped the
  existing pnpm.overrides pin from ^8.5.10 to ^8.5.18 in
  website/package.json; lockfile regenerated with
  `pnpm install --no-frozen-lockfile`.

No CodeQL alerts were open at remediation time.

Verified: go build ./..., go test on pkg/condition and its consumers
(pkg/config/schema, pkg/project/config, pkg/generator/..., pkg/schema),
website npm run build, and ./custom-gcl run --new-from-rev=origin/main
all pass.
@atmos-pro

atmos-pro Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@github-actions

Copy link
Copy Markdown

Resource Changes Found for bucket in test

Atmos CI

create

Plan: 4 to add, 0 to change, 0 to destroy.
To reproduce this locally, run:

atmos terraform plan bucket -s test

Create

+ aws_s3_bucket.checkov_target
+ aws_s3_bucket.this
+ aws_s3_bucket.trivy_target
+ aws_s3_bucket_public_access_block.trivy_target
Terraform Plan Summary
  # aws_s3_bucket.checkov_target will be created
  + resource "aws_s3_bucket" "checkov_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-checkov-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.this will be created
  + resource "aws_s3_bucket" "this" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + tags_all                    = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.trivy_target will be created
  + resource "aws_s3_bucket" "trivy_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-trivy-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket_public_access_block.trivy_target will be created
  + resource "aws_s3_bucket_public_access_block" "trivy_target" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + restrict_public_buckets = true
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = "atmos-native-ci-e2e-test"

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Hook context now resolves metadata components best-effort and provisions configured sources before eligible user hooks. A shared component-path resolver drives command, Git, and step hook working directories and ATMOS_COMPONENT_PATH, with tests and documentation covering fallback behavior.

Changes

Hook component path resolution

Layer / File(s) Summary
Resolve hook context and provision sources
cmd/terraform/utils.go, cmd/terraform/utils_hooks_test.go, pkg/provisioner/source/*
Hook preparation processes component metadata best-effort, eligible user hooks provision configured source: directories before execution, and failed provisioning removes only newly created targets.
Resolve command hook directories
pkg/hooks/command_engine.go, pkg/hooks/command_engine_test.go, pkg/hooks/main_test.go, pkg/hooks/command_engine_ci_summary_test.go
ComponentPath resolves provisioned, in-repository, and fallback paths; command hooks set existing component directories as subprocess working directories and expose the resolved path.
Apply paths to Git and step hooks
pkg/hooks/kinds/git/*, pkg/hooks/step_engine.go, pkg/hooks/step_engine_test.go
Current-repository Git hooks use component-relative workdirs, and step hooks default working directories except for explicit values and atmos steps.
Document resolved directory behavior
docs/fixes/*, docs/prd/custom-hooks.md, docs/prd/git-ops.md, docs/prd/hooks-step-types.md
Documentation records resolved component paths, source provisioning timing, Git path relativity, and step working-directory defaults.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • cloudposse/atmos#2736 — Reworks Terraform hook dispatch for per-component lifecycle execution.
  • cloudposse/atmos#2805 — Also populates metadata-resolved component fields for hook paths and working directories.

Sequence Diagram(s)

sequenceDiagram
  participant TerraformHooks
  participant ProcessStacks
  participant ComponentPath
  participant CommandHook
  participant GitHook
  participant StepHook
  TerraformHooks->>ProcessStacks: resolve component metadata
  TerraformHooks->>ComponentPath: provision or resolve component directory
  ComponentPath->>CommandHook: set cmd.Dir and ATMOS_COMPONENT_PATH
  ComponentPath->>GitHook: set current-repository workdir
  ComponentPath->>StepHook: default missing working_directory
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: hook component paths now resolve from the metadata.component target.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch osterman/atmos-issue-2799

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/hooks/step_engine_test.go`:
- Around line 345-360: Update the “steps preserve explicit directory” test to
retain explicitDir through the final assertions and compare captured[2] directly
against explicitDir. Keep the existing componentDir assertions for the first two
steps unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 329b3180-d5f2-4484-98bb-8ff90f9c5a80

📥 Commits

Reviewing files that changed from the base of the PR and between 21ef663 and b44a434.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • website/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • NOTICE
  • cmd/terraform/utils.go
  • cmd/terraform/utils_hooks_test.go
  • docs/fixes/2026-07-24-hook-component-path-metadata-component.md
  • docs/prd/custom-hooks.md
  • docs/prd/git-ops.md
  • docs/prd/hooks-step-types.md
  • go.mod
  • pkg/hooks/command_engine.go
  • pkg/hooks/command_engine_ci_summary_test.go
  • pkg/hooks/command_engine_test.go
  • pkg/hooks/kinds/git/engine.go
  • pkg/hooks/kinds/git/engine_test.go
  • pkg/hooks/main_test.go
  • pkg/hooks/step_engine.go
  • pkg/hooks/step_engine_test.go
  • website/package.json

Comment thread pkg/hooks/step_engine_test.go Outdated
…ix editorconfig

Three CI failures from the previous hooks fix (#2799):

- build: `atmos terraform plan --all` failed with "component is required".
  prepareHookContext's new ProcessStacks call unconditionally required
  ComponentFromArg, but multi-component invocations (--all/--affected/
  --components/--query/--tags/--labels) fire the global before/after hook
  before any single component is resolved. Skip the call when
  ComponentFromArg is empty; per-component hooks inside the component
  walker already have a resolved component and are unaffected.

- [floci] terraform-tests: a `type: atmos` step inside a `kind: steps`
  hook failed with "Stacks directory not found" because it re-invoked
  atmos as a subprocess with cmd.Dir forced to the resolved component
  directory. That step type must keep inheriting the ambient process
  working directory so the nested atmos run resolves the *project's*
  atmos.yaml/stacks, not the target component's own directory.
  setDefaultStepWorkingDirectory now excludes type: atmos.

- Validation (affected): touching docs/prd/git-ops.md made the whole file
  "affected", surfacing pre-existing 3-space (odd) indentation under
  numbered-list continuations that fails editorconfig-checker's
  indent_size=2 rule for *.md. Re-indented the three affected blocks to
  4 spaces.

Also addresses CodeRabbit review feedback on PR #2802: the "steps
preserve explicit directory" test compared captured[2] against itself;
now asserts it directly against explicitDir.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/hooks/step_engine_test.go (1)

370-389: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use table-driven cases for the working-directory scenarios.

This test covers atmos-step exclusion, defaulting, and explicit preservation. Expressing these as table-driven cases will align with the repository’s Go test guidelines and keep the scenarios consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/hooks/step_engine_test.go` around lines 370 - 389, Refactor
TestSetDefaultStepWorkingDirectory_ExcludesAtmosStepType into table-driven cases
covering atmos-step exclusion, shell-step defaulting, and explicit
working-directory preservation. Define each case’s input WorkflowStep and
expected directory, then run them through a subtest that calls
setDefaultStepWorkingDirectory and asserts the expected result.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/hooks/step_engine_test.go`:
- Around line 387-389: Update the test around setDefaultStepWorkingDirectory to
construct the explicit working directory with t.TempDir() or filepath.Join
rather than hardcoding `/explicit`, and assert against the same platform-neutral
value while preserving verification that explicit paths are not overwritten.

---

Nitpick comments:
In `@pkg/hooks/step_engine_test.go`:
- Around line 370-389: Refactor
TestSetDefaultStepWorkingDirectory_ExcludesAtmosStepType into table-driven cases
covering atmos-step exclusion, shell-step defaulting, and explicit
working-directory preservation. Define each case’s input WorkflowStep and
expected directory, then run them through a subtest that calls
setDefaultStepWorkingDirectory and asserts the expected result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d1e7aa7-f75f-41ea-96f7-f512255fc107

📥 Commits

Reviewing files that changed from the base of the PR and between b44a434 and b061934.

📒 Files selected for processing (6)
  • cmd/terraform/utils.go
  • cmd/terraform/utils_hooks_test.go
  • docs/prd/git-ops.md
  • docs/prd/hooks-step-types.md
  • pkg/hooks/step_engine.go
  • pkg/hooks/step_engine_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/prd/hooks-step-types.md
  • pkg/hooks/step_engine.go
  • cmd/terraform/utils.go

Comment thread pkg/hooks/step_engine_test.go Outdated
A `before.terraform.*` hook (other than before.terraform.init, the source
provisioner's own hook) is a "run before Terraform" event, not a "run
before the component's source is provisioned" event. Previously,
before-hooks fired in Cobra's PreRunE — before RunE performed JIT source
provisioning — so a hook on a freshly-provisioned (first-run) `source:`
component saw a component path that did not exist yet. Combined with the
earlier #2799 fix (which sets the hook subprocess's cmd.Dir to that
path), a kind: command hook would fail outright instead of silently
running in the wrong ambient directory.

prepareHookContext now calls component.ProvisionAndResolveComponentPath
for the resolved single component before firing hooks, matching what
ExecuteTerraform does moments later in RunE. This is a no-op for
components with no `source:` configured (hooks run normally, as before)
and for already-provisioned/non-expired components. Provisioning
failures here are logged, not returned: the actual Terraform command
performs the same provisioning and surfaces the authoritative error.

Also addresses a CodeRabbit review comment on PR #2802: a test asserted
a hardcoded "/explicit" path, which violates this repo's cross-platform
test rule; replaced with t.TempDir().

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/terraform/utils.go`:
- Around line 280-290: Gate the ensureComponentSourceProvisioned call in the
surrounding Terraform hook flow so it is skipped when the command is init, while
preserving pre-provisioning for other Terraform commands. Add a regression test
covering before.terraform.init to verify the hook runs without prior source
provisioning.

In `@docs/prd/custom-hooks.md`:
- Line 101: Update the documentation around `prepareHookContext` and
`$ATMOS_COMPONENT_PATH` to state that the path is guaranteed to be real and
populated only when best-effort source provisioning succeeds. Document that a
provisioning failure prevents hook startup, is logged, and follows `on_failure`
to determine whether the parent command continues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e3825c7-2949-487f-9c26-df7e77534781

📥 Commits

Reviewing files that changed from the base of the PR and between b061934 and d7a2761.

📒 Files selected for processing (4)
  • cmd/terraform/utils.go
  • cmd/terraform/utils_hooks_test.go
  • docs/prd/custom-hooks.md
  • pkg/hooks/step_engine_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/hooks/step_engine_test.go

Comment thread cmd/terraform/utils.go Outdated
Comment thread docs/prd/custom-hooks.md Outdated
Addresses two CodeRabbit review findings on PR #2802:

- Major: ensureComponentSourceProvisioned had no guard for `init`, so
  before.terraform.init — the source provisioner's own lifecycle event
  (HookEventBeforeTerraformInit) — ran after provisioning had already
  happened, inverting the one event whose entire point is to fire before
  the source exists. Gated the call on cmd_.Name() != "init". Added
  TestPrepareHookContextProvisionsSourceBeforeHooks and
  TestPrepareHookContextSkipsProvisioningForInit, exercising
  prepareHookContext end-to-end against a self-contained temp fixture
  with a local JIT source (no network dependency).

- Minor: docs/prd/custom-hooks.md said $ATMOS_COMPONENT_PATH "always"
  resolves to a populated directory, but the same paragraph already
  documents that provisioning failures are only logged. Clarified that
  the guarantee holds only when provisioning succeeds, and that
  before.terraform.init is excluded from this pre-provisioning step.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/prd/custom-hooks.md`:
- Line 101: Update the documentation’s failure behavior around ComponentPath()
to reflect that unresolved component paths fall back to the process working
directory, so the hook subprocess may still start there. Alternatively, if the
documented contract requires refusing to start when the component directory is
missing, change ComponentPath() to fail closed and preserve the stated
on_failure behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8bbc9c74-e46e-415a-b161-5cb7e3ba14ae

📥 Commits

Reviewing files that changed from the base of the PR and between d7a2761 and 8437529.

📒 Files selected for processing (3)
  • cmd/terraform/utils.go
  • cmd/terraform/utils_hooks_test.go
  • docs/prd/custom-hooks.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/terraform/utils.go

Comment thread docs/prd/custom-hooks.md Outdated
…ake metadata resolution best-effort

Two regressions from the prior hook-context fixes, found via the failing
Acceptance Tests (linux/windows/macos) job:

- TestJITSource_WorkdirWithLocalComponent_AllSubcommands failed for every
  subcommand after the first (show, state, taint, untaint, metadata,
  modules, providers, test, validate, ...) with "no such file or
  directory", and TestJITSource_GenerateVarfile/GenerateBackend failed
  outright. Root cause: ensureComponentSourceProvisioned ran
  unconditionally in prepareHookContext for every terraform subcommand,
  racing a second, independent provisioning attempt against the one
  RunE (ExecuteTerraform) already performs for every subcommand. The
  fixture component has no hooks configured at all, so this second
  attempt had nothing to gain and nothing to synchronize against.
  Moved the call into runUserHooks, gated on hooks.HasHooks() — it now
  only runs for components that actually have hooks, which is the only
  case that needed it in the first place.

- Several TestCLICommands golden-snapshot tests (invalid stack, invalid
  component, workflow failures) regressed: prepareHookContext's
  ProcessStacks call (added to resolve metadata.component for hooks)
  requires a valid stack/component and, on failure, aborted PreRunE with
  an errors.Join(ErrInitializeCLIConfig, ...)-wrapped message —
  different from RunE's own, differently-formatted validation error for
  the exact same invalid input. Since PreRunE failing skips RunE
  entirely, the wrapped hook-layer message now won it over the expected
  one. Made this call best-effort: on failure, log and fall back to the
  unresolved component/stack. GetHooks already tolerates an empty
  Stack/Component by returning no hooks, so hook discovery no-ops
  cleanly and RunE's own validation produces the authoritative,
  correctly-formatted error.

Verified against all three failure classes locally: full
TestJITSource_WorkdirWithLocalComponent_AllSubcommands (21 subcommands),
TestJITSource_GenerateVarfile/GenerateBackend, the 6 previously-failing
TestCLICommands snapshot cases, and the broader terraform/workflow
snapshot suites all pass.
Addresses a CodeRabbit review finding on PR #2802: the doc claimed a
missing resolved directory unconditionally prevents the hook subprocess
from starting, but ComponentPath()'s in-repo fallback only checks that
a path was computed (via u.GetComponentPath), not that it exists on
disk — it falls back to the process's own working directory only when
the component type has no configured base path or the hook context is
incomplete. Clarified both cases: an unprovisioned component still
resolves to its (missing) would-be directory and fails to start there,
while the narrower fallback-to-CWD case does not fail at all.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 25, 2026
…sn't exist

Two more regressions from the hook-path fixes, found via the failing
Acceptance Tests (linux/windows/macos) job:

- TestHelmfileRun_NodeHooksFallbackOnEarlyFailure (all 3 platforms):
  prepareSubprocess unconditionally set cmd.Dir to ComponentPath(ctx),
  so a kind: command hook's subprocess refused to start with "no such
  file or directory" whenever the component was never provisioned.
  This broke a `when: always` after-hook that must still fire on an
  early failure — before Terraform ever reaches the component (e.g.
  use_eks: true with no kubeconfig_path, rejected deterministically
  before any hook is touched) — so CI/user hooks can observe the
  failure. Added existingComponentDir: it returns ComponentPath(ctx)
  only when that directory actually exists on disk, otherwise "", so
  runSubprocess leaves cmd.Dir unset and the hook inherits the ambient
  process working directory instead of failing to start.
  $ATMOS_COMPONENT_PATH is unaffected — it still reports the resolved
  (possibly nonexistent) path unconditionally via buildAtmosEnv.

- TestComponentPathFor_Fallbacks (windows only): a pre-existing test bug
  surfaced by switching ComponentPath's in-repo fallback to
  u.GetComponentPath (from a plain filepath.Join) earlier in this PR.
  The test built its "absolute" fixture paths with
  filepath.Join(string(filepath.Separator), "repo", ...), which on
  Windows is `\repo\...` — not absolute per filepath.IsAbs (no drive
  letter) — so u.GetComponentPath's filepath.Abs() call legitimately
  expanded it to `C:\repo\...`, breaking equality against the test's
  unexpanded `want` value. Rooted the fixture paths in a genuine
  t.TempDir() instead, which is truly absolute (with drive letter) on
  every platform.

Verified: the exact TestHelmfileRun_NodeHooksFallbackOnEarlyFailure
scenario now passes, the new TestCommandEngine_
FallsBackToAmbientCWDWhenComponentDirMissing regression test passes,
TestComponentPathFor_Fallbacks passes, and the full pkg/hooks/... and
cmd/helmfile/... suites pass.
@codecov

codecov Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.35802% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.84%. Comparing base (e37dd0d) to head (bed2e95).

Files with missing lines Patch % Lines
cmd/terraform/utils.go 83.33% 2 Missing and 1 partial ⚠️
pkg/hooks/command_engine.go 95.65% 1 Missing and 1 partial ⚠️
pkg/provisioner/source/provision_hook.go 60.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2802      +/-   ##
==========================================
+ Coverage   81.82%   81.84%   +0.02%     
==========================================
  Files        1793     1793              
  Lines      173104   173173      +69     
==========================================
+ Hits       141634   141730      +96     
+ Misses      23667    23641      -26     
+ Partials     7803     7802       -1     
Flag Coverage Δ
unittests 81.84% <91.35%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/hooks/kinds/git/engine.go 94.38% <100.00%> (+0.09%) ⬆️
pkg/hooks/step_engine.go 84.23% <100.00%> (+0.53%) ⬆️
pkg/hooks/command_engine.go 90.48% <95.65%> (+0.35%) ⬆️
pkg/provisioner/source/provision_hook.go 89.95% <60.00%> (+4.44%) ⬆️
cmd/terraform/utils.go 69.91% <83.33%> (+0.35%) ⬆️

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Codecov flagged PR #2802's patch coverage at 69.74% (target 85%), with
the worst gaps in cmd/terraform/utils.go and pkg/hooks/command_engine.go.
Adds tests for the previously-uncovered branches:

- prepareHookContext: a ProcessStacks failure (no --stack given) is
  swallowed rather than surfaced as PreRunE's error, leaving info
  unresolved for RunE's own validation to handle.
- ensureComponentSourceProvisioned: a JIT source that fails to
  provision (nonexistent local path) is logged, not returned/panicked —
  the function is void-returning by design so hooks still get a chance
  to run.
- ComponentPath/resolveProvisionedWorkdir: prefers an actually
  provisioned workdir on disk over the in-repo fallback.
- componentBasePath: covers the packer/ansible/kubernetes/helm base
  paths and the unrecognized-component-type default (falls back to the
  working directory).

Two lines remain uncovered by design, noted inline: the u.GetComponentPath
error branch in ensureComponentSourceProvisioned and
resolveInRepoComponentPath's err/empty-path branch both require
os.Getwd() to fail from inside a live test process, which isn't reliably
reproducible cross-platform (verified experimentally on macOS) — this is
genuinely defensive/unreachable code, not a coverage shortfall.

Test-only change; no production code touched. Verified via
`atmos fix coverage origin/main` (STATUS: OK) and the full
cmd/terraform/... and pkg/hooks/... suites passing.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/terraform/utils_hooks_test.go`:
- Around line 1015-1042: Extend
TestEnsureComponentSourceProvisioned_ProvisioningFailureIsLoggedNotReturned to
assert that filepath.Join(terraformDir, "app") does not exist after
ensureComponentSourceProvisioned returns. Keep the existing NotPanics check and
use an appropriate filesystem assertion to detect any partially created
component directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 501ddbf4-ce3e-4b2b-adae-3947ac2a2d83

📥 Commits

Reviewing files that changed from the base of the PR and between 8437529 and acb1167.

📒 Files selected for processing (5)
  • cmd/terraform/utils.go
  • cmd/terraform/utils_hooks_test.go
  • docs/prd/custom-hooks.md
  • pkg/hooks/command_engine.go
  • pkg/hooks/command_engine_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/hooks/command_engine.go
  • cmd/terraform/utils.go

Comment thread cmd/terraform/utils_hooks_test.go
…attempt

CodeRabbit flagged that TestEnsureComponentSourceProvisioned_ProvisioningFailureIsLoggedNotReturned
promised "a failed provisioning attempt must leave no component directory
behind" without asserting it. Adding the assertion exposed a real bug:
vendorToTarget runs os.MkdirAll(targetDir) before VendorSource, so a failed
download left an empty target directory behind — and a partial copy failure
would leave a non-empty one, which needsProvisioning treats as fully
provisioned and silently skips re-provisioning on the next run.

Track whether the attempt created the directory and remove it again on
failure; a pre-existing directory is left untouched. Covered by the
strengthened cmd/terraform test plus direct positive/negative unit tests in
pkg/provisioner/source.

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

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/provisioner/source/provision_hook.go (1)

204-225: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not delete a target this invocation did not create.

The os.Stat/os.MkdirAll sequence is TOCTOU: another process can create or populate targetDir after Line 210, while createdTarget remains true. If this provisioning then fails, Lines 222-225 can remove the other process’s source. Use a target-scoped lock or isolated staging directory with atomic publish, rather than inferring ownership from a prior Stat.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/provisioner/source/provision_hook.go` around lines 204 - 225, Replace the
TOCTOU-prone createdTarget check around os.Stat, os.MkdirAll, and VendorSource
with a target-scoped lock or isolated staging directory followed by atomic
publish. Ensure cleanup after provisioning failure only removes resources owned
by this invocation, never an existing or concurrently created targetDir.
🧹 Nitpick comments (1)
pkg/provisioner/source/provision_hook_test.go (1)

1464-1505: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the cleanup cases table-driven.

These tests differ only by target pre-existence and expected retention. Combine them into cases to keep the shared provisioning contract together. As per coding guidelines, “Use table-driven tests for testing multiple scenarios.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/provisioner/source/provision_hook_test.go` around lines 1464 - 1505,
Combine TestAutoProvisionSource_FailedProvisioningCleansUpCreatedTargetDir and
TestAutoProvisionSource_FailedProvisioningKeepsPreexistingTargetDir into one
table-driven test. Define cases for a newly created target and a pre-existing
empty target, including each case’s setup and expected directory retention, then
run the shared AutoProvisionSource failure assertions through subtests.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pkg/provisioner/source/provision_hook.go`:
- Around line 204-225: Replace the TOCTOU-prone createdTarget check around
os.Stat, os.MkdirAll, and VendorSource with a target-scoped lock or isolated
staging directory followed by atomic publish. Ensure cleanup after provisioning
failure only removes resources owned by this invocation, never an existing or
concurrently created targetDir.

---

Nitpick comments:
In `@pkg/provisioner/source/provision_hook_test.go`:
- Around line 1464-1505: Combine
TestAutoProvisionSource_FailedProvisioningCleansUpCreatedTargetDir and
TestAutoProvisionSource_FailedProvisioningKeepsPreexistingTargetDir into one
table-driven test. Define cases for a newly created target and a pre-existing
empty target, including each case’s setup and expected directory retention, then
run the shared AutoProvisionSource failure assertions through subtests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6db7f7dc-fce4-4464-ac00-53b3cc780a17

📥 Commits

Reviewing files that changed from the base of the PR and between acb1167 and bed2e95.

📒 Files selected for processing (3)
  • cmd/terraform/utils_hooks_test.go
  • pkg/provisioner/source/provision_hook.go
  • pkg/provisioner/source/provision_hook_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/terraform/utils_hooks_test.go

@aknysh
Andriy Knysh (aknysh) merged commit b0dcb79 into main Jul 26, 2026
96 checks passed
@atmos-pro

atmos-pro Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@aknysh
Andriy Knysh (aknysh) deleted the osterman/atmos-issue-2799 branch July 26, 2026 15:23
@github-actions

Copy link
Copy Markdown

These changes were released in v1.224.1.

This branch was successfully deployed

1 active (outdated) and 1 inactive deployments
screengrabs — bed2e952 Deployed Jul 26, 2026 by aknysh via build #676
preview — 67cf2cd8 Deployed Jul 25, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch A minor, backward compatible change size/l Large size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hooks: built-in kinds resolve $ATMOS_COMPONENT_PATH to the wrong directory for non-JIT components using metadata.component

2 participants