Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
62750d0
Changes auto-committed by Conductor
osterman Oct 18, 2025
bc9251c
Changes auto-committed by Conductor
osterman Oct 18, 2025
d4a5097
Changes auto-committed by Conductor
osterman Oct 18, 2025
8024caa
updates
aknysh Oct 19, 2025
a5d5b5e
updates
aknysh Oct 19, 2025
6d6cfd0
Changes auto-committed by Conductor
osterman Oct 19, 2025
585f4b8
updates
aknysh Oct 19, 2025
bbee0d8
Merge remote-tracking branch 'origin/fix-terraform-exit-code-2' into …
aknysh Oct 19, 2025
a39db54
updates
aknysh Oct 19, 2025
8a3ea12
updates
aknysh Oct 19, 2025
5b817ed
Fix duplicate exit code error messages using error wrapping pattern…
osterman Oct 19, 2025
d3308ea
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 19, 2025
a1593c9
Merge remote-tracking branch 'origin/fix-terraform-exit-code-2' into …
aknysh Oct 19, 2025
4ca9638
Fix duplicate exit code error messages by moving OsExit to errors pac…
osterman Oct 19, 2025
a2325cd
Fix duplicate error message in shell runner exit code handling
osterman Oct 19, 2025
f28b2c3
Remove temporary development files
osterman Oct 19, 2025
ba18b3d
Fix markdown linting issues in auth list blog post
osterman Oct 19, 2025
4385068
Fix duplicate error printing in workflow execution
osterman Oct 19, 2025
632e810
Remove auth-list blog post (covered by separate PR)
osterman Oct 19, 2025
0f0c691
Fix workspace creation to detect ExitCodeError instead of ExitError
osterman Oct 19, 2025
533f42b
Update circuit-breaker test snapshot to remove trailing whitespace
osterman Oct 19, 2025
b158765
Merge branch 'main' into fix-terraform-exit-code-2
aknysh Oct 19, 2025
d69e6ed
updates
aknysh Oct 19, 2025
dec7a87
updates
aknysh Oct 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
# Visit https://goreleaser.com for documentation on how to customize this behavior.

#before:
# hooks:
# - 'go mod tidy'
before:
hooks:
# Free disk space on GitHub Actions runners before building to prevent "no space left on device" errors.
# Building 14 platform binaries (linux, darwin, windows, freebsd × multiple architectures) requires
# significant temporary disk space for the Go linker.
#
# The default ubuntu-latest runner (currently ubuntu-24.04 or ubuntu-22.04) does come preloaded with many development tools that consume disk space:
#
# Category Approx. Size Notes
# System files ~20 GB Base Ubuntu OS + required dependencies
# Android SDK / NDK ~12 GB Installed under /usr/local/lib/android — large but unused for most workflows
# .NET SDK ~2–3 GB Installed under /usr/share/dotnet
# Haskell (GHC + Cabal) ~2 GB Installed under /opt/ghc
# CodeQL ~6 GB Installed under /opt/hostedtoolcache/CodeQL — used for GitHub's code scanning
# Other languages/tools ~15–20 GB Includes Node.js, Python, Ruby, Go, Java, etc.
# Free space at job start ~14 GB You typically get 13–15 GB of free disk on /
- |
sh -c 'if [ "$GITHUB_ACTIONS" = "true" ]; then
echo "Freeing disk space..."
df -h /
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc 2>/dev/null || true
df -h /
fi'

builds:
- env:
# goreleaser does not work with CGO, it could also complicate
Expand All @@ -23,7 +44,14 @@ builds:
binary: atmos
ldflags:
# Set `atmos` version to the GitHub release tag using Go `ldflags`
# -s: strip symbol table
# -w: strip DWARF debug info
# Both reduce binary size and disk usage during linking.
- '-s -w -X "github.com/cloudposse/atmos/pkg/version.Version={{.Version}}"'
flags:
# Remove file system paths from compiled executables for reproducible builds and slightly smaller binaries.
# This is standard practice for open-source and CI/CD builds (used by Go core team, HashiCorp, Docker, etc.).
- -trimpath

archives:
- format: binary
Expand Down
4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ var RootCmd = &cobra.Command{
if versionErr != nil {
errUtils.CheckErrorPrintAndExit(versionErr, "", "")
}
utils.OsExit(0)
errUtils.OsExit(0)
return
}
}
Expand Down Expand Up @@ -699,7 +699,7 @@ func initCobraConfig() {
pager := pager.NewWithAtmosConfig(pagerEnabled)
if err := pager.Run("Atmos CLI Help", buf.String()); err != nil {
log.Error("Failed to run pager", "error", err)
utils.OsExit(1)
errUtils.OsExit(1)
}
} else {
fmt.Println()
Expand Down
10 changes: 5 additions & 5 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"

errUtils "github.com/cloudposse/atmos/errors"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/utils"
)

func TestNoColorLog(t *testing.T) {
Expand Down Expand Up @@ -392,9 +392,9 @@ func TestVersionFlagExecutionPath(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save original OsExit and restore after test.
originalOsExit := utils.OsExit
originalOsExit := errUtils.OsExit
t.Cleanup(func() {
utils.OsExit = originalOsExit
errUtils.OsExit = originalOsExit
tt.cleanup()
})

Expand All @@ -403,7 +403,7 @@ func TestVersionFlagExecutionPath(t *testing.T) {
type exitPanic struct {
code int
}
utils.OsExit = func(code int) {
errUtils.OsExit = func(code int) {
panic(exitPanic{code: code})
}

Expand All @@ -414,7 +414,7 @@ func TestVersionFlagExecutionPath(t *testing.T) {
// Execute should call version command and then exit with expected code.
// We expect it to panic with our exitPanic struct containing the exit code.
// This verifies that the --version flag handler is being executed and
// calls os.Exit via utils.OsExit.
// calls os.Exit via errUtils.OsExit.
assert.PanicsWithValue(t, exitPanic{code: tt.expectExit}, func() {
_ = Execute()
}, "Execute should exit with code %d", tt.expectExit)
Expand Down
8 changes: 4 additions & 4 deletions cmd/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package cmd
import (
"errors"

log "github.com/cloudposse/atmos/pkg/logger"
"github.com/spf13/cobra"

errUtils "github.com/cloudposse/atmos/errors"
"github.com/cloudposse/atmos/internal/exec"
u "github.com/cloudposse/atmos/pkg/utils"
log "github.com/cloudposse/atmos/pkg/logger"
)

// ValidateSchemaCmd represents the 'atmos validate schema' command.
Expand Down Expand Up @@ -59,12 +59,12 @@ and are compliant with expected formats, reducing configuration drift and runtim

if key == "" && schema != "" {
log.Error("key not provided for the schema to be used")
u.OsExit(1)
errUtils.OsExit(1)
}

if err := exec.NewAtmosValidatorExecutor(&atmosConfig).ExecuteAtmosValidateSchemaCmd(key, schema); err != nil {
if errors.Is(err, exec.ErrInvalidYAML) {
u.OsExit(1)
errUtils.OsExit(1)
}
return err
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/workflow.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"errors"

"github.com/spf13/cobra"

errUtils "github.com/cloudposse/atmos/errors"
Expand Down Expand Up @@ -41,6 +43,11 @@ var workflowCmd = &cobra.Command{
// Check if it's a known error that's already printed in ExecuteWorkflowCmd.
// If it is, we don't need to print it again, but we do need to exit with a non-zero exit code.
if e.IsKnownWorkflowError(err) {
// Check if the error wraps an ExitCodeError to preserve the actual exit code.
var exitCodeErr errUtils.ExitCodeError
if errors.As(err, &exitCodeErr) {
errUtils.Exit(exitCodeErr.Code)
}
errUtils.Exit(1)
}
return err
Expand Down
13 changes: 11 additions & 2 deletions errors/error_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import (
"os"
"os/exec"

log "github.com/cloudposse/atmos/pkg/logger"
"golang.org/x/text/cases"
"golang.org/x/text/language"

log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/markdown"
)

// OsExit is a variable for testing, so we can mock os.Exit.
var OsExit = os.Exit

// render is the global Markdown renderer instance initialized via InitializeMarkdown.
var render *markdown.Renderer

Expand Down Expand Up @@ -59,6 +62,12 @@ func CheckErrorPrintAndExit(err error, title string, suggestion string) {

CheckErrorAndPrint(err, title, suggestion)

// Check for ExitCodeError (from ShellRunner preserving interp.ExitStatus)
var exitCodeErr ExitCodeError
if errors.As(err, &exitCodeErr) {
Exit(exitCodeErr.Code)
}

// Find the executed command's exit code from the error
var exitError *exec.ExitError
if errors.As(err, &exitError) {
Expand All @@ -73,5 +82,5 @@ func CheckErrorPrintAndExit(err error, title string, suggestion string) {

// Exit exits the program with the specified exit code.
func Exit(exitCode int) {
os.Exit(exitCode)
OsExit(exitCode)
}
171 changes: 136 additions & 35 deletions errors/error_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,102 @@ func Test_CheckErrorPrintAndExit(t *testing.T) {
}
}

func TestPrintErrorMarkdownAndExit(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
er := errors.New("critical failure")
CheckErrorPrintAndExit(er, "Fatal Error", "Check logs.")
return
}
execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestPrintErrorMarkdownAndExit")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 1, exitError.ExitCode())
} else {
assert.Fail(t, "Expected an exit error with code 1")
}
}

func TestPrintInvalidUsageErrorAndExit(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
er := errors.New("invalid command")
CheckErrorPrintAndExit(er, "", "Use --help for usage information.")
return
}
execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestPrintInvalidUsageErrorAndExit")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 1, exitError.ExitCode())
} else {
assert.Fail(t, "Expected an exit error with code 1")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestCheckErrorPrintAndExit_ExitCodeError(t *testing.T) {
if os.Getenv("TEST_EXIT_CODE_2") == "1" {
err := ExitCodeError{Code: 2}
CheckErrorPrintAndExit(err, "Exit Code Error", "")
return
}
if os.Getenv("TEST_EXIT_CODE_42") == "1" {
err := ExitCodeError{Code: 42}
CheckErrorPrintAndExit(err, "Exit Code Error", "")
return
}

// Test exit code 2
execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestCheckErrorPrintAndExit_ExitCodeError")
cmd.Env = append(os.Environ(), "TEST_EXIT_CODE_2=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 2, exitError.ExitCode(), "Should exit with code 2")
} else {
assert.Fail(t, "Expected an exit error with code 2")
}

// Test exit code 42
cmd = exec.Command(execPath, "-test.run=TestCheckErrorPrintAndExit_ExitCodeError")
cmd.Env = append(os.Environ(), "TEST_EXIT_CODE_42=1")
err = cmd.Run()
if errors.As(err, &exitError) {
assert.Equal(t, 42, exitError.ExitCode(), "Should exit with code 42")
} else {
assert.Fail(t, "Expected an exit error with code 42")
}
}

func TestCheckErrorPrintAndExit_ExecExitError(t *testing.T) {
if os.Getenv("TEST_EXEC_EXIT") == "1" {
// Create an exec.ExitError
cmd := exec.Command("sh", "-c", "exit 3")
err := cmd.Run()
CheckErrorPrintAndExit(err, "Exec Error", "")
return
}

execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestCheckErrorPrintAndExit_ExecExitError")
cmd.Env = append(os.Environ(), "TEST_EXEC_EXIT=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 3, exitError.ExitCode(), "Should exit with code 3 from exec.ExitError")
} else {
assert.Fail(t, "Expected an exit error with code 3")
}
}

func TestCheckErrorAndPrint(t *testing.T) {
// Save original logger
originalLogger := log.Default()
Expand Down Expand Up @@ -96,42 +192,47 @@ func TestCheckErrorAndPrint(t *testing.T) {
// Should log error directly
assert.Contains(t, logBuf.String(), "error with nil render")
})
}

func TestPrintErrorMarkdownAndExit(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
er := errors.New("critical failure")
CheckErrorPrintAndExit(er, "Fatal Error", "Check logs.")
return
}
execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestPrintErrorMarkdownAndExit")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 1, exitError.ExitCode())
} else {
assert.Fail(t, "Expected an exit error with code 1")
}
// Test with empty title (defaults to "Error")
t.Run("empty title", func(t *testing.T) {
render, _ = markdown.NewTerminalMarkdownRenderer(schema.AtmosConfiguration{})

r, w, _ := os.Pipe()
os.Stderr = w

testErr := errors.New("test error with empty title")
CheckErrorAndPrint(testErr, "", "")

w.Close()
os.Stderr = oldStderr

var output bytes.Buffer
io.Copy(&output, r)

// Should default to "Error" as title
assert.Contains(t, output.String(), "Error")
})
}
Comment thread
aknysh marked this conversation as resolved.

func TestPrintInvalidUsageErrorAndExit(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
er := errors.New("invalid command")
CheckErrorPrintAndExit(er, "", "Use --help for usage information.")
return
}
execPath, err := exec.LookPath(os.Args[0])
assert.Nil(t, err)
cmd := exec.Command(execPath, "-test.run=TestPrintErrorMarkdownAndExit")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err = cmd.Run()
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 1, exitError.ExitCode())
} else {
assert.Fail(t, "Expected an exit error with code 1")
}
func TestInitializeMarkdown(t *testing.T) {
// Save original logger
originalLogger := log.Default()
defer log.SetDefault(originalLogger)

// Create test logger to capture log output
var logBuf bytes.Buffer
testLogger := log.New()
testLogger.SetOutput(&logBuf)
testLogger.SetLevel(log.TraceLevel)
log.SetDefault(testLogger)

// Test with valid configuration
t.Run("valid configuration", func(t *testing.T) {
logBuf.Reset()
atmosConfig := schema.AtmosConfiguration{}
InitializeMarkdown(atmosConfig)

// Should initialize without error
assert.NotContains(t, logBuf.String(), "failed to initialize Markdown renderer")
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading