fix: Restore PATH inheritance in workflow shell commands#1719
Conversation
Fixes workflow shell commands failing with "executable file not found in $PATH" when executing standard Unix commands like `env`, `ls`, `grep`, etc. ## Problem After commit 9fd7d15 (PR #1543), workflow shell commands lost access to the PATH environment variable, causing failures when executing any external executable: ``` "env": executable file not found in $PATH ``` The user reported this broke workflows that worked in v1.189.0 but failed in v1.195.0. ## Root Cause The bug occurred in ExecuteShell(): 1. Workflow commands call ExecuteShell with empty env: []string{} 2. ExecuteShell appends ATMOS_SHLVL: []string{"ATMOS_SHLVL=1"} 3. ShellRunner receives non-empty env, so doesn't fall back to os.Environ() 4. Shell runs with ONLY ATMOS_SHLVL set, losing PATH and all other vars ## Solution Check if env is empty and populate with os.Environ() BEFORE appending ATMOS_SHLVL. This ensures shell commands inherit the full parent process environment. ## Testing - Added unit tests in shell_utils_test.go demonstrating bug and fix - Added integration test for workflow shell commands with PATH - All existing workflow tests pass - Verified env, ls, grep commands now work in workflows Fixes #1718 Resolves DEV-3725 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
📝 WalkthroughWalkthroughExecuteShell now builds a merged environment starting from the current process env, applies provided KEY=value updates, appends ATMOS_SHLVL, and passes that merged environment to the shell runner; new unit and integration tests and a workflow fixture exercise PATH inheritance for shell commands. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller as ExecuteShell
participant OS as os.Environ()
participant Merge as merge logic
participant Shell as ShellRunner
Note over Caller,OS: Start ExecuteShell(envArg)
Caller->>OS: read current env (os.Environ)
Caller->>Merge: apply updates from envArg (parse KEY=VALUE)
Merge-->>Caller: mergedEnv (parent env + overrides)
Caller->>Merge: append ATMOS_SHLVL
Caller->>Shell: invoke ShellRunner with mergedEnv
Shell-->>Caller: command output / exit code
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Improves the fix to always merge custom environment variables with the parent process environment, rather than conditionally replacing it. This ensures: 1. Empty env (workflows): Inherits full parent environment including PATH 2. Custom env (commands): Merges custom vars with parent, overriding duplicates 3. Consistent behavior: All callers get PATH and system vars by default This matches the original behavior before commit 9fd7d15 where env was merged with append(os.Environ(), env...) rather than replaced. ## Changes - Always start with os.Environ() as the base - Merge custom env vars using UpdateEnvVar to override duplicates - Add helper functions parseEnvVarKey/parseEnvVarValue for parsing - Add test verifying custom vars override parent while preserving PATH Addresses code review feedback on PR #1719. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
d2c47f8 to
e431f70
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1719 +/- ##
==========================================
+ Coverage 68.57% 68.60% +0.02%
==========================================
Files 372 372
Lines 33439 33450 +11
==========================================
+ Hits 22932 22948 +16
+ Misses 8349 8346 -3
+ Partials 2158 2156 -2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
internal/exec/shell_utils.go (1)
142-156: Consider combining the helpers for efficiency.Both functions scan for the
=separator independently. Combining them into a singleparseEnvVar(envVar) (key, value)helper would avoid the duplicateIndexBytecall on line 128 where both are invoked on the same string.Apply this refactor if you'd like:
-// parseEnvVarKey extracts the key from an environment variable string (KEY=value). -func parseEnvVarKey(envVar string) string { - if idx := strings.IndexByte(envVar, '='); idx >= 0 { - return envVar[:idx] - } - return envVar -} - -// parseEnvVarValue extracts the value from an environment variable string (KEY=value). -func parseEnvVarValue(envVar string) string { - if idx := strings.IndexByte(envVar, '='); idx >= 0 { - return envVar[idx+1:] - } - return "" -} +// parseEnvVar extracts the key and value from an environment variable string (KEY=value). +func parseEnvVar(envVar string) (key, value string) { + if idx := strings.IndexByte(envVar, '='); idx >= 0 { + return envVar[:idx], envVar[idx+1:] + } + return envVar, "" +}Then update line 128:
- mergedEnv = u.UpdateEnvVar(mergedEnv, parseEnvVarKey(envVar), parseEnvVarValue(envVar)) + key, value := parseEnvVar(envVar) + mergedEnv = u.UpdateEnvVar(mergedEnv, key, value)internal/exec/shell_utils_test.go (2)
652-677: Update the comment to reflect the fix.The comment on lines 673-675 says "This currently fails" but should reference the past tense since the fix has been implemented and tests now pass.
Apply this diff:
- // This currently fails with "env": executable file not found in $PATH - // because ExecuteShell adds ATMOS_SHLVL to the empty slice, making it non-empty, - // and ShellRunner then uses ONLY that environment, losing PATH. + // Before the fix, this failed with "env": executable file not found in $PATH + // because ExecuteShell added ATMOS_SHLVL to the empty slice, making it non-empty, + // and ShellRunner then used ONLY that environment, losing PATH.
679-715: Update comment to reflect the fix is implemented.The comment on line 707 says "These currently fail due to the bug" but should use past tense since the fix has been applied and tests now pass.
Apply this diff:
if tc.requiresPATH { - // These currently fail due to the bug - PATH is not inherited. + // Before the fix, these failed due to the bug - PATH was not inherited. assert.NoError(t, err, "should be able to execute '%s' when PATH is inherited", tc.cmd)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
internal/exec/shell_utils.go(1 hunks)internal/exec/shell_utils_test.go(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*_test.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*_test.go: Every new feature must include comprehensive unit tests
Test both happy paths and error conditions
Use table-driven tests for multiple scenarios
**/*_test.go: Prefer unit tests with mocks over integration tests; use interfaces + DI; table-driven tests for coverage; target >80% coverage.
Tests must exercise production code paths; avoid duplicating implementation logic in tests.
Use t.Skipf("reason") with clear context when skipping tests; CLI tests may auto-build temp binaries.
Files:
internal/exec/shell_utils_test.go
**/*.go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
**/*.go: All code must pass golangci-lint checks
Follow Go error handling idioms and use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider custom error types for domain-specific errors
Follow standard Go coding style; run gofmt and goimports
Use snake_case for environment variables
Document complex logic with inline comments
**/*.go: Define interfaces for major functionality and use dependency injection to enable testability.
Generate mocks using go.uber.org/mock/mockgen via //go:generate directives (never manual mocks).
Prefer functional Options pattern instead of functions with many parameters for configuration.
Use context.Context only for cancellation, deadlines/timeouts, and request-scoped values; never for configuration or dependencies; context should be the first parameter.
All comments must end with periods (godot linter).
Preserve helpful existing comments; update them to match code when refactoring; only remove when incorrect, redundant, or code removed.
Organize imports in three groups (stdlib, third-party, Atmos) separated by blank lines and sorted alphabetically; maintain aliases cfg, log, u, errUtils.
Add defer perf.Track(atmosConfig, "pkg.FuncName")() (or nil if no atmosConfig) plus a blank line at the start of all public functions for performance tracking.
Use static errors from errors/errors.go; wrap with fmt.Errorf("%w") for chaining; use errors.Join for independent errors; avoid dynamic errors and string comparison; use errors.Is for checks.
Never use //revive:disable:file-length-limit to bypass file size limits; keep files small and focused (<600 lines).
Environment variables must be bound with viper.BindEnv using ATMOS_ prefix (e.g., viper.BindEnv("ATMOS_VAR", "ATMOS_VAR", "FALLBACK")).
Ensure cross-platform compatibility: use filepath.Join instead of hardcoded path separators and prefer SDKs over shelling out to binaries.
Files:
internal/exec/shell_utils_test.gointernal/exec/shell_utils.go
**/!(*_test).go
📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)
Document all exported functions, types, and methods with Go doc comments
Files:
internal/exec/shell_utils.go
🧬 Code graph analysis (2)
internal/exec/shell_utils_test.go (1)
internal/exec/shell_utils.go (1)
ExecuteShell(107-140)
internal/exec/shell_utils.go (2)
pkg/utils/env_utils.go (1)
UpdateEnvVar(133-156)pkg/utils/shell_utils.go (1)
ShellRunner(65-103)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Build (windows)
- GitHub Check: Build (linux)
- GitHub Check: autofix
- GitHub Check: Analyze (go)
- GitHub Check: Review Dependency Licenses
- GitHub Check: Summary
🔇 Additional comments (3)
internal/exec/shell_utils.go (2)
120-131: Solid fix for PATH inheritance.The implementation correctly addresses the regression by starting with
os.Environ()and merging custom env vars viaUpdateEnvVar. This ensures PATH and other system variables are preserved while custom vars can override as needed.
142-148: Helper function looks solid.The key extraction logic handles typical env var formats correctly, including edge cases like empty keys or values.
internal/exec/shell_utils_test.go (1)
629-650: Excellent test coverage for the override scenario.This test validates that custom env vars take precedence while PATH remains accessible. The combined assertion using
testandgrepefficiently verifies both behaviors.
|
These changes were released in v1.196.0-rc.3. |
|
These changes were released in v1.196.0-test.3. |
what
why
env,ls,grepnot foundexecuteCustomCommandbehaviorRoot Cause
The bug occurred in
ExecuteShell()function ininternal/exec/shell_utils.go:ExecuteShellwith empty env slice:[]string{}ExecuteShellappendsATMOS_SHLVLto the slice:[]string{"ATMOS_SHLVL=1"}ShellRunnerreceives a non-empty env, so it doesn't fall back toos.Environ()ATMOS_SHLVLset, losing PATH and all other environment variablesSolution
Refactored
ExecuteShell()to always merge custom env vars with parent environment:This ensures:
executeCustomCommandpattern (line 393 incmd_utils.go)Testing
Unit Tests (
internal/exec/shell_utils_test.go):TestExecuteShell/empty_env_should_inherit_PATH_from_parent_process- Verifiesenvcommand worksTestExecuteShell/empty_env_should_inherit_PATH_for_common_commands- Testsls,env,pwd,echoTestExecuteShell/custom_env_vars_override_parent_env- Verifies custom vars properly override parentIntegration Test (
tests/test-cases/workflows.yaml):atmos workflow shell command with PATH- Full end-to-end workflow test usingenv | grep PATHAll tests pass, including existing workflow tests.
references
Summary by CodeRabbit
Bug Fixes
Tests
Workflows / Snapshots