Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 10 additions & 8 deletions agent-skills/skills/atmos-modernization/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
name: atmos-modernization
description: "Atmos Modernization: migrate deprecated or legacy Atmos patterns to current names, Native CI, Atmos Pro drift detection, dependencies.components, name_template, and declared secrets"
description: "Atmos Modernization: migrate deprecated or legacy Atmos patterns to current names, Native CI, Atmos Pro drift detection, settings.pro to top-level pro, dependencies.components, name_template, and declared secrets"
metadata:
copyright: Copyright Cloud Posse, LLC 2026
version: "1.0.0"
version: "1.1.0"
---

# Atmos Modernization
Expand All @@ -26,7 +26,9 @@ the umbrella term for replacing legacy patterns with supported, current patterns
| Manual `atmos toolchain install <tool>` preinstall steps for Atmos-owned tools | Declarative `dependencies.tools` at the owning component, workflow, hook, or custom command |
| Large inline workflow/custom-command shell scripts, repeated `echo`, shell loops, ad hoc sleeps | Native step types such as `atmos`, `toast`, `table`, `parallel`, `matrix`, `wait`, `container`, `emulator`, and `http` |
| Hand-rolled scheduled drift GitHub Actions | Atmos Pro drift detection |
| `cloudposse/github-action-atmos-terraform-drift-*` | `settings.pro.drift_detection` plus `atmos terraform plan --upload-status` |
| `cloudposse/github-action-atmos-terraform-drift-*` | `pro.drift_detection` plus `atmos terraform plan --upload-status` |
| `settings.pro` in `atmos.yaml` (CLI connection config: `base_url`/`token`/`workspace_id`/etc.) | Top-level `pro:` in `atmos.yaml` (`settings.pro` remains supported as a deprecated alias) |
| `settings.pro.*` per component/stack (`enabled`/`drift_detection`/`pull_request`/`release`/`merge_group`) | Top-level `pro:` component section, a sibling of `vars:`/`metadata:`/`settings:` (`settings.pro` remains supported as a deprecated alias) |
| Secret values through raw store calls | Declared `secrets.vars` plus `!secret` |
| Legacy hook event spelling | Modern dotted lifecycle events such as `after.terraform.plan` |
| Static GitHub tokens in URLs | Atmos Auth `github/sts` through Atmos Pro |
Expand Down Expand Up @@ -109,14 +111,14 @@ configuration and staged migration, use the vendoring

## Drift Direction

Atmos Pro is the product path for drift detection. Enable drift per stack/component:
Atmos Pro is the product path for drift detection. Enable drift per stack/component using the
top-level `pro:` component section (`settings.pro:` is a deprecated alias):

```yaml
settings:
pro:
pro:
enabled: true
drift_detection:
enabled: true
drift_detection:
enabled: true
```

Then upload plan status:
Expand Down
102 changes: 64 additions & 38 deletions agent-skills/skills/atmos-pro/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
name: atmos-pro
description: "Atmos Pro setup and workflows: settings.pro, GitHub OIDC, affected and inventory uploads, stack locks, pro commit, workflow dispatch, merge queues, and drift detection"
description: "Atmos Pro setup and workflows: pro, GitHub OIDC, affected and inventory uploads, stack locks, pro commit, workflow dispatch, merge queues, drift detection, and the CI PR-comment Pro badge"
metadata:
copyright: Copyright Cloud Posse, LLC 2026
version: "1.0.0"
version: "1.1.0"
---

# Atmos Pro
Expand All @@ -14,6 +14,13 @@ dispatch, stack locking, GitHub App commits, merge queues, and drift detection.
Atmos Pro is the control plane for visibility and coordination. Atmos CLI remains the execution
layer for plans, applies, deploys, auth, toolchains, and stack resolution.

`atmos pro` is a top-level CLI command group, so its config lives at the top-level `pro:` key in
`atmos.yaml` and, per-component/stack, at the top-level `pro:` component section (a sibling of
`vars:`/`metadata:`/`settings:`). `settings.pro` is a **deprecated alias** for both — Atmos still
reads it, but an explicit top-level `pro:` block always wins over `settings.pro:` when both are
set. Recommend the top-level form in new configs; don't rewrite working `settings.pro` configs
just to modernize them.

## Related Skills

| Need | Load |
Expand All @@ -28,9 +35,8 @@ layer for plans, applies, deploys, auth, toolchains, and stack resolution.
Configure the workspace ID in `atmos.yaml` or `ATMOS_PRO_WORKSPACE_ID`. It is not a secret.

```yaml
settings:
pro:
workspace_id: "your-workspace-id"
pro:
workspace_id: "your-workspace-id"
```

In GitHub Actions, grant OIDC permission. Do not use static API keys for normal GitHub Actions
Expand Down Expand Up @@ -62,33 +68,32 @@ the Atmos Pro GitHub App do.

## Workflow Dispatch

Atmos Pro reads `settings.pro` from uploaded stack/component payloads and dispatches GitHub
workflows server-side for matching repository events.
Atmos Pro reads the component/stack `pro:` section (or the deprecated `settings.pro:` alias) from
uploaded payloads and dispatches GitHub workflows server-side for matching repository events.

```yaml
settings:
pro:
enabled: true
pull_request:
synchronize:
workflows:
atmos-terraform-plan.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
merged:
workflows:
atmos-terraform-apply.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
merge_group:
checks_requested:
workflows:
atmos-terraform-plan.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
pro:
enabled: true
pull_request:
synchronize:
workflows:
atmos-terraform-plan.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
merged:
workflows:
atmos-terraform-apply.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
merge_group:
checks_requested:
workflows:
atmos-terraform-plan.yaml:
inputs:
component: "{{ .atmos_component }}"
stack: "{{ .atmos_stack }}"
```

Use `pull_request.synchronize` for plan-on-change. Use `pull_request.merged` for the conventional
Expand All @@ -103,16 +108,14 @@ GitHub Actions or deprecated drift wrapper actions for new projects.
Enable drift detection per stack/component:

```yaml
settings:
pro:
pro:
enabled: true
drift_detection:
enabled: true
drift_detection:
enabled: true
```

Effective drift detection also requires the instance to be Pro-enabled. `metadata.enabled: false`
or `settings.pro.enabled: false` disables drift dispatch regardless of
`settings.pro.drift_detection.enabled`.
or `pro.enabled: false` disables drift dispatch regardless of `pro.drift_detection.enabled`.

Upload plan status from the workflow that Atmos Pro dispatches:

Expand All @@ -122,6 +125,21 @@ atmos terraform plan vpc -s prod-use1 --upload-status

Atmos Pro interprets plan/apply status server-side and coordinates drift visibility and remediation.

## CI PR-Comment Pro Badge

Native CI's `plan`/`apply`/`test` PR comments include a Pro status badge in the same row as the
result badges, reflecting the same effective enabled state as drift dispatch and
`atmos list instances --upload`:

- **Green (`PRO-ENABLED`)** — the component is effectively Pro-enabled. Links to the
[Atmos Pro dashboard](https://atmos-pro.com/dashboard).
- **Silver (`PRO-DISABLED`)** — the component is not Pro-enabled (no `pro:`/`settings.pro:` block,
`pro.enabled: false`, or `metadata.enabled: false`). Links to
[atmos-pro.com](https://atmos-pro.com).

No separate configuration is needed — the badge follows whatever `pro:`/`settings.pro:` the
component already has.

## GitHub STS

For private GitHub modules, vendoring, component `source`, or remote `import`, configure the
Expand All @@ -130,10 +148,18 @@ Pro mint short-lived GitHub App installation tokens in CI without storing long-l

## Troubleshooting

- If uploads fail authentication, verify `id-token: write`, `settings.pro.workspace_id` or
- If uploads fail authentication, verify `id-token: write`, `pro.workspace_id` or
`ATMOS_PRO_WORKSPACE_ID`, and that the Atmos Pro GitHub App is installed.
- If uploads return 403, verify the repository is imported into the Atmos Pro workspace.
- If merge queue checks do not resolve, verify `merge_group.checks_requested` and that
`atmos describe affected --upload` runs on `merge_group` events.
- If drift is not dispatched, verify `settings.pro.enabled`, `settings.pro.drift_detection.enabled`,
- If drift is not dispatched, verify `pro.enabled`, `pro.drift_detection.enabled`,
and that the instance appears in `atmos list instances --upload`.
- If `atmos.yaml` sets both `pro:` and `settings.pro:`, each field falls back independently: a
`pro.<field>` set at the top level wins; a field left unset there still falls back to
`settings.pro.<field>`. A stray `settings.pro:` field left behind after a partial migration can
still take effect for any field the top-level `pro:` block leaves unset; check both.
- If a stack/component config sets both `pro:` and `settings.pro:`, the top-level `pro:` block wins
outright as a whole block — it is not merged field-by-field with `settings.pro:`. A stray
`settings.pro:` left behind after a partial migration is ignored entirely once a local `pro:`
block exists on the same component; check both.
2 changes: 1 addition & 1 deletion cmd/auth/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func prepareAuthenticatedEnv(cmd *cobra.Command, v *viper.Viper) (*authExecConte
}

// Capture the global revoke_on_exit default for command-end teardown.
revokeOnExitDefault := atmosConfig.Settings.Pro.GitSTS.RevokeOnExit
revokeOnExitDefault := atmosConfig.Pro.GitSTS.RevokeOnExit

// Try to use cached credentials first (passive check, no prompts).
// Only authenticate if cached credentials are not available or expired.
Expand Down
2 changes: 1 addition & 1 deletion cmd/mcp/client/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func noServersConfiguredMessage(mcpEnabled bool) string {
// a renamed entry (via --name) doesn't trigger a false nudge. Returns "" when
// there's nothing to suggest.
func atmosProNudge(atmosConfig *schema.AtmosConfiguration) string {
if atmosConfig.Settings.Pro.WorkspaceID == "" {
if atmosConfig.Pro.WorkspaceID == "" {
return ""
}
preset, ok := mcpconfig.ResolvePreset(mcpconfig.PresetAtmosPro)
Expand Down
6 changes: 3 additions & 3 deletions cmd/mcp/client/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ func TestAtmosProNudge(t *testing.T) {
{
name: "workspace id configured, atmos-pro not added, nudges",
atmosConfig: &schema.AtmosConfiguration{
Settings: schema.AtmosSettings{Pro: schema.ProSettings{WorkspaceID: "ws-123"}},
Pro: schema.ProSettings{WorkspaceID: "ws-123"},
},
wantEmpty: false,
},
{
name: "workspace id configured, atmos-pro already added by default name, no nudge",
atmosConfig: &schema.AtmosConfiguration{
Settings: schema.AtmosSettings{Pro: schema.ProSettings{WorkspaceID: "ws-123"}},
Pro: schema.ProSettings{WorkspaceID: "ws-123"},
MCP: schema.MCPSettings{Servers: map[string]schema.MCPServerConfig{
"atmos-pro": {Type: schema.MCPTransportHTTP, URL: "https://atmos-pro.com/mcp"},
}},
Expand All @@ -65,7 +65,7 @@ func TestAtmosProNudge(t *testing.T) {
{
name: "workspace id configured, atmos-pro added under a renamed key, no nudge (matched by URL)",
atmosConfig: &schema.AtmosConfiguration{
Settings: schema.AtmosSettings{Pro: schema.ProSettings{WorkspaceID: "ws-123"}},
Pro: schema.ProSettings{WorkspaceID: "ws-123"},
MCP: schema.MCPSettings{Servers: map[string]schema.MCPServerConfig{
"my-pro-server": {Type: schema.MCPTransportHTTP, URL: "https://atmos-pro.com/mcp"},
}},
Expand Down
7 changes: 7 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ var (
ErrInvalidComponentVars = errors.New("invalid component vars section")
ErrInvalidComponentLocals = errors.New("invalid component locals section")
ErrInvalidComponentSettings = errors.New("invalid component settings section")
ErrInvalidComponentPro = errors.New("invalid component pro section")
ErrInvalidComponentEnv = errors.New("invalid component env section")
ErrInvalidComponentProviders = errors.New("invalid component providers section")
ErrInvalidComponentRequiredProviders = errors.New("invalid component required_providers section")
Expand All @@ -499,6 +500,7 @@ var (
ErrInvalidComponentOverrides = errors.New("invalid component overrides section")
ErrInvalidComponentOverridesVars = errors.New("invalid component overrides vars section")
ErrInvalidComponentOverridesSettings = errors.New("invalid component overrides settings section")
ErrInvalidComponentOverridesPro = errors.New("invalid component overrides pro section")
ErrInvalidComponentOverridesEnv = errors.New("invalid component overrides env section")
ErrInvalidComponentOverridesAuth = errors.New("invalid component overrides auth section")
ErrInvalidComponentOverridesCommand = errors.New("invalid component overrides command attribute")
Expand Down Expand Up @@ -624,6 +626,7 @@ var (
// Global/Stack-level section errors.
ErrInvalidVarsSection = errors.New("invalid vars section")
ErrInvalidSettingsSection = errors.New("invalid settings section")
ErrInvalidProSection = errors.New("invalid pro section")
ErrInvalidEnvSection = errors.New("invalid env section")
ErrInvalidGenerateSection = errors.New("invalid generate section")
ErrInvalidDependenciesSection = errors.New("invalid dependencies section")
Expand All @@ -650,6 +653,7 @@ var (
ErrInvalidTerraformCommand = errors.New("invalid terraform command")
ErrInvalidTerraformVars = errors.New("invalid terraform vars section")
ErrInvalidTerraformSettings = errors.New("invalid terraform settings section")
ErrInvalidTerraformPro = errors.New("invalid terraform pro section")
ErrInvalidTerraformEnv = errors.New("invalid terraform env section")
ErrInvalidTerraformProviders = errors.New("invalid terraform providers section")
ErrInvalidTerraformGenerateSection = errors.New("invalid terraform generate section")
Expand All @@ -669,6 +673,7 @@ var (
ErrInvalidHelmfileCommand = errors.New("invalid helmfile command")
ErrInvalidHelmfileVars = errors.New("invalid helmfile vars section")
ErrInvalidHelmfileSettings = errors.New("invalid helmfile settings section")
ErrInvalidHelmfilePro = errors.New("invalid helmfile pro section")
ErrInvalidHelmfileEnv = errors.New("invalid helmfile env section")
ErrInvalidHelmfileAuth = errors.New("invalid helmfile auth section")
ErrInvalidHelmfileDependencies = errors.New("invalid helmfile dependencies section")
Expand All @@ -688,6 +693,7 @@ var (
ErrInvalidPackerCommand = errors.New("invalid packer command")
ErrInvalidPackerVars = errors.New("invalid packer vars section")
ErrInvalidPackerSettings = errors.New("invalid packer settings section")
ErrInvalidPackerPro = errors.New("invalid packer pro section")
ErrInvalidPackerEnv = errors.New("invalid packer env section")
ErrInvalidPackerAuth = errors.New("invalid packer auth section")
ErrInvalidPackerDependencies = errors.New("invalid packer dependencies section")
Expand All @@ -700,6 +706,7 @@ var (
ErrInvalidAnsibleCommand = errors.New("invalid ansible command")
ErrInvalidAnsibleVars = errors.New("invalid ansible vars section")
ErrInvalidAnsibleSettings = errors.New("invalid ansible settings section")
ErrInvalidAnsiblePro = errors.New("invalid ansible pro section")
ErrInvalidAnsibleEnv = errors.New("invalid ansible env section")
ErrInvalidAnsibleAuth = errors.New("invalid ansible auth section")
ErrInvalidAnsibleDependencies = errors.New("invalid ansible dependencies section")
Expand Down
12 changes: 5 additions & 7 deletions internal/exec/describe_affected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2476,12 +2476,10 @@ func TestUploadShowsOutputWhenOutputFileRequested(t *testing.T) {
var gotFormat, gotFile string
var uploadReq dtos.UploadAffectedStacksRequest
atmosConfig := &schema.AtmosConfiguration{
Settings: schema.AtmosSettings{
Pro: schema.ProSettings{
BaseURL: "http://placeholder.invalid",
Endpoint: "api/v1",
Token: "test-token",
},
Pro: schema.ProSettings{
BaseURL: "http://placeholder.invalid",
Endpoint: "api/v1",
Token: "test-token",
},
}

Expand All @@ -2496,7 +2494,7 @@ func TestUploadShowsOutputWhenOutputFileRequested(t *testing.T) {
require.NoError(t, err)
}))
defer server.Close()
atmosConfig.Settings.Pro.BaseURL = server.URL
atmosConfig.Pro.BaseURL = server.URL

d := describeAffectedExec{
atmosConfig: atmosConfig,
Expand Down
6 changes: 5 additions & 1 deletion internal/exec/describe_affected_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
// - stack: Stack identification
// - included_in_dependents: Used in filtering logic
// - dependents: Nested stack processing (recursively stripped)
// - settings.pro: Workflow dispatch configuration
// - pro / settings.pro: Workflow dispatch configuration. settings.pro is a deprecated
// alias for the top-level pro: section; both are kept so Atmos Pro can apply the same
// new-wins-over-legacy precedence server-side (see pro.ResolveSection).
// - deleted: Marks components removed in HEAD.
// - deletion_type: Whether a component or entire stack was deleted.
//
Expand All @@ -38,6 +40,7 @@ func stripAffected(a schema.Affected) schema.Affected {
IncludedInDependents: a.IncludedInDependents,
Dependents: stripDependents(a.Dependents),
Settings: stripSettings(a.Settings),
Pro: a.Pro,
Deleted: a.Deleted,
DeletionType: a.DeletionType,
}
Expand All @@ -61,6 +64,7 @@ func stripDependent(d schema.Dependent) schema.Dependent {
IncludedInDependents: d.IncludedInDependents,
Dependents: stripDependents(d.Dependents),
Settings: stripSettings(d.Settings),
Pro: d.Pro,
}
}

Expand Down
8 changes: 8 additions & 0 deletions internal/exec/describe_affected_utils_2.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ func appendToAffected(
}
}

if includeSettings {
if i, ok2 := (*componentSection)[cfg.ProSectionName]; ok2 {
if proSection, ok3 := i.(map[string]any); ok3 {
affected.Pro = proSection
}
}
}

if affected.ComponentType == cfg.TerraformComponentType {
varSection := map[string]any{}

Expand Down
Loading
Loading