Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
013127c
feat: Support Atmos Toolchain for 3rd Party Tools (#1686)
osterman Dec 12, 2025
c3c3853
fix: Add mutex protection for global GitHub API client
osterman Dec 12, 2025
ff616ab
fix: Remove --github-token from global flags and fix test failures
osterman Dec 13, 2025
1a321cc
fix: Remove trailing whitespace from help text after lipgloss rendering
osterman Dec 13, 2025
cfe384e
fix: Address code quality and linting issues in toolchain subsystem
osterman Dec 13, 2025
773c7b5
fix: Add comma to multi-line comment for godot compliance
osterman Dec 13, 2025
e88d27f
docs: Fix PRD status inconsistency in multi-registry-support.md
osterman Dec 13, 2025
eee86a0
fix: Use ANSI-aware trimming for help text trailing whitespace
osterman Dec 13, 2025
4a72d34
fix: Add -buildvcs=false to test runner and regenerate snapshots
osterman Dec 13, 2025
cc909ec
fix: Use dynamic stdout/stderr references in I/O layer for test capture
osterman Dec 13, 2025
d964962
test: Improve test coverage for toolchain packages
osterman Dec 13, 2025
d0cfadd
fix: Add linter fixes and additional test coverage
osterman Dec 13, 2025
0f6469e
fix: Test quality improvements and help output regression fixes
osterman Dec 14, 2025
874b991
fix: Use cross-platform path in TestIsToolInstalled
osterman Dec 14, 2025
1625f51
fix: Add cleanup for toolchain config and viper state in test
osterman Dec 15, 2025
948558e
fix: Improve test quality in installer_test.go
osterman Dec 15, 2025
0a0f6fa
chore: Regenerate help command snapshots
osterman Dec 15, 2025
9be3236
feat: Add ref field for pinning toolchain registry to specific git refs
osterman Dec 16, 2025
e565d29
feat: Add --use-version flag and version switching with re-exec mecha…
osterman Dec 16, 2025
37083bf
fix: Strip --use-version flag before re-exec to older versions
osterman Dec 16, 2025
4217013
feat: Simplify atmos version list --installed table with active indic…
osterman Dec 16, 2025
edcd2c0
feat: Add current version indicator to atmos version list --installed
osterman Dec 16, 2025
e44291a
feat: Replace INSTALLED column with dot indicators in toolchain list
osterman Dec 16, 2025
eb18ac3
refactor: Split large toolchain files to fix file-length-limit lint i…
osterman Dec 16, 2025
63d65d2
fix: Reduce cognitive and cyclomatic complexity in toolchain code
osterman Dec 16, 2025
b24b7d3
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 16, 2025
cb3b461
fix: Reduce cognitive complexity in TestCleanToolsAndCaches
osterman Dec 16, 2025
243307c
fix: Address CodeRabbit review comments
osterman Dec 16, 2025
8ef75ed
fix: Resolve test failures and update documentation
osterman Dec 18, 2025
910369b
chore: Remove .claude/plans from version control
osterman Dec 18, 2025
d30a6da
test: Regenerate golden snapshots for --use-version flag
osterman Dec 19, 2025
f69a4f2
fix: Use errUtils.OsExit instead of os.Exit for Go 1.25+ test compati…
osterman Dec 19, 2025
1e1c32f
chore: Remove outdated toolchain development artifacts
osterman Dec 19, 2025
06c5fcd
fix: Correct golden snapshot whitespace for validate component test
osterman Dec 21, 2025
b0790c3
docs(roadmap): Mark toolchain management as shipped
osterman Dec 27, 2025
9d0c135
test: Regenerate golden snapshots for --help command changes
osterman Dec 28, 2025
c19d881
Merge branch 'main' into tools-experiment
osterman Dec 28, 2025
5a706d4
Merge branch 'main' into tools-experiment
osterman Dec 29, 2025
a8e2bdf
Merge branch 'main' into tools-experiment
osterman Dec 30, 2025
d6b82ae
Merge branch 'main' into tools-experiment
aknysh Dec 31, 2025
1f8c645
add tests
aknysh Dec 31, 2025
834e8e7
update docs
aknysh Dec 31, 2025
4ae2a00
update docs
aknysh Dec 31, 2025
35d9d94
add tests
aknysh Dec 31, 2025
9ba2889
Merge branch 'main' into osterman/list-affected-command
aknysh Dec 31, 2025
e0b3923
address comments
aknysh Dec 31, 2025
6bafe3d
address comments, update Atmos JSON schemas
aknysh Dec 31, 2025
f778a2c
update docs
aknysh Dec 31, 2025
61de449
fix issues, address comments, update docs, add tests
aknysh Dec 31, 2025
56a46ca
fix issues, address comments, update docs, add tests
aknysh Dec 31, 2025
53fa465
fix: prevent Bubble Tea spinner hang in non-TTY environments
aknysh Jan 1, 2026
456b0c2
fix: address CodeRabbit review comments
aknysh Jan 1, 2026
30de6a0
fix tests
aknysh Jan 1, 2026
d24ce5f
fix tests, address comments
aknysh Jan 1, 2026
90b465e
fix tests
aknysh Jan 1, 2026
e0621e3
fix MacOS CI tests
aknysh Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,23 @@ tools/gomodcheck/.gomodcheck
# Claude Code and Hive Mind development tools
.claude-flow/
.hive-mind/
.claude/plans/

performance-optimization/
node_modules

# Toolchain installed tools
.tools/
toolchain/.tools/
toolchain/.tool-versions

# Merge conflict artifacts and backup files
*.orig
*.rej
.scratch
*.bak
*.bak[0-9]
*.bak[0-9][0-9]
*.go.bak
**/*.go.bak

Expand All @@ -110,4 +119,10 @@ TEST_FAILURE_ANALYSIS.md
scratch/
COVERAGE_SUMMARY.md
TESTABILITY_CHANGES_SUMMARY.md

# Conductor scratch/temporary files
*.log
*_progress.txt
*_summary.txt
reply_to_*.sh
*.ansi
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ ALWAYS build the website after documentation changes: `cd website && npm run bui
### Git (MANDATORY)
Don't commit: todos, research, scratch files. Do commit: code, tests, requested docs, schemas. Update `.gitignore` for patterns only.

**NEVER run destructive git commands without explicit user confirmation:**
- `git reset HEAD` or `git reset --hard` - discards staged/committed changes
- `git checkout HEAD -- .` or `git checkout -- .` - discards all working changes
- `git clean -fd` - deletes untracked files
- `git stash drop` - permanently deletes stashed changes

Always ask first: "This will discard uncommitted changes. Proceed? [y/N]"

### Test Coverage (MANDATORY)
80% minimum (CodeCov enforced). All features need tests. `make testacc-coverage` for reports.

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2020-2025 Cloud Posse, LLC
Copyright 2020-2026 Cloud Posse, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
1 change: 1 addition & 0 deletions cmd/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ func TestIdentitySelectorBehavior(t *testing.T) {
// will check flag first, then fall back to viper.

// Set viper value if needed to simulate config/env values.
// Clean up viper value after subtest to prevent pollution.
if tt.setViperValue {
viper.Set(IdentityFlagName, tt.viperIdentityValue)
t.Cleanup(func() {
Expand Down
72 changes: 46 additions & 26 deletions cmd/auth_shell_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package cmd

import (
"path/filepath"
"runtime"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

errUtils "github.com/cloudposse/atmos/errors"
"github.com/cloudposse/atmos/pkg/auth/identities/aws"
)

Expand All @@ -21,47 +23,49 @@ func disableAWSCredentialPrompting(t *testing.T) {
}

func TestAuthShellCmd_FlagParsing(t *testing.T) {
// Use TestKit to ensure proper test isolation.
_ = NewTestKit(t)
disableAWSCredentialPrompting(t)

tests := []struct {
name string
args []string
expectedError string
name string
args []string
expectedSentinelErr error // The specific sentinel error expected
}{
{
name: "no identity specified uses default",
args: []string{},
// This will fail with auth errors since we don't have real AWS SSO configured.
expectedError: "authentication failed",
// Fixture has test-admin as default. Will fail at authentication since we don't have real AWS SSO.
expectedSentinelErr: errUtils.ErrAuthenticationFailed,
},
{
name: "nonexistent identity",
args: []string{"--identity=nonexistent"},
expectedError: "identity not found",
name: "nonexistent identity",
args: []string{"--identity=nonexistent"},
expectedSentinelErr: errUtils.ErrIdentityNotFound,
},
{
name: "valid identity",
args: []string{"--identity=test-user"},
// This will fail with auth errors since we don't have real AWS credentials.
expectedError: "authentication failed",
// This will fail at authentication since we don't have real AWS credentials.
expectedSentinelErr: errUtils.ErrAuthenticationFailed,
},
{
name: "shell override flag",
args: []string{"--shell", "/bin/bash"},
// This will fail with auth errors since we don't have real AWS credentials.
expectedError: "authentication failed",
// Fixture has test-admin as default. Will fail at authentication since we don't have real AWS SSO.
expectedSentinelErr: errUtils.ErrAuthenticationFailed,
},
{
name: "shell args after double dash",
args: []string{"--", "-c", "echo test"},
// This will fail with auth errors since we don't have real AWS credentials.
expectedError: "authentication failed",
// Fixture has test-admin as default. Will fail at authentication since we don't have real AWS SSO.
expectedSentinelErr: errUtils.ErrAuthenticationFailed,
},
{
name: "identity with shell args",
args: []string{"--identity=test-user", "--", "-c", "env"},
// This will fail with auth errors since we don't have real AWS credentials.
expectedError: "authentication failed",
// This will fail at authentication since we don't have real AWS credentials.
expectedSentinelErr: errUtils.ErrAuthenticationFailed,
},
}

Expand All @@ -82,11 +86,11 @@ func TestAuthShellCmd_FlagParsing(t *testing.T) {
// Call the core business logic directly, bypassing handleHelpRequest and checkAtmosConfig.
err := executeAuthShellCommandCore(testCmd, tt.args)

if tt.expectedError != "" {
assert.Error(t, err)
if err != nil {
assert.Contains(t, err.Error(), tt.expectedError)
}
if tt.expectedSentinelErr != nil {
require.Error(t, err, "Expected an error but got nil")
assert.ErrorIs(t, err, tt.expectedSentinelErr,
"Expected error chain to contain %v, but got: %v",
tt.expectedSentinelErr, err)
} else {
assert.NoError(t, err)
}
Expand All @@ -95,6 +99,8 @@ func TestAuthShellCmd_FlagParsing(t *testing.T) {
}

func TestAuthShellCmd_CommandStructure(t *testing.T) {
_ = NewTestKit(t)

// Test that the real authShellCmd has the expected structure.
assert.Equal(t, "shell", authShellCmd.Use)
assert.True(t, authShellCmd.DisableFlagParsing, "DisableFlagParsing should be true to allow pass-through of shell arguments")
Expand All @@ -113,6 +119,7 @@ func TestAuthShellCmd_CommandStructure(t *testing.T) {
}

func TestAuthShellCmd_InvalidFlagHandling(t *testing.T) {
_ = NewTestKit(t)
disableAWSCredentialPrompting(t)

// Set up test fixture.
Expand All @@ -136,7 +143,9 @@ func TestAuthShellCmd_EmptyEnvVars(t *testing.T) {

// Test that the command handles nil environment variables gracefully.
// This tests the path where envVars is nil and gets initialized to empty map.
testDir := "../tests/fixtures/scenarios/atmos-auth"
testDir, err := filepath.Abs("../tests/fixtures/scenarios/atmos-auth")
require.NoError(t, err, "Failed to get absolute path to test fixture")

t.Setenv("ATMOS_CLI_CONFIG_PATH", testDir)
t.Setenv("ATMOS_BASE_PATH", testDir)

Expand All @@ -147,13 +156,16 @@ func TestAuthShellCmd_EmptyEnvVars(t *testing.T) {
testCmd.Flags().AddFlagSet(authShellCmd.Flags())

// This will fail at authentication but will exercise the env var initialization path.
err := executeAuthShellCommandCore(testCmd, []string{"--identity=test-user"})
assert.Error(t, err)
// Should contain authentication failed, not nil pointer errors.
assert.Contains(t, err.Error(), "authentication failed")
err = executeAuthShellCommandCore(testCmd, []string{"--identity=test-user"})
// Should be an authentication error, not nil pointer errors.
require.Error(t, err, "Expected an error but got nil")
assert.ErrorIs(t, err, errUtils.ErrAuthenticationFailed,
"Expected ErrAuthenticationFailed, but got: %v", err)
}

func TestAuthShellCmd_HelpRequest(t *testing.T) {
_ = NewTestKit(t)

// Test that the command handles help request arguments.
// When DisableFlagParsing is true, Cobra doesn't add the help flag automatically,
// so handleHelpRequest in cmd/helpers.go handles --help and -h manually.
Expand All @@ -170,6 +182,8 @@ func TestAuthShellCmd_HelpRequest(t *testing.T) {
}

func TestAuthShellCmd_ShellEnvironmentBinding(t *testing.T) {
_ = NewTestKit(t)

// Test that SHELL and ATMOS_SHELL environment variables are bound.
// This verifies the init() function's viper bindings work correctly.
testShell := "/bin/test-shell"
Expand All @@ -186,6 +200,8 @@ func TestAuthShellCmd_ShellEnvironmentBinding(t *testing.T) {
}

func TestAuthShellCmd_WithMockProvider(t *testing.T) {
_ = NewTestKit(t)

if testing.Short() {
t.Skipf("Skipping integration test in short mode: spawns actual shell process")
}
Expand Down Expand Up @@ -228,6 +244,8 @@ func TestAuthShellCmd_WithMockProvider(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = NewTestKit(t)

// Set up mock auth provider fixture for each subtest.
testDir := "../tests/fixtures/scenarios/atmos-auth-mock"
t.Setenv("ATMOS_CLI_CONFIG_PATH", testDir)
Expand All @@ -251,6 +269,8 @@ func TestAuthShellCmd_WithMockProvider(t *testing.T) {
}

func TestAuthShellCmd_MockProviderEnvironmentVariables(t *testing.T) {
_ = NewTestKit(t)

if testing.Short() {
t.Skipf("Skipping integration test in short mode: spawns actual shell process")
}
Expand Down
13 changes: 5 additions & 8 deletions cmd/auth_whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
Expand All @@ -17,6 +16,7 @@ import (
authTypes "github.com/cloudposse/atmos/pkg/auth/types"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/config/homedir"
"github.com/cloudposse/atmos/pkg/data"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui"
Expand Down Expand Up @@ -161,13 +161,10 @@ func printWhoamiJSON(whoami *authTypes.WhoamiInfo) error {
if whoami.Environment != nil {
redactedWhoami.Environment = sanitizeEnvMap(whoami.Environment, homeDir)
}
jsonData, err := json.MarshalIndent(redactedWhoami, "", " ")
if err != nil {
errUtils.CheckErrorAndPrint(errUtils.ErrInvalidAuthConfig, "Failed to marshal JSON", "")
return errUtils.ErrInvalidAuthConfig
}
fmt.Println(string(jsonData))
return nil
// Use data.WriteJSON() to write to the data channel (stdout) with proper I/O handling.
// This ensures output goes through the I/O layer with automatic masking and respects
// stream redirection in tests.
return data.WriteJSON(redactedWhoami)
}

func printWhoamiHuman(whoami *authTypes.WhoamiInfo, isValid bool) {
Expand Down
46 changes: 46 additions & 0 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/cloudposse/atmos/pkg/auth/credentials"
"github.com/cloudposse/atmos/pkg/auth/validation"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/dependencies"
envpkg "github.com/cloudposse/atmos/pkg/env"
l "github.com/cloudposse/atmos/pkg/list"
log "github.com/cloudposse/atmos/pkg/logger"
Expand Down Expand Up @@ -430,6 +431,26 @@ func executeCustomCommand(
finalArgs = args
}

// Resolve and install command dependencies
resolver := dependencies.NewResolver(&atmosConfig)
deps, err := resolver.ResolveCommandDependencies(commandConfig)
if err != nil {
errUtils.CheckErrorPrintAndExit(err, "", fmt.Sprintf("Failed to resolve dependencies for command '%s'", commandConfig.Name))
}

if len(deps) > 0 {
log.Debug("Installing command dependencies", "command", commandConfig.Name, "tools", deps)
installer := dependencies.NewInstaller(&atmosConfig)
if err := installer.EnsureTools(deps); err != nil {
errUtils.CheckErrorPrintAndExit(err, "", fmt.Sprintf("Failed to install dependencies for command '%s'", commandConfig.Name))
}

// Update PATH to include installed tools
if err := dependencies.UpdatePathForTools(&atmosConfig, deps); err != nil {
errUtils.CheckErrorPrintAndExit(err, "", fmt.Sprintf("Failed to update PATH for command '%s'", commandConfig.Name))
}
}

// Create auth manager if identity is specified for this custom command.
// Check for --identity flag first (it overrides the config).
var authManager auth.AuthManager
Expand Down Expand Up @@ -806,6 +827,31 @@ func isVersionCommand() bool {
return len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "--version")
}

// isVersionManagementCommand checks if the current command is a version management command.
// These commands should not trigger re-exec to avoid infinite loops.
func isVersionManagementCommand(cmd *cobra.Command) bool {
if cmd == nil {
return false
}

// Check the command hierarchy.
cmdName := cmd.Name()

// Direct version subcommands that manage local installations (install, uninstall).
// Note: "list" is excluded because it can reasonably work with --use-version
// to list releases using a different Atmos version.
if cmd.Parent() != nil && cmd.Parent().Name() == "version" {
return cmdName == "install" || cmdName == "uninstall"
}

// The version command itself (shows current version).
if cmdName == "version" && cmd.Parent() != nil && cmd.Parent().Name() == "atmos" {
return true
}

return false
}

// handleHelpRequest shows help content and exits only if the first argument is "help" or "--help" or "-h".
func handleHelpRequest(cmd *cobra.Command, args []string) {
if (len(args) > 0 && args[0] == "help") || Contains(args, "--help") || Contains(args, "-h") {
Expand Down
6 changes: 6 additions & 0 deletions cmd/describe_affected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ func TestDescribeAffected(t *testing.T) {
t.Setenv("IDENTITY", "")

t.Chdir("../tests/fixtures/scenarios/basic")

// Disable authentication for this test to prevent validation errors.
// Set both environment variable and viper value to ensure it's recognized.
t.Setenv("ATMOS_IDENTITY", "false")
viper.Set("identity", "false")

ctrl := gomock.NewController(t)
defer ctrl.Finish()

Expand Down
5 changes: 4 additions & 1 deletion cmd/help_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,10 @@ func printDescription(w io.Writer, cmd *cobra.Command, styles *helpStyles) {
// Use markdown rendering to respect terminal width and wrapping settings.
// This ensures long descriptions wrap properly based on screen width.
rendered := renderMarkdownDescription(desc)
fmt.Fprintln(w, styles.commandDesc.Render(rendered))
styled := styles.commandDesc.Render(rendered)
// Lipgloss pads multi-line strings to uniform width. Trim trailing whitespace from each line.
styled = ui.TrimLinesRight(styled)
fmt.Fprintln(w, styled)
fmt.Fprintln(w)
}

Expand Down
9 changes: 9 additions & 0 deletions cmd/markdown/atmos_toolchain_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- Add a tool with version
```
$ atmos toolchain add <tool-name> <version>
```

- Use a custom tool versions file
```
$ atmos toolchain add --file <path/to/.tool-versions> <tool-name> <version>
```
4 changes: 4 additions & 0 deletions cmd/markdown/atmos_toolchain_aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Get the aliases configured
```
$ atmos toolchain aliases
```
5 changes: 5 additions & 0 deletions cmd/markdown/atmos_toolchain_clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- Delete everything from toolchain

```
$ atmos toolchain clean
```
Loading
Loading