🚀 Enhancements
fix(ci): classify output-only plan summaries @osterman (#2407)
## SummaryFixes CI summary classification for output-only Terraform/OpenTofu plans on the stdout fallback path. These plans already had HasChanges=true, but the rendered summary could still use the no-change heading and badge because the template context only inferred output changes from parsed output values.
Root Cause
JSON plan parsing has structured output_changes, but stdout parsing only sees Changes to Outputs:. The parser set the changed-result text but did not preserve an explicit output-change signal for the template context.
Changes
- Add
TerraformOutputData.HasOutputChanges. - Set it from JSON plan output changes and stdout
Changes to Outputs:detection. - Use the explicit signal when building the Terraform summary template context.
- Add an anonymized end-to-end regression test for the
after.terraform.plansummary path. - Document the fix under
docs/fixes.
Validation
go test ./pkg/ci/plugins/terraform -run TestOnAfterPlan_OutputOnlyStdout_RendersOutputChangeSummary -count=1go test ./pkg/ci/plugins/terraform -run 'OutputOnly|OnAfterPlan|Template' -count=1go test ./pkg/ci/plugins/terraform -count=1- pre-commit hooks during commit, including Go build, gofumpt, tidy check, and golangci-lint
Summary by CodeRabbit
- Bug Fixes
- Fixed CI summary display for Terraform and OpenTofu plans containing output-only changes. Previously, plans that only modified output values incorrectly displayed "No Changes" heading and badge on CI summaries. They now correctly display "Output Changes" heading with appropriate messaging.
fix(profile,auth): list table truncation, alignment, and active-profile indicator @osterman (#2365)
## what- Profile list table now shows every configured profile instead of just the first row, and flags the currently-active profile with a green dot resolved from
--profile/ATMOS_PROFILE/profiles.default. - Auth list table (providers + identities) gets the same row-truncation fix, so
atmos auth list --format=tableno longer drops rows. - Profile-fallback path made viable from
terraform:TerraformPreHooknow offers the profile-switch fallback (matchingatmos auth login/exec/shell/env/console/whoami) when the loaded config has no usable auth, and--profileis propagated to auth subcommand config init so the re-exec re-loads the picked profile's identities. profiles.base_pathsynced to global viper inLoadConfigso the auth profile fallback can discover profiles in custom locations.- New exported
cfg.GetActiveProfiles(*schema.AtmosConfiguration)helper centralises the resolution order used by both the renderer and any future callers. - Regression tests added in
pkg/profile/list,pkg/auth/list,pkg/config,pkg/auth, andcmd/auth_config_test.go.
why
bubbles/tablereserves one of the configuredWithHeight()lines for the header — passinglen(rows)renderedlen(rows)-1data rows. With 2 profiles, only 1 was visible. The data layer (--format=json) was always correct, so the bug was purely in the table renderer at three call sites.- After fixing height, the cursor row (row 0) was rendered with a blue highlight from bubbles' default
Selectedstyle. Aliasings.Selected = s.Cellremoved the highlight but re-appliedPadding(0,1)to the whole joined row, shifting the cursor row 1 column right of every other row. Using an emptylipgloss.NewStyle()removes the highlight without the double-padding. - The active-profile dot was being mangled into
…in TTY output because bubbles'renderRowcallsrunewidth.Truncate(value, width, "…"), andrunewidthcounts ANSI escape bytes as visible width: a pre-styled\x1b[…m●\x1b[0mreports width ~8 and gets truncated to a single…. Putting a plain●in the cell and post-styling afterView()returns sidesteps the bug. - The Terraform pre-hook surfaced a bare "no default identity" error in the exact scenarios the auth commands' fallback already handles. Wiring the same fallback in keeps the UX consistent.
- Without syncing
profiles.base_pathto the global viper, the fallback could not see profiles in user-configured locations and silently returned no candidates.
references
- Symptom: running
atmos profile listin a directory with multiple profiles displayed only the first one in the table view (JSON output was unaffected).
Summary by CodeRabbit
-
New Features
- Profile list now displays active profiles with a visual indicator (●).
- Added support for default profiles in configuration.
- Profile fallback suggestions when authentication configuration is incomplete.
-
Bug Fixes
- Fixed table rendering to properly display all rows with multiple entries.
- Eliminated duplicate help flag output during configuration loading.
- Improved profile resolution for edge cases in flag parsing.
fix(ci): use active atmos identity for S3 planfile uploads @calxus (#2372)
## what- Make the S3 planfile store authenticate via the active Atmos auth identity instead of always falling back to the AWS SDK default credential chain.
- Add an
IdentityAwareBackendinterface topkg/ci/artifactso the registry can inject anAuthContextResolver(mirroring the existingpkg/storepattern). - Refactor the S3 backend to defer client initialization when an identity is configured and resolve credentials lazily via the injected resolver.
- Wire
ci.createPlanfileStoreto propagateinfo.Identity(the resolved--identity/ATMOS_IDENTITYvalue) and a resolver built frominfo.AuthManager. - Add unit tests covering registry resolver injection, S3 lazy init paths, and
attachIdentityprecedence.
why
atmos terraform planwas succeeding in CI (the Terraform subprocess inherits the prepared environment) but the subsequent S3 planfile upload failed with IMDS errors because the upload runs in the parent atmos process, which had no awareness of the active identity.- Common CI setups assume GitHub OIDC + IAM role assumption is enough — the parent process has no ambient AWS credentials to fall back on, so the failure surfaced as a confusing AWS-side error rather than an Atmos config issue.
- The fix reuses the same identity resolver pattern already used by SSM / Azure Key Vault / GSM stores, so AWS-store auth wiring stays consistent across the codebase.
- When no identity is in scope, behavior is unchanged: the default credential chain is still used, so existing setups that relied on ambient credentials are unaffected.
references
- closes #2369
- Mirrors the identity-aware store pattern in
pkg/store/aws_ssm_param_store.goandpkg/store/authbridge/resolver.go.
Summary by CodeRabbit
-
New Features
- Artifact storage backends now support identity-aware authentication with lazy credential resolution and the ability to attach or override an auth identity/resolver after store creation.
-
Tests
- Added tests validating identity propagation, deferred auth initialization, resolver invocation semantics, and correct error handling when auth resolution fails.
fix(ci): wire PR comment posting into terraform plan hook @joshAtRula (#2405)
Resolves the regression reported in #2367 where `ci.comments.enabled: true` had no effect: the schema field existed and `ATMOS_CI_COMMENTS_ENABLED` was wired, but no code path ever called the GitHub API to create or update a PR comment. The terraform plugin now posts a PR comment on `after.terraform.plan` whenever comments are enabled and the event is a pull request.Interface changes:
- Add
Provider.PostComment(ctx, *PostCommentOptions) (*Comment, error)to the CI provider interface, along withCommentBehavior(create / update / upsert) and result types. pkg/ci/providers/github/comments.go: upsert via HTML marker<!-- atmos:ci:{command}:{component}:{stack} -->usingIssues.ListComments+EditComment/CreateComment. 403/404 errors carry hints directing users to thepermissions: pull-requests: writeworkflow grant. Pagination-aware marker scan.- Generic provider returns
ErrCIOperationNotSupported.
Plugin wiring:
pkg/ci/plugins/terraform/comments.go: newpostComment()handler reuses the summary rendered bywriteSummary()soci.templates.terraform.planoverrides apply to both the job summary and the PR comment body.- Hooked into
onAfterPlanas warn-only (apply/deploy intentionally excluded for this fix — plan-only scope). - Skips silently when the event is not a PR, repo context is incomplete, or the rendered summary is empty.
Schema:
CICommentsConfig.Enabledchanged fromboolto*boolso nil (unset) can default off without colliding with explicitfalse. This matches the siblingCIChecksConfig.Enabledand prevents upgrading installations from silently starting to post comments.ATMOS_CI_COMMENTS_ENABLEDenv override updated for pointer-bool semantics; tests extended to cover the unset-nil path.
Errors:
- Add
ErrCICommentPostFailed,ErrCICommentListFailed,ErrCICommentUpdateFailed,ErrCICommentNotFound. - Incidentally convert 4 pre-existing dynamic
fmt.Errorfsites inpkg/config/utils.go:GetContextPrefixto wrap a newErrStackNamePatternPartMissingsentinel; gofumpt reformatted those lines, which pulled them into lint's new-from-rev diff. Error message text is preserved so existing tests still match.
Tests:
- 13 httptest-backed tests for the GitHub comments API covering upsert create, upsert update (marker matches), create-always, update-returns- not-found, input validation, 403/404 hint propagation, pagination, and default behavior.
- 8 handler tests covering enabled + PR present, skip when disabled, skip when unset (nil), skip when no PR, skip when no repo context, warn-only on API failure, and behavior-flag respect.
registry_provider_testandhandlers_testmockProviderupdated for the new interface method.
Docs:
docs/prd/native-ci/providers/github/pr-comments.md: status flipped from Deferred to Shipped with locked-in decisions.implementation-status.md: PR Comments row now Done; v14.0 changelog entry added; totals updated to 148/151.
Closes #2367
what
- Wires up PR comment posting for the native CI integration, which was previously schema-only (
ci.comments.enabled: truedid nothing). - Adds
Provider.PostCommentto the CI provider interface with create/update/upsert semantics. - Implements the GitHub provider's comment API (
pkg/ci/providers/github/comments.go) using HTML-marker-based upsert (<!-- atmos:ci:{command}:{component}:{stack} -->) so repeat runs update the existing comment instead of stacking new ones. - Hooks comment posting into
onAfterPlan(plan-only, as agreed) and reuses the already-rendered summary soci.templates.terraform.planoverrides apply to both the GitHub job summary and the PR comment body. - Changes
CICommentsConfig.Enabledfromboolto*boolso unset (nil) defaults to off and upgrading installations don't silently start posting comments. - Returns actionable hints on 403/404 errors that point users to the missing
permissions: pull-requests: writeworkflow grant. - Adds 4 new sentinel errors (
ErrCICommentPostFailed,ErrCICommentListFailed,ErrCICommentUpdateFailed,ErrCICommentNotFound). - Incidentally: converts 4 pre-existing dynamic
fmt.Errorfsites inpkg/config/utils.go:GetContextPrefixto a newErrStackNamePatternPartMissingsentinel — error message text preserved so existing tests still match.
why
- Customer-facing regression in v1.216.0: the docs at https://atmos.tools/cli/configuration/ci/comments promise PR comments, and the schema field +
ATMOS_CI_COMMENTS_ENABLEDenv var suggest the feature works, but no code path ever called the GitHub API. Users configuringci.comments.enabled: truein apull_requestworkflow see the job summary render correctly but no comment on the PR. - The PRD (docs/prd/native-ci/providers/github/pr-comments.md) explicitly marked the design as "Deferred" and
implementation-status.mdtracked it as "Not Started". This PR locks in the design decisions and ships the feature. *boolforEnabledmatches the pattern already used byCIChecksConfig.Enabledand gives us room to default off without breaking explicitfalsevalues — preventing silent behavior change on upgrade.- Reusing the rendered summary for the comment body (rather than rendering a second template) keeps user template customization coherent: one override point (
ci.templates.terraform.plan) controls what shows up in both surfaces. - 403/404 hinting mirrors the pattern already established in
checks.gofor the commit status API, since the permissions failure mode is identical and common.
scope
- Plan-only, deliberately.
applyanddeploydo not post comments. Review happens on the PR thread for plans; apply/deploy outcomes live in the checks pane. Revisit if users ask. - Custom
ci.comments.templateoverride is still a follow-up — the comment currently reuses the summary template. The schema field is preserved so this can be wired later without another breaking change.
testing
go test -race ./pkg/ci/... ./pkg/config/... ./pkg/schema/... ./errors/...— 2308 tests pass across 19 packages.- 13 new httptest-backed tests for
pkg/ci/providers/github/comments.gocover upsert create/update, create-always, update-returns-not-found, input validation, 403/404 hint propagation, pagination across list pages, and default-behavior fallback. - 8 new handler tests cover enabled + PR present (posts), disabled (skips), unset/nil (skips — upgrade safety), no PR event (skips), no repo context (skips), API failure (warn-only, handler still returns nil), and behavior flag respected.
pre-commit runis clean: go-fumpt, go build, go.mod tidy, golangci-lint config verify, golangci-lint, gomodcheck, CLAUDE.md size, trailing whitespace, end-of-files, yaml, large files — all pass.
references
- closes #2367
- PRD: docs/prd/native-ci/providers/github/pr-comments.md (status flipped Deferred → Shipped in this PR)
- Implementation tracker: docs/prd/native-ci/framework/implementation-status.md (PR Comments row now Done; v14.0 changelog entry added)
- Docs: cli/configuration/ci/comments (no content changes — behavior now matches the doc)
- Precedent for marker-based upsert: tfcmt
Summary by CodeRabbit
-
New Features
- Opt-in PR comment posting for CI plan runs on GitHub with marker-based updates; defaults to disabled.
-
Providers
- GitHub provider: create/update/upsert comment behaviors implemented; generic provider reports comments unsupported.
-
Configuration
- CI comments setting changed to tri-state (unset vs true/false), with env var support to explicitly set.
-
Tests
- Extensive tests covering comment behaviors and handler wiring.
-
Documentation
- Docs and implementation status updated to mark PR comments as shipped.
fix(auth/eks): suppress success line for no-op kubeconfig writes @mtb-xt (#2402)
## what - `WriteClusterConfig` now returns `(changed bool, err error)` and detects no-op writes by comparing the serialized kubeconfig against the on-disk file. - `EKSIntegration.Execute` only emits `ui.Success` when the kubeconfig actually changed; otherwise it logs at debug. - The explicit `atmos aws eks update-kubeconfig --integration` command keeps its unconditional success message — the user asked for it explicitly there. - Added three tests: merge no-op, replace no-op, and merge with a changed field (to confirm `changed` flips back to true for real updates).why
Auto-provisioned EKS integrations re-run on every identity resolution. In a single atmos workflow ... (or template lookup, or !terraform.output evaluation), that can mean hundreds of identity authentications back-to-back, each printing:
✓ EKS kubeconfig: platform-apse2-dev → /home/.../.config/atmos/kube/config
The repeated lines bury real output and errors (one user reported the output completely hiding a credential-chain failure under thousands of identical success lines). The on-disk file was almost always already correct after the first write, so the success line was misleading on top of being noisy.
Now you see the success line once, the first time the kubeconfig is actually written or modified, and quiet debug logs the rest of the time. This also avoids unnecessary mtime / chmod churn on the kubeconfig file.
references
- Originating discussion: user observed 1000+
✓ EKS kubeconfig: ...lines from a singleatmos workflow plan/dev -f bedrockinvocation. - Code touchpoints:
pkg/auth/cloud/kube/config.go—WriteClusterConfig, newwriteIfChanged/mergeIfChanged/configsEqualhelpers.pkg/auth/integrations/aws/eks.go—EKSIntegration.Executegate.cmd/aws/eks/update_kubeconfig_sdk.go— caller update (still always prints success for the explicit command).
test plan
-
go test ./pkg/auth/cloud/kube/... ./pkg/auth/integrations/aws/...passes. - New tests cover merge no-op, replace no-op, and field-change → changed=true.
- No-op call confirmed to leave the file mtime untouched.
- Manual: run
atmos workflow ...against a stack with auto-provisionedaws/eksintegrations and confirm the success line appears once instead of per-component.
🤖 Generated with Claude Code
Summary by CodeRabbit
-
Bug Fixes
- Kubeconfig writes now detect structurally identical content and skip unnecessary disk writes.
- File permission drift is reconciled without rewriting unchanged content.
- CLI success output is shown only when the kubeconfig was actually modified; no-op runs report the config is already up to date (debug).
- Preserves third-party kubeconfig entries when managing clusters.
-
Tests
- Added coverage for no-op merge/replace, permission-reconcile without content change, visible-field change detection, multi-cluster rewrite behavior, and preservation of third-party kubeconfig entries.