Skip to content

fix(deployment): Fn::Join over list-returning intrinsics + AWS::NotificationARNs in Fn::Sub (#838) - #847

Merged
go-to-k merged 3 commits into
mainfrom
test/intrinsics-torture
Jun 13, 2026
Merged

fix(deployment): Fn::Join over list-returning intrinsics + AWS::NotificationARNs in Fn::Sub (#838)#847
go-to-k merged 3 commits into
mainfrom
test/intrinsics-torture

Conversation

@go-to-k

@go-to-k go-to-k commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

Two real fixes to cdkd's hand-rolled intrinsic-function resolver
(src/deployment/intrinsic-function-resolver.ts), both surfaced by a new
failure-seeking intrinsics-torture integration test:

(a) issue #838 -- Fn::Join over a list-returning intrinsic

Fn::Join with a list-returning intrinsic (Fn::Cidr / Fn::GetAZs /
Fn::Split) as its second argument crashed with values.map is not a function. The resolver assumed the second argument was already an array and
called .map() on it, but a list-returning intrinsic is an object
({ "Fn::Cidr": [...] }) until it is resolved. The fix resolves the second
argument FIRST, then maps over the resulting list. Closes #838.

(b) AWS::NotificationARNs in Fn::Sub / Ref

AWS::NotificationARNs resolved to undefined, which left the literal
${AWS::NotificationARNs} placeholder in an Fn::Sub output (the pseudo
branch is skipped on undefined, and the subsequent Ref attempt throws and
keeps the placeholder). cdkd has no stack-notification-ARN concept, so the
list is always empty -- and CloudFormation resolves an empty
AWS::NotificationARNs list to an empty string in a string context. The
resolver now returns '' (empty string) for AWS::NotificationARNs in both
Fn::Sub and bare Ref contexts, matching CloudFormation's own behavior
instead of leaking an unresolved placeholder downstream.

Tests

  • Unit (tests/unit/deployment/intrinsic-functions.test.ts): Fn::Join
    over Fn::Cidr / Fn::GetAZs / Fn::Split, and AWS::NotificationARNs
    resolving to '' in both Fn::Sub and Ref.
  • Integration (tests/integration/intrinsics-torture/**): a new
    failure-seeking fixture (an SNS topic + SQS queue + ten
    AWS::SSM::Parameters, no VPC / NAT / Lambda) that stress-tests the
    less-common + deeply-nested intrinsics the existing intrinsic-functions
    fixture never exercised -- Fn::Cidr, Fn::FindInMap, Fn::GetAZs +
    Fn::Select, Fn::Base64, nested Fn::Split + Fn::Select + Fn::Join,
    a deeply-nested two-arg Fn::Sub, and ALL pseudo-parameters. Each SSM
    parameter is built with the raw CFn escape hatch
    (new ssm.CfnParameter + addPropertyOverride('Value', <intrinsic>)) so the
    synth template carries the EXACT intrinsic shape under test. verify.sh
    reads each parameter back and asserts it equals a value computed
    independently from the account / region, so a wrong resolution pinpoints
    which intrinsic cdkd got wrong; it then destroys and asserts clean (state
    gone + zero orphan SSM parameters).
  • New scenario tag intrinsics-torture; coverage matrices regenerated;
    testing-guide entry added to docs/testing.md.

Validation

  • Real AWS: the intrinsics-torture integ passed GREEN end-to-end
    (/run-integ intrinsics-torture) -- deploy clean, every intrinsic resolved
    to the expected value, destroy clean with 0 orphans.
  • Broad: the bench-cdk-sample broad integ passed clean in the same
    worktree (integ-broad + integ-destroy markers fresh).
  • Local checks: typecheck / lint / build / full test suite (5771 tests) all
    green.

Closes #838

go-to-k added 3 commits June 14, 2026 00:10
…tion bugs

cdkd resolves every CloudFormation intrinsic itself in
src/deployment/intrinsic-function-resolver.ts (unlike the CDK CLI, which
defers them to CloudFormation), so the less-common intrinsics and deep
nesting are where cdkd is most likely to diverge. The existing
intrinsic-functions fixture only exercises Ref / Fn::GetAtt / Fn::Join /
Fn::Sub.

The new CdkdIntrinsicsTortureExample stack is cheap (SNS topic + SQS queue
+ ten AWS::SSM::Parameter; no VPC / NAT / Lambda) and computes each SSM
parameter Value via a harder intrinsic, built with the raw CFn escape hatch
(new ssm.CfnParameter + addPropertyOverride('Value', <intrinsic>)) so the
synth template carries the exact intrinsic shape under test.

Coverage beyond intrinsic-functions:
- Fn::Cidr ('10.0.0.0/16', 8, 8) -> eight /24 blocks (Fn::Select[3] and the
  full Fn::Join'ed list asserted)
- Fn::FindInMap (Mappings section with a {Ref: AWS::Region} top-level key +
  a region-independent DEFAULT row)
- Fn::GetAZs + Fn::Select[0] (first AZ, computed the same way cdkd sorts it)
- Fn::Base64
- nested Fn::Split + Fn::Select + Fn::Join (expect a|c|e)
- deeply-nested two-arg Fn::Sub (literal-map var via a nested Fn::Join +
  ${AWS::Region} + a ${<Queue>.Arn} GetAtt)
- ALL pseudo-parameters (AWS::AccountId / AWS::Region / AWS::Partition /
  AWS::StackName / AWS::URLSuffix / AWS::NotificationARNs)

verify.sh (BSD/macOS-portable, real deploy-rc capture, explicit
[verify] PASS) deploys, reads each parameter back via aws ssm get-parameter,
and asserts it equals an expected value computed independently from the
account / region, so a wrong resolution pinpoints the offending intrinsic;
a failed deploy prints the failing resource + error for triage. It then
destroys and asserts clean (state.json gone + zero orphan SSM parameters).
The pseudo assertion deliberately pins cdkd's documented behavior that
AWS::NotificationARNs resolves to the literal "undefined" inside Fn::Sub.

New scenario tag intrinsics-torture in the canonical taxonomy
(scripts/build-scenario-coverage-matrix.ts); coverage matrices regenerated;
README + testing-guide + changelog entries added.

NOTE: not yet run against real AWS -- needs /run-integ intrinsics-torture
before merge.
…ue list (#838)

IntrinsicFunctionResolver.resolveJoin assumed Fn::Join's second argument
was always a literal array and called values.map(...) directly. But
CloudFormation also allows the second argument to be a SINGLE intrinsic
that returns a list (Fn::Cidr, Fn::GetAZs, Fn::Split, or a Ref to a
CommaDelimitedList parameter), which crashed deploy with
"values.map is not a function".

resolveJoin now resolves the second argument first when it is not already
an array (it may be a list-returning intrinsic), then maps over the
resulting list. If the resolved value is still not an array it throws a
clear error naming the accepted shapes, for CloudFormation parity.

Adds unit tests covering Fn::Join over Fn::Cidr, Fn::Split, Fn::GetAZs, a
Ref to a CommaDelimitedList parameter, the literal-array regression case,
and the non-list error path.

Closes #838
…pty in Fn::Sub/Ref

cdkd's intrinsic resolver returned `undefined` for the AWS::NotificationARNs
pseudo-parameter. In an Fn::Sub body that left the literal placeholder
`${AWS::NotificationARNs}` in the output (the pseudo branch is skipped on
`undefined`, and the subsequent Ref attempt throws + keeps the placeholder),
diverging from CloudFormation.

cdkd has no stack-notification-ARN concept (a cdkd deploy never sets SNS
notification ARNs on a stack), so the list is always empty. CloudFormation
resolves an empty AWS::NotificationARNs list to an empty string in an
Fn::Sub / Ref string context, so the resolver now returns '' (empty string)
for AWS::NotificationARNs in both Fn::Sub and bare Ref contexts.

- src/deployment/intrinsic-function-resolver.ts: return '' instead of undefined
- tests/unit/deployment/intrinsic-functions.test.ts: add a describe block
  asserting Fn::Sub('${AWS::NotificationARNs}') and Ref: AWS::NotificationARNs
  both resolve to ''
- tests/integration/intrinsics-torture: update the verify.sh expected value
  from `notif=undefined` to `notif=` (the prior expectation pinned a
  non-existent "undefined" behavior); update the stack/README/changelog notes
  to describe the CFn-parity empty-string resolution
@go-to-k

go-to-k commented Jun 13, 2026

Copy link
Copy Markdown
Owner Author

Independent 3-axis review complete (code re-review + test review; spec N/A for a bug fix). No blockers. Code review confirmed the Fn::Join 2nd-arg list-intrinsic resolution (no literal-array regression, throws on non-list) and AWS::NotificationARNs -> '' parity (strictly better: the bare-Ref path previously threw). Test review confirmed exact-value unit + integ assertions for both fixes incl. real-AWS readback. Setting pr-review bound to c4fdf1a.

@go-to-k
go-to-k merged commit 78d239a into main Jun 13, 2026
5 checks passed
@go-to-k
go-to-k deleted the test/intrinsics-torture branch June 13, 2026 15:23
github-actions Bot pushed a commit that referenced this pull request Jun 13, 2026
## [0.221.3](v0.221.2...v0.221.3) (2026-06-13)

### Bug Fixes

* **deployment:** Fn::Join over list-returning intrinsics + AWS::NotificationARNs in Fn::Sub ([#838](#838)) ([#847](#847)) ([78d239a](78d239a))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.221.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(deployment): Fn::Join with a list-returning intrinsic (Fn::Cidr/GetAZs/Split) as its 2nd arg crashes 'values.map is not a function'

1 participant