Skip to content

feat: version-aware JIT source provisioning with TTL-based cleanup#2010

Merged
aknysh merged 23 commits intomainfrom
osterman/jit-vendor-force
Feb 5, 2026
Merged

feat: version-aware JIT source provisioning with TTL-based cleanup#2010
aknysh merged 23 commits intomainfrom
osterman/jit-vendor-force

Conversation

@osterman
Copy link
Member

@osterman osterman commented Jan 22, 2026

what

  • Implement version-aware JIT source provisioning that automatically re-provisions workdirs when remote source version or URI changes
  • Add incremental local sync using per-file checksum comparison (SyncDir) instead of full directory copy
  • Support TTL-based cleanup for stale workdirs with --expired, --ttl, and --dry-run flags
  • Move workdir metadata from .workdir-metadata.json to .atmos/metadata.json for better organization
  • Track source_uri, source_version, and last_accessed timestamps in metadata

Additional Fixes

why

  • Previously, changing a component's source version required manually cleaning the workdir before changes took effect
  • Full directory copy was inefficient for local development with frequent small changes
  • No mechanism existed to automatically clean up stale workdirs that accumulate over time
  • Enhanced metadata enables intelligent provisioning decisions and better observability
  • Generate commands failed with JIT-sourced components because they lacked source provisioning (generate varfile and generate backend don't work with JIT vendoring #2019)

references

  • New pkg/duration package extracted for reusable duration parsing
  • Updated workdir list and workdir show commands display version and access information
  • Blog post: website/blog/2025-01-22-version-aware-jit-provisioning.mdx

Summary by CodeRabbit

  • New Features
    • JIT source provisioning now takes precedence over local components across all terraform commands when source and workdir are enabled.
    • Automatic component refresh when version or source URI changes.
    • TTL-based cleanup for stale workdirs using --ttl flag (e.g., --ttl=7d).
    • Enhanced workdir information displays source type, version, and last accessed timestamp.
    • Incremental file synchronization—only changed files sync to workdirs.

@osterman osterman requested a review from a team as a code owner January 22, 2026 21:38
@github-actions github-actions bot added the size/xl Extra large size PR label Jan 22, 2026
@mergify
Copy link

mergify bot commented Jan 22, 2026

Warning

This PR exceeds the recommended limit of 1,000 lines.

Large PRs are difficult to review and may be rejected due to their size.

Please verify that this PR does not address multiple issues.
Consider refactoring it into smaller, more focused PRs to facilitate a smoother review process.

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

This PR adds version-aware JIT source provisioning with automatic component refresh, TTL-based workdir cleanup, incremental per-file synchronization, enhanced metadata tracking with platform-specific locking, and JIT provisioning support for generate commands.

Changes

Cohort / File(s) Summary
Workdir Manager & CLI
cmd/terraform/workdir/mock_workdir_manager_test.go, cmd/terraform/workdir/workdir_clean.go, cmd/terraform/workdir/workdir_clean_cmd_test.go
Extends MockWorkdirManager with CleanExpiredWorkdirs; adds --expired, --ttl, --dry-run flags; validates mutual exclusivity rules; routes expired cleanup through new cleanExpiredWorkdirs helper; includes comprehensive CLI validation tests.
Workdir Display & Listing
cmd/terraform/workdir/workdir_list.go, cmd/terraform/workdir/workdir_show.go, cmd/terraform/workdir/workdir_list_test.go, cmd/terraform/workdir/workdir_show_test.go
Updates table output to display SourceType (default "local"), SourceVersion (default "-"), and LastAccessed (formatted or "-"); conditionally shows Source URI/Version for remote sources; adds test coverage for timestamp handling and remote source display.
Workdir Core Infrastructure
cmd/terraform/workdir/workdir_helpers.go, cmd/terraform/workdir/workdir_helpers_test.go
Extends WorkdirInfo with SourceType, SourceURI, SourceVersion, LastAccessed fields; updates ListWorkdirs and GetWorkdirInfo to populate from metadata; adds CleanExpiredWorkdirs interface method; removes old readWorkdirMetadata function; adds tests for metadata reading and cleanup scenarios.
Metadata Management & Locking
pkg/provisioner/workdir/metadata.go, pkg/provisioner/workdir/metadata_lock_unix.go, pkg/provisioner/workdir/metadata_lock_windows.go, pkg/provisioner/workdir/metadata_test.go, pkg/provisioner/workdir/metadata_lock_unix_test.go
Implements atomic metadata read/write with platform-specific flock (Unix) and direct file access (Windows); supports legacy metadata location fallback; provides UpdateLastAccessed, ReadMetadata, WriteMetadata APIs; includes comprehensive tests for locking, concurrency, and error handling.
Workdir Types & Paths
pkg/provisioner/workdir/types.go
Changes metadata storage from .workdir-metadata.json to .atmos/metadata.json; adds SourceURI, SourceVersion, LastAccessed fields; introduces SourceTypeRemote constant; adds MetadataPath() public function; includes AtmosDir and MetadataFile constants.
Filesystem Sync Operations
pkg/provisioner/workdir/fs.go, pkg/provisioner/workdir/interfaces.go, pkg/provisioner/workdir/mock_interfaces_test.go, pkg/provisioner/workdir/fs_test.go
Adds SyncDir(src, dst, hasher) to FileSystem interface for incremental per-file sync with change detection; skips .atmos metadata; includes fileNeedsCopy, deleteRemovedFiles, copyFile helpers; includes extensive test coverage for sync scenarios.
Expired Workdir Cleanup
pkg/provisioner/workdir/clean.go, pkg/provisioner/workdir/clean_test.go
Implements CleanExpiredWorkdirs with TTL parsing via duration.Parse; discovers expired workdirs via LastAccessed/UpdatedAt/CreatedAt; supports dry-run reporting and actual deletion; adds formatDuration helper; includes 469 lines of test coverage.
Workdir Service & Provisioning
pkg/provisioner/workdir/workdir.go, pkg/provisioner/workdir/workdir_test.go, pkg/provisioner/workdir/integration_test.go
Replaces CopyDir with SyncDir for incremental sync; adds atomic WriteMetadata via public API; introduces buildLocalMetadata to preserve CreatedAt/UpdatedAt/LastAccessed; refactors to emit different UI messages based on sync changes; updates integration tests for metadata paths and SyncDir usage.
Source Provisioning Hooks
pkg/provisioner/source/provision_hook.go, pkg/provisioner/source/provision_hook_test.go
Enhances needsProvisioning signature to return (bool, string) with reason; implements metadata-driven provisioning decisions; adds checkMetadataChanges to detect version/URI changes; adds writeWorkdirMetadata for local/remote sources; adds isNonEmptyDir and isLocalSource helpers; includes 469 lines of test coverage.
Duration Parsing
pkg/duration/duration.go, pkg/duration/duration_test.go
New package providing Parse(s) (int64, error) and ParseDuration(s) (time.Duration, error) supporting numeric, suffix-based (s/m/h/d), and keyword formats (hourly/daily/weekly/monthly/yearly); includes overflow protection and centralized error handling; 150+ lines of test coverage.
Configuration & Schema
pkg/schema/schema.go, pkg/config/cache.go, pkg/config/cache_test.go
Adds ProvisionSettings and ProvisionWorkdirSettings to AtmosSettings; replaces parseFrequency with duration.Parse in shouldCheckForUpdatesAt; simplifies cache update frequency parsing.
JIT Provisioning in Generate Commands
internal/exec/helmfile_generate_varfile.go, internal/exec/packer_output.go, internal/exec/terraform.go
Adds AutoProvisionSource calls before component path validation with 5-minute timeout; handles workdir path precedence over local component paths; uses cfg.TerraformComponentType constant instead of hardcoded "terraform".
Examples & Documentation
examples/source-provisioning/README.md, examples/source-provisioning/stacks/deploy/dev.yaml, examples/source-provisioning/stacks/deploy/prod.yaml, website/blog/2026-01-22-version-aware-jit-provisioning.mdx
Updates source-provisioning example with weather (local) and ipinfo (remote) components; rewrites README with Key Concepts section; adds blog post documenting version-aware provisioning, TTL cleanup, and metadata tracking.
Integration Tests & Snapshots
tests/cli_jit_source_workdir_test.go, tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden, tests/snapshots/TestCLICommands_secrets-masking_describe_config.stdout.golden, website/src/data/roadmap.js, lychee.toml
Adds 350 lines of CLI integration tests for JIT provisioning with workdir; updates config snapshots with provision field; updates roadmap milestones; adds kubernetes.io URL exclusion to lychee.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI/Terraform
    participant Provisioner as Source Provisioner
    participant Metadata as Metadata Manager
    participant FileSystem as FileSystem
    participant Workdir as Workdir Service

    User->>CLI: terraform plan (with source configured)
    CLI->>Provisioner: AutoProvisionSource()
    
    Provisioner->>Metadata: ReadMetadata(workdirPath)
    Metadata-->>Provisioner: WorkdirMetadata or nil
    
    alt Metadata exists
        Provisioner->>Provisioner: checkMetadataChanges()
        alt Version/URI changed
            Provisioner->>Workdir: SyncDir(source, workdir)
            Workdir->>FileSystem: SyncDir (incremental sync)
            FileSystem-->>Workdir: changed=true
            Provisioner->>Metadata: WriteMetadata() with new version
        else No changes
            Provisioner-->>CLI: ready (skip re-provisioning)
        end
    else No metadata (new workdir)
        Provisioner->>Workdir: SyncDir(source, workdir)
        Workdir->>FileSystem: SyncDir (copy all files)
        Provisioner->>Metadata: WriteMetadata() with SourceVersion
    end
    
    CLI->>CLI: Use workdir for component execution
    CLI-->>User: Terraform result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • PR #1876 — Introduces core workdir provisioning/management infrastructure that this PR extends with TTL-based cleanup and metadata fields.
  • PR #1962 — Modifies the same examples/source-provisioning example stacks to demonstrate local/remote component vendoring.
  • PR #2054 — Updates generate varfile and generate backend command flows with JIT provisioning logic, addressing the same component-path resolution issue.

Suggested Reviewers

  • osterman
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.28% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature: version-aware JIT source provisioning with TTL-based cleanup, which aligns with the primary changes throughout the changeset.
Linked Issues check ✅ Passed The PR successfully addresses issue #2019 by enabling JIT provisioning in generate commands (terraform generate varfile, generate backend, helmfile generate varfile, packer output) through proper component path resolution.
Out of Scope Changes check ✅ Passed All changes align with stated objectives: version tracking, incremental sync, TTL cleanup, metadata relocation, and command enhancements. Cache.go refactoring to use duration.Parse is a supporting utility extracted for reuse.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch osterman/jit-vendor-force

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 and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces version-aware JIT source provisioning with TTL-based workdir cleanup. It adds duration parsing utilities, workdir metadata management with platform-specific locking, incremental file synchronization, and a new CleanExpiredWorkdirs command. Provisioning logic is refactored to proactively check provisioning necessity based on version/URI changes and metadata state.

Changes

Cohort / File(s) Summary
Duration Parsing Utilities
pkg/duration/duration.go, pkg/duration/duration_test.go, pkg/config/cache.go, pkg/config/cache_test.go
New duration package with Parse() and ParseDuration() functions supporting integer seconds, suffix-based durations (s, m, h, d), and keywords (daily, weekly, monthly, yearly). Cache frequency parsing simplified to use duration package instead of custom parseFrequency().
Workdir Metadata Management
pkg/provisioner/workdir/types.go, pkg/provisioner/workdir/metadata.go, pkg/provisioner/workdir/metadata_lock_unix.go, pkg/provisioner/workdir/metadata_lock_windows.go
New metadata fields added: SourceURI, SourceVersion, LastAccessed. Introduces MetadataPath() and MetadataFile constant pointing to .atmos/metadata.json. Platform-specific locking: Unix uses flock with retries; Windows uses sleep-based approach. Public APIs: ReadMetadata(), WriteMetadata(), UpdateLastAccessed() with atomic operations and read-lock support.
Workdir Cleanup & Expiration
pkg/provisioner/workdir/clean.go, cmd/terraform/workdir/workdir_clean.go
New CleanExpiredWorkdirs() function detects expired workdirs by comparing last accessed time against TTL duration. Adds --expired, --ttl, --dry-run flags to CLI. Includes ExpiredWorkdirInfo type and helpers (findExpiredWorkdirs(), checkWorkdirExpiry(), getLastAccessedTime(), formatDuration()).
Workdir Synchronization
pkg/provisioner/workdir/fs.go, pkg/provisioner/workdir/interfaces.go, pkg/provisioner/workdir/workdir.go
New SyncDir() method replaces full-copy behavior with incremental per-file sync (checks hashes, copies changed files, deletes removed files). Skips .atmos/ directory. Refactored workdir provisioning to use sync-based flow instead of unconditional copy; metadata timestamps preserved across syncs.
Provisioning Flow Refactoring
pkg/provisioner/source/provision_hook.go, pkg/provisioner/source/provision_hook_test.go, internal/exec/terraform.go
Refactored AutoProvisionSource() to use needsProvisioning() check (returns bool and reason string). Provisioning invoked unconditionally when source is configured, before path validation. New helpers: checkMetadataChanges() (detects version/URI drift), writeRemoteMetadata() (creates/updates remote source metadata). CLI provisioning moved before path existence checks.
CLI Commands & Helpers
cmd/terraform/workdir/workdir_helpers.go, cmd/terraform/workdir/workdir_helpers_test.go, cmd/terraform/workdir/workdir_list.go, cmd/terraform/workdir/workdir_show.go, cmd/terraform/workdir/mock_workdir_manager_test.go
Enhanced WorkdirInfo with SourceType, SourceURI, SourceVersion, LastAccessed fields. ListWorkdirs() and GetWorkdirInfo() now use ReadMetadata() API. Table output adds TYPE, VERSION, LAST_ACCESSED columns. Show command displays conditional source rows (remote vs local) and last accessed timestamp. New mock method CleanExpiredWorkdirs() added to MockWorkdirManager.
Mocks & Integration Tests
pkg/provisioner/workdir/mock_interfaces_test.go, pkg/provisioner/workdir/integration_test.go, pkg/provisioner/workdir/workdir_test.go
Updated mocks to include SyncDir() method signature. Integration tests refactored to use SyncDir instead of CopyDir, create expected workdir before provisioning, and verify metadata written to .atmos/metadata.json. Workdir tests updated with temp-based paths and real FileSystem for metadata operations.
Configuration Schema
pkg/schema/schema.go
Added Provision field to AtmosSettings with nested ProvisionSettings and ProvisionWorkdirSettings types containing Enabled (bool) and TTL (string) fields for provisioning defaults.
Integration Tests & Documentation
tests/cli_jit_source_workdir_test.go, website/blog/2025-01-22-version-aware-jit-provisioning.mdx
New tests verify JIT provisioning with workdir when local component exists; validates remote source content in workdir and source precedence behavior. Blog post documents version-aware re-provisioning, URI change detection, TTL-based cleanup, and enhanced metadata fields with usage examples.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

minor

Suggested reviewers

  • aknysh
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: version-aware JIT source provisioning and TTL-based cleanup are the core features introduced across 22 modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch osterman/jit-vendor-force

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
pkg/provisioner/workdir/workdir_test.go (1)

414-458: Make the metadata write failure deterministic across OSes.

Relying on chmod’d read-only dirs isn’t reliable on Windows, so this test can pass unexpectedly. Consider forcing a cross-platform error by creating a file at the .atmos path so MkdirAll fails consistently.

🔧 Suggested change
-	// Create .atmos directory as read-only to prevent writing metadata.
-	atmosDir := filepath.Join(workdirPath, AtmosDir)
-	err = os.MkdirAll(atmosDir, 0o555)
-	require.NoError(t, err)
-	t.Cleanup(func() {
-		// Restore permissions so cleanup can delete the directory.
-		_ = os.Chmod(atmosDir, 0o755)
-	})
+	// Create a file at .atmos to force WriteMetadata to fail cross-platform.
+	atmosDir := filepath.Join(workdirPath, AtmosDir)
+	err = os.WriteFile(atmosDir, []byte("not-a-dir"), 0o444)
+	require.NoError(t, err)

As per coding guidelines, keep tests cross-platform.

🤖 Fix all issues with AI agents
In `@pkg/duration/duration.go`:
- Around line 139-146: ParseDuration currently converts seconds -> time.Duration
by multiplying seconds * time.Second without checking for overflow; before
multiplying, validate the parsed seconds (from Parse) against time.Duration
bounds (int64 nanoseconds) so the result won't overflow. In ParseDuration,
compute the safe max/min seconds as int64(math.MaxInt64)/int64(time.Second) and
int64(math.MinInt64)/int64(time.Second) (or use float64 equivalents if Parse
returns float64), check that seconds lies within those bounds, and return a
clear error if not; then perform the multiplication and return the duration.
Reference functions: ParseDuration and Parse.

In `@pkg/provisioner/source/provision_hook.go`:
- Around line 276-283: In checkMetadataChanges, remove the `sourceSpec.Version
!= ""` guard so the code directly compares sourceSpec.Version to
metadata.SourceVersion; this ensures removing a pinned version (emptying
Version) is detected as a change and triggers re-provisioning—update the
comparison in the function (checkMetadataChanges, the Version change branch) to
always compare the two values and return the change message when they differ.
- Around line 79-98: needsProvisioning currently treats missing metadata as a
signal to re-provision, which forces non-workdir component-path targets to
re-vendor on every run; change needsProvisioning to accept the isWorkdir flag
(e.g., needsProvisioning(targetDir, sourceSpec, isWorkdir)) and update its logic
so that missing metadata only triggers re-provision when isWorkdir is true,
keeping existing checks for version/URI changes otherwise; update the call in
provision_hook.go (the current call around determineSourceTargetDirectory and
the other occurrence at the region noted 239-258) to pass isWorkdir, and ensure
any tests or callers of needsProvisioning are adjusted accordingly.

In `@website/blog/2025-01-22-version-aware-jit-provisioning.mdx`:
- Around line 93-95: The blog filename and embedded sample timestamps are off by
one year; rename the file "2025-01-22-version-aware-jit-provisioning.mdx" to use
2026-01-22 and update the Created, Updated, and Last Accessed timestamp values
in the front-matter/block (the lines containing "Created", "Updated", and "Last
Accessed") to 2026-01-22 (and adjust times if needed) so the filename and the
sample dates are consistent with the 2026-01-22 release.
🧹 Nitpick comments (7)
pkg/duration/duration_test.go (1)

55-61: Assert the sentinel error for invalid inputs.

These tests can be tighter by asserting ErrInvalidDuration via ErrorIs, not just any error. As per coding guidelines, use errors.Is() semantics in tests.

✅ Suggested test tightening
 import (
 	"testing"
 	"time"

+	errUtils "github.com/cloudposse/atmos/errors"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@
 		t.Run(tt.name, func(t *testing.T) {
 			result, err := Parse(tt.input)
 			if tt.wantErr {
 				assert.Error(t, err, "expected error for input %q", tt.input)
+				assert.ErrorIs(t, err, errUtils.ErrInvalidDuration)
 			} else {
 				require.NoError(t, err, "unexpected error for input %q", tt.input)
 				assert.Equal(t, tt.expected, result, "unexpected result for input %q", tt.input)
 			}
 		})
@@
 		t.Run(tt.name, func(t *testing.T) {
 			result, err := ParseDuration(tt.input)
 			if tt.wantErr {
 				assert.Error(t, err, "expected error for input %q", tt.input)
+				assert.ErrorIs(t, err, errUtils.ErrInvalidDuration)
 			} else {
 				require.NoError(t, err, "unexpected error for input %q", tt.input)
 				assert.Equal(t, tt.expected, result, "unexpected result for input %q", tt.input)
 			}
 		})

Also applies to: 86-91

pkg/provisioner/workdir/fs.go (1)

138-167: Verify empty directory cleanup.

After deleting files, empty directories in dst (except AtmosDir) may remain. This could accumulate over time if source directories are removed.

♻️ Optional: Clean up empty directories after file deletion
 func deleteRemovedFiles(dst string, srcFiles map[string]bool) (bool, error) {
 	anyDeleted := false
+	dirsToCheck := []string{}
 
 	err := filepath.WalkDir(dst, func(path string, d fs.DirEntry, err error) error {
 		// ... existing logic ...
 
 		if d.IsDir() || srcFiles[relPath] {
+			if d.IsDir() && relPath != "." && relPath != AtmosDir {
+				dirsToCheck = append(dirsToCheck, path)
+			}
 			return nil
 		}
 
 		anyDeleted = true
 		return os.Remove(path)
 	})
+
+	// Remove empty directories (in reverse order for deepest-first)
+	for i := len(dirsToCheck) - 1; i >= 0; i-- {
+		_ = os.Remove(dirsToCheck[i]) // Silently fails if not empty
+	}
 
 	return anyDeleted, err
 }
tests/cli_jit_source_workdir_test.go (1)

26-92: Test design is solid and effectively validates the source precedence fix.

The marker file approach clearly distinguishes local vs remote content, and the assertions properly verify provisioning succeeded. The fixture exists and the test setup is clean.

The error from cmd.Execute() is safe to ignore—if provisioning fails, the require.NoError(t, err, ...) on os.Stat(workdirPath) catches it. That said, logging the error would help with debugging when things go wrong:

♻️ Capture error from Execute for better debugging
 	// Execute - this may error due to terraform not being configured,
 	// but the provisioning should have been attempted.
-	_ = cmd.Execute()
+	err := cmd.Execute()
+	if err != nil {
+		t.Logf("Execute returned error (expected for incomplete terraform setup): %v", err)
+	}
pkg/provisioner/workdir/metadata_lock_windows.go (1)

31-47: Inconsistent error handling compared to Unix implementation.

The Unix version wraps errors with errUtils.Build(errUtils.ErrWorkdirMetadata) for both read and unmarshal failures, but this Windows version returns raw errors. Consider aligning for consistent error types across platforms.

♻️ Suggested alignment with Unix error handling
+import (
+	"encoding/json"
+	"os"
+	"time"
+
+	errUtils "github.com/cloudposse/atmos/errors"
+)

 func loadMetadataWithReadLockWindows(metadataFile string) (*WorkdirMetadata, error) {
 	// On Windows, skip read locks entirely to avoid timeout issues.
 	data, err := os.ReadFile(metadataFile)
 	if err != nil {
 		if os.IsNotExist(err) {
 			return nil, nil
 		}
-		return nil, err
+		return nil, errUtils.Build(errUtils.ErrWorkdirMetadata).
+			WithCause(err).
+			WithExplanation("Failed to read metadata file").
+			WithContext("path", metadataFile).
+			Err()
 	}

 	var metadata WorkdirMetadata
 	if err := json.Unmarshal(data, &metadata); err != nil {
-		return nil, err
+		return nil, errUtils.Build(errUtils.ErrWorkdirMetadata).
+			WithCause(err).
+			WithExplanation("Failed to unmarshal metadata").
+			WithContext("path", metadataFile).
+			Err()
 	}

 	return &metadata, nil
 }
pkg/provisioner/workdir/metadata.go (1)

24-41: Minor TOCTOU race in ReadMetadata.

The os.Stat check at line 30 and subsequent read via loadMetadataWithReadLock has a small race window where the file could be deleted between checks. Since loadMetadataWithReadLock already handles os.IsNotExist, consider simplifying:

♻️ Remove redundant stat checks
 func ReadMetadata(workdirPath string) (*WorkdirMetadata, error) {
 	defer perf.Track(nil, "workdir.ReadMetadata")()
 
 	metadataPath := MetadataPath(workdirPath)
 
-	// Check if metadata file exists.
-	if _, err := os.Stat(metadataPath); os.IsNotExist(err) {
-		// Try legacy location.
-		legacyPath := filepath.Join(workdirPath, WorkdirMetadataFile)
-		if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
-			return nil, nil
-		}
-		metadataPath = legacyPath
-	}
-
-	// Use read lock for concurrent safety.
-	return loadMetadataWithReadLock(metadataPath)
+	// Try new location first.
+	metadata, err := loadMetadataWithReadLock(metadataPath)
+	if err != nil {
+		return nil, err
+	}
+	if metadata != nil {
+		return metadata, nil
+	}
+
+	// Try legacy location.
+	legacyPath := filepath.Join(workdirPath, WorkdirMetadataFile)
+	return loadMetadataWithReadLock(legacyPath)
 }
pkg/provisioner/workdir/metadata_lock_unix.go (1)

57-61: Variable shadowing in defer.

The err in the defer shadows the outer err variable. While it works here since the outer err isn't used after, it's a subtle gotcha.

♻️ Rename to avoid shadowing
 	defer func() {
-		if err := lock.Unlock(); err != nil {
-			log.Trace("Failed to unlock metadata file", "error", err, "path", lockPath)
+		if unlockErr := lock.Unlock(); unlockErr != nil {
+			log.Trace("Failed to unlock metadata file", "error", unlockErr, "path", lockPath)
 		}
 	}()
cmd/terraform/workdir/workdir_helpers.go (1)

151-160: Consider distinguishing metadata error vs not found.

When ReadMetadata returns (nil, nil), the current code wraps with WithCause(nil). This works but loses the distinction between "no metadata file" and "error reading metadata". A minor clarity improvement:

♻️ Handle nil metadata explicitly
 	metadata, err := provWorkdir.ReadMetadata(workdirPath)
-	if err != nil || metadata == nil {
+	if err != nil {
+		return nil, errUtils.Build(errUtils.ErrWorkdirMetadata).
+			WithCause(err).
+			WithExplanation(fmt.Sprintf("Failed to read workdir metadata for component '%s' in stack '%s'", component, stack)).
+			WithContext("component", component).
+			WithContext("stack", stack).
+			Err()
+	}
+	if metadata == nil {
 		return nil, errUtils.Build(errUtils.ErrWorkdirMetadata).
-			WithCause(err).
 			WithExplanation(fmt.Sprintf("Workdir not found for component '%s' in stack '%s'", component, stack)).
 			WithHint("Run 'atmos terraform init' to create the workdir").
 			WithContext("component", component).
 			WithContext("stack", stack).
 			Err()
 	}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@cmd/terraform/workdir/workdir_helpers.go`:
- Around line 118-123: The loop silently drops all errors from
provWorkdir.ReadMetadata(workdirPath); change it to return a wrapped error when
err != nil (including context like the workdirPath) while keeping the current
behavior of skipping when metadata == nil; locate the call to
provWorkdir.ReadMetadata and replace the silent continue-on-error with returning
fmt.Errorf("read metadata for %s: %w", workdirPath, err) (or errors.Wrap) so
failures (permission/corruption) are surfaced but still continue to skip on
metadata == nil.

In `@pkg/provisioner/source/provision_hook.go`:
- Around line 239-246: The block comment above the needsProvisioning function
has bullet lines without trailing periods which fails godot linting; update the
comment text for needsProvisioning so each bullet ends with a period (e.g.,
change lines like "- Directory doesn't exist or is empty" to "- Directory
doesn't exist or is empty.") and ensure the final summary line "Returns (false,
"") if the existing workdir is up-to-date." also ends with a period to satisfy
comment lint rules.
- Around line 276-283: checkMetadataChanges currently ignores a removed version
because it only flags changes when sourceSpec.Version != "" and differs; update
the logic in checkMetadataChanges to detect any difference between
metadata.SourceVersion and sourceSpec.Version (including when sourceSpec.Version
is empty) and return true with a clear message (e.g., "Source version removed (X
→ default/latest)" or "Source version changed (X → Y)") so that removing the
version triggers a refresh; modify the branch that references sourceSpec.Version
and metadata.SourceVersion accordingly and keep the existing URI comparison for
sourceSpec.Uri vs metadata.SourceURI.
- Around line 86-98: The current logic treats any missing metadata as "needs
provisioning" causing non-workdir components to be re-vendored every run; change
the branch after needsProvisioning(targetDir, sourceSpec) so that if needs==true
and isWorkdir==false you first check for existing installed artifacts/metadata
(e.g., presence of the targetDir or a provisioning marker) and skip provisioning
if they already exist, returning nil; keep the existing
workdir.UpdateLastAccessed behavior for isWorkdir paths. Update the code around
needsProvisioning, isWorkdir, targetDir and componentConfig (and use a helper
like hasProvisionMetadata or a simple os.Stat(targetDir)/marker-file check) to
preserve the original "skip if exists" contract for non-workdir components.
- Around line 263-272: Update isNonEmptyDir to treat a directory containing only
the metadata folder ".atmos" as empty: after os.ReadDir in isNonEmptyDir,
iterate the entries and ignore any entry with Name() == ".atmos" (and continue
for non-nil errors); return true only if there exists at least one entry whose
name is not ".atmos" and false otherwise. Keep the initial os.Stat checks and
error handling unchanged and ensure the function still returns false on ReadDir
error.

In `@pkg/provisioner/workdir/fs.go`:
- Around line 96-117: syncSourceToDest's WalkDir currently processes the .atmos
directory and can overwrite destination metadata; update the WalkDir callback
(the function that computes relPath and uses srcFiles, fileNeedsCopy, copyFile)
to skip any entry whose relPath is ".atmos" or starts with ".atmos/" (or the OS
path separator variant) by returning fs.SkipDir for directories and nil for
files so the entire .atmos tree is ignored during the source walk.

In `@website/blog/2025-01-22-version-aware-jit-provisioning.mdx`:
- Around line 93-95: Update the example timestamps in the frontmatter block:
change the "Created", "Updated", and "Last Accessed" entries from 2025 to 2026
(e.g., set them to 2026-01-20 and 2026-01-22 or align them with the intended
publish date 2026-01-22) so the sample dates in the "Created/Updated/Last
Accessed" lines reflect the 2026 release; edit the literal lines containing
"Created", "Updated", and "Last Accessed" accordingly.
🧹 Nitpick comments (5)
pkg/provisioner/workdir/metadata_lock_windows.go (2)

22-25: Fixed delay adds latency regardless of need.

The 50ms sleep runs after every operation. For bulk workdir operations, this accumulates. Consider making this configurable or only applying the delay when the function actually performed writes.

♻️ Potential optimization
-func withMetadataFileLockWindows(metadataFile string, fn func() error) error {
-	// No file locking on Windows to avoid timeout issues.
-	// The metadata is non-critical functionality, so we can operate
-	// without strict locking on Windows.
-
-	// Add a small delay after operations to let Windows release file handles.
-	defer func() {
-		time.Sleep(50 * time.Millisecond)
-	}()
-
-	// Just execute the function without any locking.
-	return fn()
+func withMetadataFileLockWindows(_ string, fn func() error) error {
+	// No file locking on Windows to avoid timeout issues.
+	// The metadata is non-critical functionality, so we can operate
+	// without strict locking on Windows.
+	return fn()
 }

41-44: Consider wrapping unmarshal errors with context.

Per coding guidelines, errors should be wrapped with context using fmt.Errorf or the error builder. The raw unmarshal error lacks information about which file failed.

♻️ Add error context
 	var metadata WorkdirMetadata
 	if err := json.Unmarshal(data, &metadata); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("failed to unmarshal metadata from %s: %w", metadataFile, err)
 	}
pkg/duration/duration.go (1)

27-33: Consider adding 'w' for weeks suffix.

The keywordDurations map includes "weekly", but unitMultipliers doesn't support 'w'. Users might expect "1w" to work alongside "7d".

♻️ Add week suffix support
 var unitMultipliers = map[byte]int64{
 	's': 1,
 	'm': secondsPerMinute,
 	'h': secondsPerHour,
 	'd': secondsPerDay,
+	'w': secondsPerWeek,
 }
tests/cli_jit_source_workdir_test.go (2)

61-63: Consider capturing execution errors for debugging.

The return value is intentionally ignored since terraform may error without full configuration. However, logging or storing the error could help debug test failures.

♻️ Optional improvement
 	// Execute - this may error due to terraform not being configured,
 	// but the provisioning should have been attempted.
-	_ = cmd.Execute()
+	err := cmd.Execute()
+	t.Logf("cmd.Execute() returned: %v", err) // Helps debug test failures

113-125: Test coverage could be expanded.

This test verifies terraform source describe succeeds but doesn't assert on the output. Consider adding assertions on the describe output to verify source configuration is correctly recognized, or documenting why this is sufficient as a smoke test.

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 82.03957% with 118 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.02%. Comparing base (2779cf9) to head (a329aea).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/provisioner/workdir/metadata.go 61.03% 24 Missing and 6 partials ⚠️
pkg/provisioner/workdir/clean.go 81.30% 14 Missing and 6 partials ⚠️
pkg/provisioner/workdir/fs.go 72.97% 10 Missing and 10 partials ⚠️
pkg/provisioner/workdir/metadata_lock_unix.go 72.13% 12 Missing and 5 partials ⚠️
internal/exec/terraform.go 36.84% 9 Missing and 3 partials ⚠️
pkg/provisioner/source/provision_hook.go 91.25% 4 Missing and 3 partials ⚠️
internal/exec/packer_output.go 58.33% 2 Missing and 3 partials ⚠️
pkg/provisioner/workdir/workdir.go 94.64% 2 Missing and 1 partial ⚠️
cmd/terraform/workdir/workdir_clean.go 95.12% 2 Missing ⚠️
internal/exec/helmfile_generate_varfile.go 60.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2010      +/-   ##
==========================================
+ Coverage   75.73%   76.02%   +0.28%     
==========================================
  Files         793      797       +4     
  Lines       74060    74558     +498     
==========================================
+ Hits        56089    56679     +590     
+ Misses      14476    14334     -142     
- Partials     3495     3545      +50     
Flag Coverage Δ
unittests 76.02% <82.03%> (+0.28%) ⬆️

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

Files with missing lines Coverage Δ
cmd/terraform/workdir/workdir_helpers.go 96.12% <100.00%> (+0.06%) ⬆️
cmd/terraform/workdir/workdir_list.go 62.10% <100.00%> (+6.00%) ⬆️
cmd/terraform/workdir/workdir_show.go 64.86% <100.00%> (+7.48%) ⬆️
pkg/config/cache.go 67.56% <100.00%> (-10.42%) ⬇️
pkg/duration/duration.go 100.00% <100.00%> (ø)
pkg/provisioner/workdir/types.go 100.00% <100.00%> (ø)
pkg/schema/schema.go 88.98% <ø> (ø)
cmd/terraform/workdir/workdir_clean.go 79.01% <95.12%> (+42.64%) ⬆️
internal/exec/helmfile_generate_varfile.go 51.06% <60.00%> (+51.06%) ⬆️
pkg/provisioner/workdir/workdir.go 95.67% <94.64%> (+3.78%) ⬆️
... and 7 more

... and 29 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@pkg/duration/duration.go`:
- Around line 108-126: The multiplication valInt * multiplier can overflow int64
and produce invalid negative durations; update the code in the parsing path
(where valInt and multiplier are used—e.g., the block that currently returns
valInt * multiplier) to perform a bounds check before multiplying: check whether
abs(valInt) > math.MaxInt64/abs(multiplier) (or use safe compare with
math.MaxInt64 and math.MinInt64) and if it would overflow return
errUtils.Build(errUtils.ErrInvalidDuration).WithExplanation("duration
overflow").WithContext("value", valInt).WithContext("multiplier",
multiplier).Err(); otherwise perform the multiplication and return the result.
Ensure you reference the existing unitMultipliers, valInt, multiplier, and
ErrInvalidDuration symbols when implementing the check.

In `@pkg/provisioner/source/provision_hook.go`:
- Around line 110-115: The current block always calls writeRemoteMetadata when
isWorkdir, which forces SourceTypeRemote and clears ContentHash; update the
logic to check sourceSpec.Type and call the appropriate metadata writer (e.g.,
call writeLocalMetadata for local sources or keep writeRemoteMetadata for remote
sources) or alter writeRemoteMetadata to accept the SourceType and preserve
local fields when sourceSpec.Type indicates a local source; locate the isWorkdir
branch in provisionHook (provision_hook.go) and branch on sourceSpec.Type (or
pass sourceSpec.Type into writeRemoteMetadata/writeLocalMetadata) so local
workdir targets retain their local-specific metadata and content hash.

In `@pkg/provisioner/workdir/fs.go`:
- Around line 130-143: fileNeedsCopy currently only compares content hashes and
returns false when hashes match, which misses mode-only changes; update
fileNeedsCopy to stat both srcPath and dstPath (using os.Stat) and compare their
permission bits (e.g., FileMode.Perm()) in addition to the hasher.HashFile
result, treating any stat error on src or dst as needing a copy, and return true
if either hashes differ or permission bits differ so permission-only changes
(which copyFile preserves via os.Chmod) are synced.
🧹 Nitpick comments (1)
pkg/provisioner/source/provision_hook_test.go (1)

191-331: Add coverage for the non-workdir branch in needsProvisioning.

TestNeedsProvisioning always passes isWorkdir=true, so the new isWorkdir=false path isn’t exercised. Consider adding a small case to lock the “existing non-empty dir → no provisioning” behavior. As per coding guidelines, new features should include comprehensive unit tests.

♻️ Suggested test addition
@@
 func TestNeedsProvisioning(t *testing.T) {
@@
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			path := tt.setup(t)
 			// Test with isWorkdir=true since these tests verify metadata behavior.
 			result, reason := needsProvisioning(path, tt.sourceSpec, true)
 			assert.Equal(t, tt.expected, result)
 			assert.Equal(t, tt.expectedReason, reason)
 		})
 	}
+
+	t.Run("non-workdir with existing files", func(t *testing.T) {
+		tempDir := t.TempDir()
+		dirPath := filepath.Join(tempDir, "component")
+		err := os.MkdirAll(dirPath, 0o755)
+		require.NoError(t, err)
+		err = os.WriteFile(filepath.Join(dirPath, "main.tf"), []byte("# test"), 0o644)
+		require.NoError(t, err)
+
+		result, reason := needsProvisioning(dirPath, sourceSpec, false)
+		assert.False(t, result)
+		assert.Empty(t, reason)
+	})
 }

@mergify
Copy link

mergify bot commented Jan 23, 2026

💥 This pull request now has conflicts. Could you fix it @osterman? 🙏

@mergify mergify bot added the conflict This PR has conflicts label Jan 23, 2026
@osterman osterman force-pushed the osterman/jit-vendor-force branch from a994750 to 7b06069 Compare January 23, 2026 16:12
@github-actions
Copy link

Warning

Release Documentation Required

This PR is labeled minor or major and requires documentation updates:

  • Changelog entry
  • Roadmap update - Update website/src/data/roadmap.js with the new milestone

Alternatively: If this change doesn't require release documentation, remove the minor or major label.

@mergify mergify bot removed the conflict This PR has conflicts label Jan 23, 2026
@github-actions
Copy link

Warning

Release Documentation Required

This PR is labeled minor or major and requires documentation updates:

  • Changelog entry
  • Roadmap update - Update website/src/data/roadmap.js with the new milestone

Alternatively: If this change doesn't require release documentation, remove the minor or major label.

@github-actions
Copy link

Warning

Release Documentation Required

This PR is labeled minor or major and requires documentation updates:

  • Changelog entry
  • Roadmap update - Update website/src/data/roadmap.js with the new milestone

Alternatively: If this change doesn't require release documentation, remove the minor or major label.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/terraform/workdir/workdir_list.go`:
- Around line 101-123: The code currently formats lastAccessed (computed from
workdirs[i].LastAccessed or CreatedAt) and will render "0001-01-01..." when both
times are zero; change the logic in the block that builds rows (around the
lastAccessed variable and the rows = append(...) that uses lastAccessed.Format)
to detect IsZero() after falling back and instead set a placeholder string
(e.g., "-") for the timestamp; introduce a lastAccessedStr used in the rows
slice instead of calling lastAccessed.Format directly, and apply the same change
to the other occurrence that formats timestamps (the second formatting location
referenced in the comment).

In `@pkg/provisioner/source/provision_hook.go`:
- Around line 314-345: The isLocalSource function misclassifies Windows absolute
paths; update it to use filepath.IsAbs(uri) (from the path/filepath package) for
OS-aware absolute-path detection instead of only checking for "/" and "."
prefixes, while keeping the existing file:// check and remoteIndicators logic;
ensure you add the filepath import and run uri through filepath.IsAbs early in
isLocalSource (before remoteIndicators) so Windows paths like C:\... or \... are
correctly treated as local.
🧹 Nitpick comments (1)
pkg/provisioner/workdir/clean_test.go (1)

286-413: Consider table-driven consolidation for expired-clean scenarios.

These cases share setup patterns and assertions; a table-driven structure would reduce duplication and ease expansion. As per coding guidelines, prefer table-driven tests when exercising multiple scenarios.

@mergify
Copy link

mergify bot commented Jan 23, 2026

💥 This pull request now has conflicts. Could you fix it @osterman? 🙏

@mergify mergify bot added the conflict This PR has conflicts label Jan 23, 2026
@github-actions
Copy link

Warning

Release Documentation Required

This PR is labeled minor or major and requires documentation updates:

  • Changelog entry
  • Roadmap update - Update website/src/data/roadmap.js with the new milestone

Alternatively: If this change doesn't require release documentation, remove the minor or major label.

- Add tests for workdir clean command edge cases
- Add tests for workdir show command scenarios
- Add duration parsing tests for TTL validation
- Add filesystem tests for workdir operations
- Add metadata lock tests for Unix file locking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@cmd/terraform/workdir/workdir_clean_cmd_test.go`:
- Around line 465-535: The tests currently call resetViperForTest to mutate
viper state; instead initialize a fresh command test context by calling
cmd.NewTestKit(t) at the start of each test (e.g., in
TestCleanCmd_RunE_ExpiredWithoutTTL, TestCleanCmd_RunE_ExpiredWithAll,
TestCleanCmd_RunE_ExpiredWithComponent,
TestCleanCmd_RunE_ActualCall_AllWithComponent,
TestCleanCmd_RunE_ActualCall_NoFlagsNoArgs,
TestCleanCmd_RunE_ActualCall_ComponentWithoutStack) to isolate RootCmd/viper
state; remove or stop using resetViperForTest and ensure each test starts with
cmd.NewTestKit(t) before setting any viper flags and invoking cleanCmd.RunE so
the command state is reset between tests.

In `@pkg/provisioner/workdir/metadata_lock_windows.go`:
- Around line 31-47: The function loadMetadataWithReadLockWindows should wrap
returned errors with errUtils.ErrWorkdirMetadata: when os.ReadFile(metadataFile)
returns a non-NotExist error, return nil and wrap the error (e.g.,
fmt.Errorf("%w: %v", errUtils.ErrWorkdirMetadata, err)); likewise wrap the
json.Unmarshal error before returning; add imports for fmt and errUtils. Ensure
the special-case os.IsNotExist still returns (nil, nil).

In `@pkg/provisioner/workdir/workdir.go`:
- Around line 219-233: The error hint is hardcoded to "components/terraform"
which can be incorrect; update the error construction around
extractComponentPath(...) and the subsequent existence check to use the resolved
componentPath variable (or derive the actual components base from
atmosConfig/Components.Terraform.BasePath if preferred) instead of the literal
string: change the WithHint call to include componentPath (or a message
referencing it) so the hint accurately points to the real path when
s.fs.Exists(componentPath) fails; keep the existing WithContext("path",
componentPath) and errUtils.Build usage.
♻️ Duplicate comments (2)
internal/exec/packer_output.go (1)

64-66: Wrap auto-provision error with sentinel (duplicate).

This was flagged in a previous review. Line 65 should join with errUtils.ErrProvisionSource or similar sentinel before adding context to support errors.Is checks.

cmd/terraform/workdir/workdir_helpers.go (1)

118-123: Don’t silently drop metadata read errors in ListWorkdirs.
Right now err != nil is treated the same as “no metadata,” which can mask corruption or permission problems. Consider returning a wrapped error on err != nil but still skipping when metadata == nil.

🛠️ Suggested tweak
-		metadata, err := provWorkdir.ReadMetadata(workdirPath)
-		if err != nil || metadata == nil {
-			// Skip directories with invalid or missing metadata.
-			// This allows the list operation to succeed even if some workdirs are corrupt.
-			continue
-		}
+		metadata, err := provWorkdir.ReadMetadata(workdirPath)
+		if err != nil {
+			return nil, errUtils.Build(errUtils.ErrWorkdirMetadata).
+				WithCause(err).
+				WithExplanation("Failed to read workdir metadata").
+				WithContext("path", workdirPath).
+				Err()
+		}
+		if metadata == nil {
+			// Skip directories without metadata.
+			continue
+		}

As per coding guidelines, errors should be wrapped with context.

🧹 Nitpick comments (9)
examples/source-provisioning/README.md (1)

27-31: Clarify the version‑pinning claim.
The bullet says remote URIs are version‑pinned, but the example doesn’t show a version. Consider softening the wording or adding a version in the example.

Proposed tweak
-2. **Remote URIs** - GitHub URLs with version pinning
+2. **Remote URIs** - GitHub URLs (optionally version‑pinned)
pkg/provisioner/workdir/fs_test.go (1)

14-71: Consider table-driven cases for fileNeedsCopy.
These scenarios share setup and assertions; a table-driven test would trim repetition and scale better. As per coding guidelines, prefer table-driven tests for multi-scenario coverage.

pkg/provisioner/workdir/clean_test.go (1)

286-413: Consider table-driven tests for the TTL cleanup scenarios.
These cases are parallel and would be easier to extend as a single table. As per coding guidelines, prefer table-driven tests for multi-scenario coverage.

cmd/terraform/workdir/workdir_show_test.go (1)

470-484: Consider asserting SourceType defaults correctly.

This test verifies zero LastAccessed isn't displayed, but doesn't set SourceType. If printShowHuman has conditional logic based on SourceType, you may want to explicitly test local vs unset SourceType behavior separately. Not blocking, just a thought for completeness.

pkg/provisioner/workdir/metadata_lock_unix_test.go (1)

48-75: Potential test flakiness with exclusive locks.

The assertion at line 74 requires at least one goroutine to succeed. If the locking implementation uses non-blocking LOCK_NB and the timing is unfortunate, all 5 goroutines could theoretically fail. Consider either:

  • Removing the assertion entirely (the test still verifies no crashes/deadlocks)
  • Running goroutines sequentially with a smaller subset to guarantee success

Not blocking since the probability is low, but worth noting.

cmd/terraform/workdir/workdir_show.go (1)

75-86: Consider using SourceTypeRemote constant instead of magic string.

Line 90 compares sourceType == "remote" directly. The workdir package defines SourceTypeRemote constant which would be more maintainable.

♻️ Suggested improvement
+import provWorkdir "github.com/cloudposse/atmos/pkg/provisioner/workdir"
+
 // In printShowHuman:
-if sourceType == "remote" {
+if sourceType == string(provWorkdir.SourceTypeRemote) {
cmd/terraform/workdir/workdir_clean_cmd_test.go (1)

475-535: Consider a table‑driven structure for RunE validation cases.
The validation tests are very similar; a small table of flag/args permutations would cut repetition and make coverage easier to extend.

As per coding guidelines, prefer table-driven tests for multiple scenarios.

tests/cli_jit_source_workdir_test.go (2)

92-123: Placeholder test provides limited coverage.

Lines 121-122 explicitly call this a "placeholder." The test only confirms source describe doesn't error - it doesn't verify source precedence behavior.

Consider either:

  • Adding meaningful assertions (e.g., workdir state, output verification)
  • Removing this test since the first test already covers the behavior

294-334: Inconsistent assertions compared to other tests.

TestJITSource_PackerOutput and TestJITSource_HelmfileGenerateVarfile lack the workdir existence and content verification that TestJITSource_GenerateVarfile and TestJITSource_GenerateBackend have.

For consistency, add workdir assertions:

♻️ Suggested additions

For TestJITSource_PackerOutput (after line 315):

+	// Verify workdir was provisioned by JIT source.
+	workdirPath := filepath.Join(".workdir", "packer", "dev-ami-workdir")
+	info, statErr := os.Stat(workdirPath)
+	require.NoError(t, statErr, "Workdir should exist at %s (JIT provisioning should have run)", workdirPath)
+	require.True(t, info.IsDir(), "Workdir path should be a directory")

For TestJITSource_HelmfileGenerateVarfile (after line 333):

+	// Verify workdir was provisioned.
+	workdirPath := filepath.Join(".workdir", "helmfile", "dev-nginx-workdir")
+	info, err := os.Stat(workdirPath)
+	require.NoError(t, err, "Workdir should exist at %s", workdirPath)
+	require.True(t, info.IsDir(), "Workdir path should be a directory")

osterman and others added 2 commits January 24, 2026 22:03
- Skip permission-based tests on Windows (Unix permissions not supported)
  - TestFileNeedsCopy_DifferentPermissions
  - TestCopyFile_PreservesPermissions
  - TestServiceProvision_WriteMetadataFails (read-only dirs work differently)
- Use actual componentPath in error hint instead of hardcoded path

Addresses CodeRabbit review feedback.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wrap auto-provision error with ErrSourceProvision sentinel (packer_output.go)
- Add error wrapping with ErrWorkdirMetadata in Windows metadata loader
- Document circular import limitation preventing cmd.NewTestKit usage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/provisioner/workdir/workdir_test.go`:
- Around line 416-418: The test currently checks os.Getenv("GOOS") which is
incorrect because GOOS is a compile-time value; update the condition to use
runtime.GOOS (e.g., if runtime.GOOS == "windows" || filepath.Separator == '\\')
and add the runtime import to the test file; ensure you remove the os.Getenv
usage and import "runtime" so the read-only directory skip logic in
workdir_test.go uses the correct platform value.
♻️ Duplicate comments (1)
cmd/terraform/workdir/workdir_clean_cmd_test.go (1)

465-476: Use cmd.NewTestKit(t) to isolate RootCmd/Viper state.

The helper bypasses the required testkit reset and can leak global state across cmd tests. Consider restructuring tests (e.g., external test package or shared helper that avoids the circular import) so cmd.NewTestKit(t) runs before touching Viper. As per coding guidelines, ...

🧹 Nitpick comments (4)
pkg/provisioner/workdir/fs_test.go (2)

3-10: Use runtime.GOOS for OS checks.

os.Getenv("GOOS") is non-idiomatic and may be unset; runtime.GOOS is the canonical, reliable check. Consider updating both Windows skips accordingly.

🔧 Suggested change
 import (
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"

 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@
-	if os.Getenv("GOOS") == "windows" || filepath.Separator == '\\' {
+	if runtime.GOOS == "windows" {
 		t.Skip("Skipping permission test on Windows - Unix file permissions not supported")
 	}
@@
-	if os.Getenv("GOOS") == "windows" || filepath.Separator == '\\' {
+	if runtime.GOOS == "windows" {
 		t.Skip("Skipping permission test on Windows - Unix file permissions not supported")
 	}

Also applies to: 49-52, 199-202


14-74: Consider table-driven tests to reduce repetition.

These scenarios are great, but the repeated setup makes the file longer than needed. A table-driven approach for fileNeedsCopy and copyFile would align with test guidelines and improve maintainability. As per coding guidelines, ...

Also applies to: 155-220

pkg/provisioner/workdir/workdir.go (1)

184-184: Silent discard of ReadMetadata error may hide real failures.

Ignoring the error is fine when metadata doesn't exist yet. However, this also silences permission errors or corrupt JSON. A warning log would help debugging without breaking the flow.

-	existingMetadata, _ := ReadMetadata(workdirPath)
+	existingMetadata, err := ReadMetadata(workdirPath)
+	if err != nil {
+		ui.Warning(fmt.Sprintf("Failed to read existing metadata (will recreate): %s", err))
+	}
cmd/terraform/workdir/workdir_clean_cmd_test.go (1)

478-538: Consolidate RunE validation tests into a table-driven test.

These cases are repetitive and easier to maintain as a single table-driven test. As per coding guidelines, ...

♻️ Suggested refactor
-func TestCleanCmd_RunE_ExpiredWithoutTTL(t *testing.T) {
-	resetViperForTest(t)
-	v := viper.GetViper()
-	v.Set("expired", true)
-	v.Set("ttl", "") // No TTL provided.
-
-	err := cleanCmd.RunE(cleanCmd, []string{})
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
-
-func TestCleanCmd_RunE_ExpiredWithAll(t *testing.T) {
-	resetViperForTest(t)
-	v := viper.GetViper()
-	v.Set("expired", true)
-	v.Set("ttl", "7d")
-	v.Set("all", true) // Cannot use --all with --expired.
-
-	err := cleanCmd.RunE(cleanCmd, []string{})
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
-
-func TestCleanCmd_RunE_ExpiredWithComponent(t *testing.T) {
-	resetViperForTest(t)
-	v := viper.GetViper()
-	v.Set("expired", true)
-	v.Set("ttl", "7d")
-
-	err := cleanCmd.RunE(cleanCmd, []string{"vpc"}) // Component arg with --expired.
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
-
-func TestCleanCmd_RunE_ActualCall_AllWithComponent(t *testing.T) {
-	resetViperForTest(t)
-	v := viper.GetViper()
-	v.Set("all", true)
-
-	err := cleanCmd.RunE(cleanCmd, []string{"vpc"}) // --all with component arg.
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
-
-func TestCleanCmd_RunE_ActualCall_NoFlagsNoArgs(t *testing.T) {
-	resetViperForTest(t)
-
-	err := cleanCmd.RunE(cleanCmd, []string{}) // No flags, no args.
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
-
-func TestCleanCmd_RunE_ActualCall_ComponentWithoutStack(t *testing.T) {
-	resetViperForTest(t)
-	v := viper.GetViper()
-	v.Set("stack", "") // No stack provided.
-
-	err := cleanCmd.RunE(cleanCmd, []string{"vpc"}) // Component without --stack.
-	assert.Error(t, err)
-	assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
-}
+func TestCleanCmd_RunE_Validation(t *testing.T) {
+	cases := []struct {
+		name string
+		setup func(v *viper.Viper)
+		args  []string
+	}{
+		{
+			name: "expired without ttl",
+			setup: func(v *viper.Viper) {
+				v.Set("expired", true)
+				v.Set("ttl", "")
+			},
+			args: []string{},
+		},
+		{
+			name: "expired with all",
+			setup: func(v *viper.Viper) {
+				v.Set("expired", true)
+				v.Set("ttl", "7d")
+				v.Set("all", true)
+			},
+			args: []string{},
+		},
+		{
+			name: "expired with component",
+			setup: func(v *viper.Viper) {
+				v.Set("expired", true)
+				v.Set("ttl", "7d")
+			},
+			args: []string{"vpc"},
+		},
+		{
+			name: "all with component",
+			setup: func(v *viper.Viper) {
+				v.Set("all", true)
+			},
+			args: []string{"vpc"},
+		},
+		{
+			name: "no flags no args",
+			setup: func(v *viper.Viper) {},
+			args: []string{},
+		},
+		{
+			name: "component without stack",
+			setup: func(v *viper.Viper) {
+				v.Set("stack", "")
+			},
+			args: []string{"vpc"},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			resetViperForTest(t)
+			v := viper.GetViper()
+			tc.setup(v)
+
+			err := cleanCmd.RunE(cleanCmd, tc.args)
+			assert.Error(t, err)
+			assert.ErrorIs(t, err, errUtils.ErrWorkdirClean)
+		})
+	}
+}

GOOS is a compile-time constant, not a runtime environment variable.
os.Getenv("GOOS") returns empty unless explicitly set.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
pkg/provisioner/workdir/workdir_test.go (2)

502-536: Use filepath.FromSlash for test paths to maintain cross-platform compatibility.

The test should align with other tests in this file, which consistently use filepath.FromSlash() for path values in componentConfig and mock expectations. Extract the hardcoded path as a variable to avoid repetition.

♻️ Proposed update
 	mockFS := NewMockFileSystem(ctrl)
 	mockHasher := NewMockHasher(ctrl)

+	componentPath := filepath.FromSlash("/custom/path/to/component")
+
 	mockFS.EXPECT().MkdirAll(workdirPath, gomock.Any()).Return(nil)
-	mockFS.EXPECT().Exists("/custom/path/to/component").Return(true)
-	mockFS.EXPECT().SyncDir("/custom/path/to/component", workdirPath, mockHasher).Return(true, nil)
+	mockFS.EXPECT().Exists(componentPath).Return(true)
+	mockFS.EXPECT().SyncDir(componentPath, workdirPath, mockHasher).Return(true, nil)
 	mockHasher.EXPECT().HashDir(workdirPath).Return("abc123", nil)
 	// Note: WriteMetadata uses real filesystem with atomic write, not mocked FileSystem.
 	
 	componentConfig := map[string]any{
 		"component":      "vpc",
 		"atmos_stack":    "dev",
-		"component_path": "/custom/path/to/component",
+		"component_path": componentPath,
 		"provision": map[string]any{
 			"workdir": map[string]any{
 				"enabled": true,

416-463: Guard read-only metadata test against root bypass.

Tests running as root may bypass write-permission restrictions, causing this test to fail unexpectedly. Skip the test when EUID is 0, matching the pattern already established in pkg/config/cache_test.go and cmd/working_directory_test.go.

🔧 Suggested hardening
 func TestServiceProvision_WriteMetadataFails(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		t.Skip("Skipping read-only directory test on Windows - directory permissions work differently")
 	}
+	if os.Geteuid() == 0 {
+		t.Skip("Skipping read-only directory test when running as root - permissions may be bypassed")
+	}
 	// Create a temp dir with a read-only .atmos subdirectory to force WriteMetadata to fail.
🧹 Nitpick comments (1)
pkg/provisioner/workdir/workdir_test.go (1)

1194-1278: Consider table-driven BuildLocalMetadata tests.

These three cases are structurally similar and could be consolidated to reduce duplication and make future additions easier.

As per coding guidelines, ...

The kubernetes.io domain frequently has connection failures/timeouts
in CI, causing spurious link check failures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 25, 2026
Instead of silently passing when main.tf doesn't exist, the tests now:
- Explicitly fail if main.tf exists (unexpected)
- Read and check for LOCAL_VERSION_MARKER to provide better diagnostics
- Use t.Fatalf to fail fast with clear error messages

Addresses CodeRabbit feedback about test assertion clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 25, 2026
aknysh and others added 2 commits February 5, 2026 12:44
Resolve conflicts in terraform_generate_varfile.go and
terraform_generate_backend.go by adopting main's cleaner
ensureTerraformComponentExists helper function approach.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive tests for:

- pkg/provisioner/workdir/metadata.go:
  - MetadataPath function
  - WriteMetadata with all fields populated
  - ReadMetadata new location priority over legacy
  - UpdateLastAccessed preserves all fields

- pkg/provisioner/workdir/clean.go:
  - checkWorkdirExpiry for expired/non-expired workdirs
  - getModTimeFromEntry
  - findExpiredWorkdirs with mixed workdirs
  - CleanExpiredWorkdirs with empty base path
  - Clean with Expired option precedence
  - formatDuration edge cases

- pkg/provisioner/workdir/fs.go:
  - DefaultPathFilter.Match with patterns
  - SyncDir with nested directories
  - SyncDir updating changed files

- pkg/provisioner/source/provision_hook.go:
  - checkMetadataChanges with version scenarios
  - isNonEmptyDir edge cases
  - needsProvisioning for non-workdir targets
  - writeWorkdirMetadata source type detection
  - writeWorkdirMetadata preserving ContentHash

Coverage improvements:
- workdir package: ~79% → 92.5%
- source package: ~76% → 83.6%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@aknysh aknysh merged commit 2a065e8 into main Feb 5, 2026
58 checks passed
@aknysh aknysh deleted the osterman/jit-vendor-force branch February 5, 2026 22:46
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

These changes were released in v1.206.0-rc.1.

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 size/xl Extra large size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

generate varfile and generate backend don't work with JIT vendoring

2 participants