Skip to content

feat(cloudformation): migration guide, inline templates, and field-test fixes [EXPERIMENTAL] - #3002

Draft
Erik Osterman (Cloud Posse) (osterman) wants to merge 18 commits into
osterman/cfn-phase3-stacksets-observabilityfrom
osterman/cfn-phase4-migration-graduation
Draft

feat(cloudformation): migration guide, inline templates, and field-test fixes [EXPERIMENTAL]#3002
Erik Osterman (Cloud Posse) (osterman) wants to merge 18 commits into
osterman/cfn-phase3-stacksets-observabilityfrom
osterman/cfn-phase4-migration-graduation

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Aug 26, 2026

Copy link
Copy Markdown
Member

what

  • Phase 4 (final layer) of the native aws/cloudformation component type: backend management
    (backend create/describe/update/delete/list) for the S3 artifact bucket CloudFormation
    packaging uses, and a migration guide for Rain/raw-CloudFormation users.
  • A full field-test pass across the whole feature, with fixes for every finding: bulk-selection
    infinite recursion (shared by every bulk-capable component type), logs stdout routing, missing
    confirmation gates, changeset/diff leaks, error-sentinel misuse, and small doc drift.
  • Real backend auto-provisioning (provision.backend.enabled: true now actually provisions,
    instead of requiring a manual backend create first).
  • Inline CloudFormation templates: template: is now the inline template body (string or
    structured map, flowing through Atmos's own {{ }} templating pipeline), and a new path: key
    takes over the file-reference role template: used to have.
  • Fixes a missing identity EndpointURL override in the shared pkg/ci/artifact/s3 store that
    broke template uploads against emulated AWS endpoints (e.g. Floci).
  • Fixes aws cfn/helm/kubernetes duplicating the entire global CLI flag set as local
    persistent flags (found via this session's --help output investigation).
  • Makes --labels repeatable across every command family that has it (kubernetes, helm,
    container, workflow, terraform, vendor, list, cloudformation), matching --tags' existing
    repeatable-flag behavior.

why

  • This is the layer that makes the feature real for users: end-to-end backend lifecycle
    management, a documented migration path off Rain, and every rough edge found during a real,
    adversarial DX test pass fixed before the feature is announced.
  • Inline templates and the duplicate-flag-registration fix are genuine capability/DX gains
    discovered during this pass, not originally scoped work.

references

  • Individual fixes are each documented under docs/fixes/.
  • Migration guide: website/docs/migration/from-rain.mdx
  • Closes out the 6-PR stack starting at cloudformation-component-prd.

@atmos-pro

atmos-pro Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@github-actions github-actions Bot added the size/xl Extra large size PR label Aug 26, 2026
@osterman Erik Osterman (Cloud Posse) (osterman) changed the title osterman/cfn phase4 migration graduation feat(cloudformation): migration guide, inline templates, and field-test fixes [EXPERIMENTAL] Aug 26, 2026
@osterman Erik Osterman (Cloud Posse) (osterman) added the minor New features that do not break anything label Aug 26, 2026
@osterman
Erik Osterman (Cloud Posse) (osterman) force-pushed the osterman/cfn-phase4-migration-graduation branch from 5542812 to 1c5a72c Compare August 27, 2026 11:39
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@github-actions

Copy link
Copy Markdown

Resource Changes Found for bucket in test

Atmos CI

create

Plan: 4 to add, 0 to change, 0 to destroy.
To reproduce this locally, run:

atmos terraform plan bucket -s test

Create

+ aws_s3_bucket.checkov_target
+ aws_s3_bucket.this
+ aws_s3_bucket.trivy_target
+ aws_s3_bucket_public_access_block.trivy_target
Terraform Plan Summary
  # aws_s3_bucket.checkov_target will be created
  + resource "aws_s3_bucket" "checkov_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-checkov-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.this will be created
  + resource "aws_s3_bucket" "this" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + tags_all                    = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.trivy_target will be created
  + resource "aws_s3_bucket" "trivy_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-trivy-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket_public_access_block.trivy_target will be created
  + resource "aws_s3_bucket_public_access_block" "trivy_target" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + restrict_public_buckets = true
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = "atmos-native-ci-e2e-test"

@osterman
Erik Osterman (Cloud Posse) (osterman) force-pushed the osterman/cfn-phase4-migration-graduation branch from 3ab568d to 39f7b60 Compare August 27, 2026 15:43
@osterman
Erik Osterman (Cloud Posse) (osterman) force-pushed the osterman/cfn-phase4-migration-graduation branch from 39f7b60 to 1e61d70 Compare August 27, 2026 16:00
@github-actions github-actions Bot removed the size/xl Extra large size PR label Aug 27, 2026
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.35673% with 91 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.65%. Comparing base (e108500) to head (835c153).

Files with missing lines Patch % Lines
cmd/aws/cloudformation/backend/backend_helpers.go 81.96% 18 Missing and 4 partials ⚠️
cmd/aws/cloudformation/backend/backend_create.go 85.22% 8 Missing and 5 partials ⚠️
cmd/aws/cloudformation/backend/backend_delete.go 86.30% 5 Missing and 5 partials ⚠️
cmd/aws/cloudformation/backend/backend_describe.go 85.71% 5 Missing and 5 partials ⚠️
cmd/aws/cloudformation/backend/backend_list.go 82.75% 5 Missing and 5 partials ⚠️
cmd/aws/cloudformation/backend/backend_update.go 83.67% 4 Missing and 4 partials ⚠️
cmd/aws/cloudformation/backend/flag_values.go 74.19% 5 Missing and 3 partials ⚠️
cmd/aws/cloudformation/cloudformation.go 93.33% 2 Missing ⚠️
pkg/component/aws/cloudformation/executor.go 88.88% 1 Missing and 1 partial ⚠️
pkg/component/aws/cloudformation/spec.go 89.47% 1 Missing and 1 partial ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@                               Coverage Diff                               @@
##           osterman/cfn-phase3-stacksets-observability    #3002      +/-   ##
===============================================================================
+ Coverage                                        83.61%   83.65%   +0.03%     
===============================================================================
  Files                                             1975     1984       +9     
  Lines                                           192295   193027     +732     
===============================================================================
+ Hits                                            160797   161471     +674     
- Misses                                           23489    23511      +22     
- Partials                                          8009     8045      +36     
Flag Coverage Δ
unittests 83.65% <89.35%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/aws/cloudformation/backend/backend.go 100.00% <100.00%> (ø)
cmd/container/container.go 77.27% <100.00%> (ø)
cmd/container/verbs.go 88.40% <100.00%> (ø)
cmd/helm/helm.go 94.41% <100.00%> (ø)
cmd/kubernetes/kubernetes.go 93.50% <100.00%> (ø)
cmd/list/components.go 83.88% <100.00%> (ø)
cmd/list/dependencies.go 91.11% <100.00%> (ø)
cmd/list/flag_wrappers.go 100.00% <100.00%> (ø)
cmd/list/instances.go 71.42% <100.00%> (ø)
cmd/list/metadata.go 64.93% <100.00%> (ø)
... and 42 more

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Closes a real Phase 1 PRD gap surfaced by a completeness audit: the artifact
bucket used by template packaging (kind: aws/s3 provision targets) had no
CLI-managed lifecycle, unlike Terraform's equivalent atmos terraform backend
verb-for-verb clone this PRD explicitly called for.

pkg/provisioner/backend's ProvisionBackend/DeleteBackendWithParams read
Terraform's own top-level backend_type/backend stack-manifest shape, not
CFN's provision.targets.<name>: {kind: aws/s3, ...} shape. Rather than
teaching the shared provisioner layer a second config shape (which would
break the PRD's explicit "no new backend type" promise), the new
pkg/component/aws/cloudformation/backend.go adapts entirely on the CFN side:
resolves the kind: aws/s3 target (implicit-single-or---target, same pattern
already used for stackset/packaging targets) and builds a synthetic
backend_type/backend map before handing off to the existing, real
ProvisionBackend/DeleteBackendWithParams code path — spinner and warning
handling included, unmodified.

ListBackends/DescribeBackend in the shared provisioner layer are themselves
unconditional ErrNotImplemented stubs today, even for Terraform. CFN's need
is narrower (one kind: aws/s3 target per component, not Terraform's full
backend-type matrix), so describe/list are real implementations here via a
direct S3BackendExists call rather than mirroring the stub.

Verified live against Floci: create/describe/list/update/delete all
exercised end-to-end against a real S3 bucket (confirmed independently via
raw `aws s3` calls), including delete's --force gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes the PRD's deferred Migration Guide scope. Framed as "migrating off
Rain onto Atmos," not "how to write Rain syntax in Atmos" — there is no Rain
compatibility layer, so the guide leads with resolving !Rain:: directives
(Constant/Env/Include/Embed/S3/Module) to their Atmos-native or
plain-CloudFormation equivalents before a template will deploy at all, plus
a verb cross-reference table (every right-hand-side Atmos verb checked
directly against cmd/aws/cloudformation/cloudformation.go's mounted
commands, none invented).

Two new artifacts, both linked from the agent-skills routing table and the
website's Migration Guides sidebar: the agent-facing skill reference
(agent-skills/skills/atmos-migration/references/from-rain.md) and the
user-facing Crawl/Walk/Run guide (website/docs/migration/from-rain.mdx).

Corrects an assumption from earlier in the PRD: template packaging (kind:
aws/s3 provision targets) is size-triggered only — it uploads a template
past CloudFormation's 51,200-byte inline limit, but does not rewrite local
asset references the way `aws cloudformation package`/Rain's `pkg` do. The
guide states this explicitly rather than implying full asset-aware parity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Continuation lines under numbered-list items used 3-space indentation
(aligned to the "1. " marker width), violating .editorconfig's indent_size=2
for *.md — caught by the atmos-validate-editorconfig pre-commit hook, which
scans the whole tree rather than the diff, on an unrelated later commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes a gap flagged in the example: no recorded demo, unlike its sibling
examples/emulator-aws. Mirrors emulator-aws's cast pipeline exactly (fixture
setup, recording, sanitize/validate scripts), showing the deploy/output/
delete lifecycle against a real Floci emulator.

Noted for later: pkg/component/aws/cloudformation's DescribeStackEvents poll
loop occasionally races a fast-completing create against Floci (events not
yet visible when the stack already reports CREATE_COMPLETE) — the deploy
still succeeds correctly, but recording needed a retry roughly 1-in-4 to
1-in-5 attempts to capture the full event stream. Not a functional bug (the
same race the delete-path fix earlier in this stack already handles for
DescribeStackEvents' own not-found case), but worth keeping in mind if an
automated Floci E2E test is ever written — it may need the same retry
tolerance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ooks/S3-auth fixes

Fixes the full set of findings from this session's field-test pass on aws/cloudformation
(bulk-selection infinite recursion shared by every bulk-capable component type, logs stdout
routing, error-sentinel misuse, missing confirmation gates, diff/changeset leaks, real backend
auto-provisioning, small doc drift), adds CFN inline-template support (`template:` as inline
body, new `path:` for file references, plumbed through two component-config allowlists that were
silently dropping the new key), and fixes a missing EndpointURL override in the shared
pkg/ci/artifact/s3 store that broke template uploads against emulated AWS endpoints. Live-verified
hooks fire only for diff/apply/delete, matching the code.

Each change is documented in its own docs/fixes/ entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e-templates fix-log

atmos-validate-editorconfig requires indentation in multiples of 2; the numbered-list
continuation lines used 3-space indentation (aligned to "1. "/"2. "'s width) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ainer/workflow/terraform/vendor/list

--labels was WithStringFlag (comma-separated only) while --tags was already WithStringSliceFlag
(repeatable and/or comma-separated) — an inconsistency found while fixing the same issue for
aws/cloudformation. The flag is independently duplicated (not shared) across cloudformation,
kubernetes, helm, container, workflow, terraform, vendor (x4), and list (x6) commands, so this
converts every one of them to match --tags' registration and parsing.

pkg/tags.ParseLabelsFlag now takes []string instead of string; every call site converts from
GetString("labels") to GetStringSlice("labels"), and every flag registration from
WithStringFlag("labels", ...) to WithStringSliceFlag("labels", ...). Docs updated to show the
repeatable form.

See docs/fixes/2026-08-25-labels-flag-repeatable.md for the full file list and validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the changelog post covering the whole feature (all 4 phases plus this session's
field-test fixes, inline templates, and migration guide), and links it into the extensibility
initiative's roadmap milestone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…plate

golangci-lint's add-constant rule flagged a 4th literal "%w: %w" after the
gh-stack rebase merged phase1's wrapFmt-introducing commit ahead of this
one; swap in the existing package-level constant instead of a duplicate
literal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…uild failure

The docs build's cast-validation step failed because every aws/cloudformation
doc page beyond Phase 1's 8 verbs (backend, changeset, drift, fmt, get, list,
logs, source, stackset, tree, watch) referenced a screengrab cast that was
never recorded — demo/casts/atmos.d/screengrabs/cli.yaml's command manifest
still only listed Phase 1's verbs. Added the missing 32 commands to both the
generate and validate command lists and recorded all 41 aws/cloudformation
casts via `atmos --chdir=demo/casts casts generate screengrabs cli --filter=cloudformation`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… stacks

followLogs wrote each stack's newly-fetched events immediately after
polling it, per poll iteration — printing events in poll order rather than
event-time order whenever an earlier-polled stack's events were actually
newer than a later-polled stack's. Now collects the iteration's events
across every stack, sorts by timestamp, then writes, matching the
non-follow path's existing sort.

Also tightened the aws/cloudformation JSON schema: 'template'/'path' now
reject empty values, and the object schema enforces they're mutually
exclusive (previously only checked in Go's validateComponentConfig, so an
IDE/editor validating against the schema wouldn't catch either mistake).

Found via CodeRabbit review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ws " from every usage line

cmd/aws/cloudformation/cloudformation.go's init() called both
awsCmd.AddCommand(CloudFormationCmd) (correct, in cmd/aws/aws.go) AND its own
internal.Register(&CommandProvider{}), which re-parents CloudFormationCmd
directly onto RootCmd via the top-level command registry. Cobra's AddCommand
mutates the child's parent pointer in place, so whichever call ran last won
— the registry-based one, since it runs from cmd/root.go's init() after every
imported package's own init() has already run. Two live, confirmed effects:

- Every `atmos aws cloudformation ...` --help usage line rendered
  "atmos cloudformation ..." (via cmd.CommandPath()/UseLine()), missing "aws".
- `atmos cloudformation ...` (without the aws namespace) worked as an
  undocumented, unintended duplicate top-level command, alongside the real
  `atmos aws cloudformation ...` path.

No other `aws/<service>` subcommand (eks, security, compliance, ecr) does
this self-registration — cloudformation was the only one. Removed the rogue
internal.Register call and the now-dead CommandProvider type; it was never
needed since aws.go already wires the command in correctly.

Regenerated all 41 aws/cloudformation screengrab casts to reflect the
corrected usage lines (`atmos --chdir=demo/casts casts generate screengrabs
cli --filter=cloudformation`), which also incidentally resolved the
`changeset delete --help` cast advertising a --retain-resources flag it
doesn't have (that cast was stale, predating other fixes).

Also: documented that `backend update` creates the bucket first when it
doesn't exist yet, matching `backend create`'s shared code path (the doc
previously only described it as an existing-bucket update); and updated the
backend auto-provisioning fix-log's stale Follow-ups section — the S3
identity/EndpointURL bug it flagged as unresolved was fixed the same day in
2026-08-25-artifact-s3-store-endpoint-override.md.

Found via CodeRabbit review (phase4 --dir website/--dir docs passes). --dir
internal came back clean (0 findings). The Rain --config Parameters-map
wrapping finding on from-rain.mdx and the source.uri single-file
template-inference finding on aws-cloudformation.mdx were not fixed: the
former needs verifying Rain's actual external config-file format (can't
confirm from this repo alone), and the latter's claimed behavior doesn't
exist anywhere in pkg/component/aws/cloudformation — no code derives a
template path from source.uri's basename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ntinel

The stack rebase silently auto-merged a phase3 hunk (the nil StackSetOperation
guard) referencing ErrAwsCloudFormationStackSetFailed, a sentinel dropped
during conflict resolution in favor of this branch's existing
APICallFailed/OperationFailed split — no conflict marker surfaced since the
hunk didn't textually overlap with the errors.go conflict. Caught by
`go build` immediately after the rebase, not by git.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…/list

renderBackendStatuses fell through to the table renderer for any
unrecognized format value (e.g. a typo'd --format=jsonn), the same
silently-swallowed-bad-format bug class already fixed for `output`'s
renderOutputsSummary. Now errors with ErrInvalidFlagValue, matching the
convention pkg/toolchain/list.go already uses for the same kind of check.

Found via CodeRabbit review (phase4 --dir cmd pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ypes list, fix quoting claim

- apply.mdx's Flags list was missing --base, despite the --affected example
  right above it using --base origin/main.
- The shared /cli/configuration/components overview page never listed
  AWS CloudFormation in its "Supported Component Types" dl, even though the
  YAML example below it already includes aws/cloudformation.
- The CFN configuration page claimed the "aws/cloudformation" YAML key
  "must be quoted... because it contains a /" — verified against a real
  YAML parser: a bare '/' does not require quoting in YAML. Reworded to
  describe the quoting as a documentation convention, not a requirement.

Found via CodeRabbit review (phase1 pass). A pkg/datafetcher/schema/stacks/
stack-config/1.0.json finding (add secrets to the CFN component manifest)
was not fixed: every component type in that schema file is missing secrets
(it's a stale, test-only-referenced duplicate of the actively-maintained
pkg/datafetcher/schema/atmos/manifest/1.0.json, which already has it) —
fixing only CFN there would be a new inconsistency, not a fix. The
"union of sections" affected-detection finding (removed sections aren't
flagged as affected) was not fixed either: it's the deliberate, tested
behavior "section absent locally is skipped" shared by every component
type's equivalent helper (see TestAddHelmSectionAffected_NoFalsePositives
and TestAddKubernetesSectionAffected's identical subtests) — diverging only
CFN from that convention would be new inconsistency, not a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The --labels flag was migrated from a single comma-separated string to a
repeatable pflag StringSlice (fix(cli) 5587d16), but the CLI --help
golden snapshots that render its flag description were never regenerated,
so every Acceptance Tests shard exercising `--help` for terraform/config
commands fails on a stdout mismatch.

Regenerated via `-regenerate-snapshots` and hand-verified against the
diff CI itself produced; unrelated macOS-local artifacts (trailing
blank-line drift, toolchain-info column width) from the same batch
regeneration run were discarded rather than committed.
…ndependent

TestPrintStackEvent_FailedStatusWithReason and TestValidateTemplate assert
raw substring containment against captured stderr from ui.Error/ui.Success,
which route through toastMarkdown's styled renderer. At narrower terminal
widths (as GitHub Actions' non-TTY runners render, vs. this local
environment), toastMarkdown soft-wraps the text and re-emits ANSI style
codes per wrapped segment, splitting literal substrings like
"CREATE_FAILED" and "template is valid" across escape-code (and sometimes
newline+indent) boundaries — breaking the naive assert.Contains checks in
CI while passing locally.

Verified against a real repro: sweeping COLUMNS from 15 to 120 reproduced
both failure shapes (escape-code-only splits and real newline+indent
wraps). Fixed by stripping ANSI (github.com/charmbracelet/x/ansi, already
vendored) and collapsing whitespace before asserting, via a shared
normalizeUIOutput test helper — confirmed passing at every width in that
sweep, not just the one CI happened to hit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor New features that do not break anything size/xxl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant