Skip to content

Commit aaeb2d2

Browse files
authored
Merge branch 'main' into copilot/add-description-field-to-components
2 parents 9d988f6 + 2be2909 commit aaeb2d2

File tree

108 files changed

+5908
-684
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+5908
-684
lines changed

.claude/agents/gist-creator.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
name: gist-creator
3+
description: >-
4+
Expert in creating Atmos gists with proper structure, README documentation,
5+
file-browser plugin integration, and blog post announcements.
6+
7+
**Invoke when:**
8+
- User wants to create a new gist
9+
- User asks about gist best practices
10+
- User wants to add a community recipe or pattern
11+
- User mentions "gist" in the context of documentation
12+
13+
tools: Read, Write, Edit, Grep, Glob, Bash, Task, TodoWrite
14+
model: sonnet
15+
color: purple
16+
---
17+
18+
# Gist Creator Agent
19+
20+
Expert in creating well-structured Atmos gists that demonstrate creative combinations of Atmos features.
21+
22+
## What Are Gists?
23+
24+
Gists are community-contributed recipes that show how to combine Atmos features in creative ways. Unlike maintained examples, gists are shared as-is and may not work with current versions of Atmos without adaptations.
25+
26+
## Core Responsibilities
27+
28+
1. Create new gists with proper directory structure
29+
2. Write comprehensive README documentation
30+
3. Add tags/docs mappings to the file-browser plugin
31+
4. Create blog post announcements for new gists
32+
33+
## Gist Directory Structure
34+
35+
Gists live in the `/gists/` directory at the repo root (alongside `/examples/`).
36+
37+
```text
38+
gists/{name}/
39+
├── README.md # Comprehensive guide (REQUIRED)
40+
├── atmos.yaml # Atmos configuration (REQUIRED)
41+
├── .atmos.d/ # Split config files (if applicable)
42+
│ ├── feature1.yaml
43+
│ └── feature2.yaml
44+
├── .mcp.json # MCP config (if applicable)
45+
└── stacks/ # Stack configs (if needed)
46+
```
47+
48+
## README Format
49+
50+
Every gist README.md MUST include:
51+
52+
1. **Title** — Clear, descriptive name as H1
53+
2. **Overview** — One paragraph explaining the problem and solution
54+
3. **The Problem** — What pain point this solves
55+
4. **The Solution** — How Atmos features combine to solve it
56+
5. **Features Used** — Bulleted list with links to relevant Atmos docs
57+
6. **How It Works** — Step-by-step explanation of the architecture/flow
58+
7. **Getting Started** — Prerequisites and setup steps
59+
8. **Configuration Files** — Table describing each file in the gist
60+
9. **Usage** — Concrete command examples
61+
10. **Customization** — How to adapt for different environments
62+
11. **The Key Insight** — The main takeaway or "aha moment"
63+
64+
## File-Browser Plugin Integration
65+
66+
After creating the gist directory, add entries to `website/plugins/file-browser/index.js`:
67+
68+
### TAGS_MAP
69+
Add tags for the new gist. Available tags: Quickstart, Stacks, Components, Automation, DX
70+
71+
```js
72+
'my-gist-name': ['DX', 'Automation'],
73+
```
74+
75+
### DOCS_MAP
76+
Add documentation links for the new gist:
77+
78+
```js
79+
'my-gist-name': [
80+
{ label: 'Feature Name', url: '/docs/url' },
81+
],
82+
```
83+
84+
## Blog Post Announcement
85+
86+
New gists MUST be announced with a blog post in `website/blog/YYYY-MM-DD-gist-slug.mdx`.
87+
88+
IMPORTANT: Only use tags from `website/blog/tags.yml` and authors from `website/blog/authors.yml`.
89+
90+
```mdx
91+
---
92+
slug: gist-slug
93+
title: "Gist: Descriptive Title"
94+
authors: [author-id] # Must exist in website/blog/authors.yml
95+
tags: [feature]
96+
---
97+
98+
Brief intro about the gist.
99+
100+
<!--truncate-->
101+
102+
## What This Gist Does
103+
[Description]
104+
105+
## Features Used
106+
[List of Atmos features combined]
107+
108+
## Try It Out
109+
[Link to /gists/name]
110+
111+
## Get Involved
112+
- Browse the [Gists collection](/gists)
113+
- [Join us on Slack](/community/slack)
114+
- [Attend Office Hours](/community/office-hours)
115+
```
116+
117+
## Key Differences from Examples
118+
119+
| | Examples | Gists |
120+
|---|---|---|
121+
| **Location** | `/examples/` | `/gists/` |
122+
| **Maintained** | Yes, tested each release | No, shared as-is |
123+
| **Scope** | Single feature | Multiple features combined |
124+
| **Disclaimer** | None | GistDisclaimer shown via file-browser plugin |
125+
| **Style** | Minimal config files | Rich README + config files |
126+
127+
## GistDisclaimer Component
128+
129+
The disclaimer is automatically shown on all gist pages by the file-browser plugin (configured via the `disclaimer` option in `website/docusaurus.config.js`). No manual inclusion needed in gist files.
130+
131+
The component is at `website/src/components/GistDisclaimer/` and accepts a `text` prop.
132+
133+
## Verification Checklist
134+
135+
After creating a gist:
136+
1. `gists/{name}/README.md` exists and is comprehensive
137+
2. `gists/{name}/atmos.yaml` exists
138+
3. Tags added to TAGS_MAP in `website/plugins/file-browser/index.js`
139+
4. Docs added to DOCS_MAP in `website/plugins/file-browser/index.js`
140+
5. Blog post created in `website/blog/`
141+
6. Website builds successfully: `cd website && npm run build`

cmd/auth_exec.go

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,17 @@ func executeAuthExecCommandCore(cmd *cobra.Command, args []string) error {
110110

111111
// Prepare shell environment with file-based credentials.
112112
// Start with current OS environment + global env from atmos.yaml and let PrepareShellEnvironment configure auth.
113+
// PrepareShellEnvironment sanitizes the env (removes IRSA/credential vars) and adds auth vars.
113114
baseEnv := envpkg.MergeGlobalEnv(os.Environ(), atmosConfig.Env)
114115
envList, err := authManager.PrepareShellEnvironment(ctx, identityName, baseEnv)
115116
if err != nil {
116117
return fmt.Errorf("failed to prepare command environment: %w", err)
117118
}
118119

119-
// Convert environment list to map for executeCommandWithEnv.
120-
envMap := make(map[string]string)
121-
for _, envVar := range envList {
122-
if idx := strings.IndexByte(envVar, '='); idx >= 0 {
123-
key := envVar[:idx]
124-
value := envVar[idx+1:]
125-
envMap[key] = value
126-
}
127-
}
128-
129-
// Execute the command with authentication environment.
130-
err = executeCommandWithEnv(commandArgs, envMap)
120+
// Execute the command with the sanitized environment directly.
121+
// The envList already includes os.Environ() (sanitized) + auth vars,
122+
// so we pass it as the complete subprocess environment.
123+
err = executeCommandWithEnv(commandArgs, envList)
131124
if err != nil {
132125
// For any subprocess error, provide a tip about refreshing credentials.
133126
// This helps users when AWS tokens are expired or invalid.
@@ -137,29 +130,25 @@ func executeAuthExecCommandCore(cmd *cobra.Command, args []string) error {
137130
return nil
138131
}
139132

140-
// executeCommandWithEnv executes a command with additional environment variables.
141-
func executeCommandWithEnv(args []string, envVars map[string]string) error {
133+
// executeCommandWithEnv executes a command with a complete environment.
134+
// The env parameter should be a fully prepared environment (e.g., from PrepareShellEnvironment).
135+
// It is used directly as the subprocess environment without re-reading os.Environ().
136+
func executeCommandWithEnv(args []string, env []string) error {
142137
if len(args) == 0 {
143138
return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrNoCommandSpecified, errUtils.ErrInvalidSubcommand)
144139
}
145140

146-
// Prepare the command
141+
// Prepare the command.
147142
cmdName := args[0]
148143
cmdArgs := args[1:]
149144

150-
// Look for the command in PATH
145+
// Look for the command in PATH.
151146
cmdPath, err := exec.LookPath(cmdName)
152147
if err != nil {
153148
return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrCommandNotFound, err)
154149
}
155150

156-
// Prepare environment variables
157-
env := os.Environ()
158-
for key, value := range envVars {
159-
env = append(env, fmt.Sprintf("%s=%s", key, value))
160-
}
161-
162-
// Execute the command
151+
// Execute the command with the provided environment directly.
163152
execCmd := exec.Command(cmdPath, cmdArgs...)
164153
execCmd.Env = env
165154
execCmd.Stdin = os.Stdin

cmd/auth_exec_test.go

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"bytes"
55
"errors"
6+
"os"
67
"runtime"
78
"testing"
89

@@ -218,52 +219,57 @@ func TestExtractIdentityFlag(t *testing.T) {
218219
}
219220

220221
func TestExecuteCommandWithEnv(t *testing.T) {
222+
// Use the test binary itself as a cross-platform subprocess helper.
223+
// TestMain in testing_main_test.go handles _ATMOS_TEST_EXIT_ONE.
224+
exePath, err := os.Executable()
225+
require.NoError(t, err, "os.Executable() must succeed")
226+
227+
// Prerequisite: verify that env vars reach the child process.
228+
t.Run("env propagation to subprocess", func(t *testing.T) {
229+
// Running the test binary with -test.run=^$ matches no tests and exits 0,
230+
// confirming the subprocess receives the provided environment.
231+
err := executeCommandWithEnv(
232+
[]string{exePath, "-test.run=^$"},
233+
[]string{"TEST_VAR=test-value"},
234+
)
235+
assert.NoError(t, err)
236+
})
237+
221238
// Test the command execution helper directly.
222239
tests := []struct {
223240
name string
224241
args []string
225-
envVars map[string]string
226-
skipOnWindows bool
242+
envVars []string
227243
expectedError string
228-
expectedCode int // Expected exit code if error is ExitCodeError
244+
expectedCode int // Expected exit code if error is ExitCodeError.
229245
}{
230246
{
231247
name: "empty args",
232248
args: []string{},
233-
envVars: map[string]string{},
249+
envVars: []string{},
234250
expectedError: "no command specified",
235251
},
236252
{
237-
name: "simple echo command",
238-
args: []string{"echo", "hello"},
239-
envVars: map[string]string{
240-
"TEST_VAR": "test-value",
241-
},
242-
skipOnWindows: true,
253+
name: "successful command",
254+
args: []string{exePath, "-test.run=^$"},
255+
envVars: []string{"TEST_VAR=test-value"},
243256
},
244257
{
245258
name: "nonexistent command",
246259
args: []string{"nonexistent-command-xyz"},
247-
envVars: map[string]string{},
260+
envVars: []string{},
248261
expectedError: "command not found",
249262
},
250263
{
251-
name: "command with non-zero exit code",
252-
args: []string{"sh", "-c", "exit 2"},
253-
envVars: map[string]string{
254-
"TEST_VAR": "test-value",
255-
},
256-
skipOnWindows: true,
257-
expectedCode: 2,
264+
name: "command with non-zero exit code",
265+
args: []string{exePath, "-test.run=^$"},
266+
envVars: []string{"_ATMOS_TEST_EXIT_ONE=1"},
267+
expectedCode: 1,
258268
},
259269
}
260270

261271
for _, tt := range tests {
262272
t.Run(tt.name, func(t *testing.T) {
263-
if tt.skipOnWindows && runtime.GOOS == "windows" {
264-
t.Skipf("Skipping test on Windows: command behaves differently")
265-
}
266-
267273
err := executeCommandWithEnv(tt.args, tt.envVars)
268274

269275
switch {
@@ -274,7 +280,7 @@ func TestExecuteCommandWithEnv(t *testing.T) {
274280
}
275281
case tt.expectedCode != 0:
276282
assert.Error(t, err)
277-
// Check that it's an ExitCodeError with the correct code
283+
// Check that it's an ExitCodeError with the correct code.
278284
var exitCodeErr errUtils.ExitCodeError
279285
if assert.True(t, errors.As(err, &exitCodeErr), "error should be ExitCodeError") {
280286
assert.Equal(t, tt.expectedCode, exitCodeErr.Code)

cmd/auth_shell.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func executeAuthShellCommandCore(cmd *cobra.Command, args []string) error {
115115

116116
// Prepare shell environment with file-based credentials.
117117
// Start with current OS environment and let PrepareShellEnvironment configure auth.
118+
// PrepareShellEnvironment sanitizes the env (removes IRSA/credential vars) and adds auth vars.
118119
envList, err := authManager.PrepareShellEnvironment(ctx, identityName, os.Environ())
119120
if err != nil {
120121
return fmt.Errorf("failed to prepare shell environment: %w", err)
@@ -129,18 +130,9 @@ func executeAuthShellCommandCore(cmd *cobra.Command, args []string) error {
129130
// Get provider name from the identity to display in shell messages.
130131
providerName := authManager.GetProviderForIdentity(identityName)
131132

132-
// Execute the shell with authentication environment.
133-
// ExecAuthShellCommand expects env vars as a map, so convert the list.
134-
envMap := make(map[string]string)
135-
for _, envVar := range envList {
136-
if idx := strings.IndexByte(envVar, '='); idx >= 0 {
137-
key := envVar[:idx]
138-
value := envVar[idx+1:]
139-
envMap[key] = value
140-
}
141-
}
142-
143-
return exec.ExecAuthShellCommand(atmosConfigPtr, identityName, providerName, envMap, shell, shellArgs)
133+
// Execute the shell with the sanitized environment directly.
134+
// envList already includes os.Environ() (sanitized) + auth vars.
135+
return exec.ExecAuthShellCommand(atmosConfigPtr, identityName, providerName, envList, shell, shellArgs)
144136
}
145137

146138
// extractAuthShellFlags extracts --identity and --shell flags from args and returns the remaining shell args.

cmd/list/affected.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/spf13/viper"
66

77
e "github.com/cloudposse/atmos/internal/exec"
8+
cfg "github.com/cloudposse/atmos/pkg/config"
89
"github.com/cloudposse/atmos/pkg/flags"
910
"github.com/cloudposse/atmos/pkg/flags/global"
1011
"github.com/cloudposse/atmos/pkg/list"
@@ -39,6 +40,9 @@ type AffectedOptions struct {
3940
ProcessTemplates bool
4041
ProcessFunctions bool
4142
Skip []string
43+
44+
// Auth flags.
45+
IdentityName string
4246
}
4347

4448
// affectedCmd lists affected Atmos components and stacks.
@@ -59,6 +63,15 @@ var affectedCmd = &cobra.Command{
5963
return err
6064
}
6165

66+
// Read identity from flag (inherited from listCmd PersistentFlags) or env var.
67+
var identityName string
68+
if cmd.Flags().Changed(cfg.IdentityFlagName) {
69+
identityName, _ = cmd.Flags().GetString(cfg.IdentityFlagName)
70+
} else {
71+
identityName = v.GetString(cfg.IdentityFlagName)
72+
}
73+
identityName = cfg.NormalizeIdentityValue(identityName)
74+
6275
opts := &AffectedOptions{
6376
Flags: flags.ParseGlobalFlags(cmd, v),
6477
Format: v.GetString("format"),
@@ -77,6 +90,7 @@ var affectedCmd = &cobra.Command{
7790
ProcessTemplates: v.GetBool("process-templates"),
7891
ProcessFunctions: v.GetBool("process-functions"),
7992
Skip: v.GetStringSlice("skip"),
93+
IdentityName: identityName,
8094
}
8195

8296
return executeListAffectedCmd(cmd, args, opts)
@@ -147,5 +161,6 @@ func executeListAffectedCmd(cmd *cobra.Command, args []string, opts *AffectedOpt
147161
ProcessFunctions: opts.ProcessFunctions,
148162
Skip: opts.Skip,
149163
ExcludeLocked: opts.ExcludeLocked,
164+
IdentityName: opts.IdentityName,
150165
})
151166
}

0 commit comments

Comments
 (0)