Describe the bug
cdk diff reports no change for a resource whose DependsOn list was completely rewritten, as long as the number of entries stays the same and there are at least two of them.
dependsOnEqual is meant to compare two DependsOn arrays irrespective of element order. The loop that does it never runs past its first iteration:
cloudformation-diff/lib/diff/util.ts#L103-L117
if (Array.isArray(lvalue) && Array.isArray(rvalue)) {
if (lvalue.length !== rvalue.length) {
return false;
}
for (let i = 0 ; i < lvalue.length ; i++) {
for (let j = 0 ; j < lvalue.length ; j++) {
if ((!deepEqual(lvalue[i], rvalue[j])) && (j === lvalue.length - 1)) {
return false;
}
break; // <-- unconditional, so j is only ever 0
}
}
return true;
}
The break at the end of the inner body is unconditional, so the inner loop only ever executes j = 0. The single return false is guarded by j === lvalue.length - 1, which on that one iteration is only true when the array has exactly one element. For any array of length ≥ 2 the function therefore returns true no matter what the arrays contain — the length check is the only comparison that survives.
deepEqual routes every DependsOn key through this function, so the incorrect true propagates up to diffResource and the change is dropped from the diff.
Expected Behavior
Two DependsOn lists should compare equal when they contain the same entries, in any order, and unequal otherwise:
| before |
after |
expected |
['A', 'B'] |
['B', 'A'] |
equal (reorder only) |
['A', 'B'] |
['C', 'D'] |
different |
['A', 'B'] |
['A', 'D'] |
different |
['A', 'B', 'C'] |
['B', 'C', 'D'] |
different |
Current Behavior
Measured against main (536ad69), diffing two templates that are identical except for one resource's DependsOn:
| before |
after |
differenceCount |
correct? |
['A'] |
['B'] |
1 |
✅ |
['A', 'B'] |
['A'] |
1 |
✅ (length differs) |
['A', 'B'] |
['B', 'A'] |
0 |
✅ (reorder) |
['A', 'B'] |
['C', 'D'] |
0 |
❌ change is invisible |
['A', 'B'] |
['A', 'D'] |
0 |
❌ change is invisible |
['A', 'B', 'C'] |
['B', 'C', 'D'] |
0 |
❌ change is invisible |
So exactly one shape of change is detected — a single-element list, or a change in list length. Every other real DependsOn change is silently dropped.
This is not a rare shape. CDK emits multi-element DependsOn routinely (a lambda.Function alone produces DependsOn: [FnServiceRoleDefaultPolicy…, FnServiceRole…], as noted in #1602), so any refactor that swaps one dependency for another while keeping the count the same shows up as an empty diff. The user sees "There were no differences" for a template that did change, and deployment ordering changes without ever being surfaced for review.
Reproduction Steps
import { diffTemplate } from '@aws-cdk/cloudformation-diff';
const template = (dependsOn: string[]) => ({
Resources: {
A: { Type: 'AWS::SNS::Topic' },
B: { Type: 'AWS::SNS::Topic' },
C: { Type: 'AWS::SNS::Topic' },
D: { Type: 'AWS::SNS::Topic' },
Consumer: { Type: 'AWS::SQS::Queue', DependsOn: dependsOn },
},
});
// Consumer's dependencies are entirely replaced:
const diff = diffTemplate(template(['A', 'B']), template(['C', 'D']));
console.log(diff.resources.differenceCount); // 0 <- expected 1
Possible Solution
Match each element on the left against a distinct element on the right, so duplicates are compared by multiplicity, and neither input array is reordered — the no-mutation property that #1575 / #1602 established:
if (Array.isArray(lvalue) && Array.isArray(rvalue)) {
if (lvalue.length !== rvalue.length) {
return false;
}
const unmatched = rvalue.map((_, i) => i);
for (const l of lvalue) {
const at = unmatched.findIndex((i) => deepEqual(l, rvalue[i]));
if (at === -1) {
return false;
}
unmatched.splice(at, 1);
}
return true;
}
All three existing DependsOn behaviours in template-and-changeset-diff-merger.test.ts are preserved: reordering stays equal, differing lengths stay unequal, and 'A' vs ['A'] stays equal (handled by the branch above this one).
On backwards compatibility: this makes cdk diff surface changes it currently hides. It does not invent differences — every newly reported change is a real edit to the template.
Additional Information/Context
The existing test suite covers reordering, differing lengths, and the string-vs-single-element-array case, but never two same-length arrays with different contents, which is why this survived.
I have a fix with regression tests for each row of the table above; all 151 tests in the package pass with it. The four new tests fail on main and pass with the fix.
CDK CLI Version
main (536ad69), reproduced against @aws-cdk/cloudformation-diff in this repo
Node.js Version
v24.1.0
OS
macOS 15 (Darwin 25.2.0)
Language
TypeScript
Describe the bug
cdk diffreports no change for a resource whoseDependsOnlist was completely rewritten, as long as the number of entries stays the same and there are at least two of them.dependsOnEqualis meant to compare twoDependsOnarrays irrespective of element order. The loop that does it never runs past its first iteration:cloudformation-diff/lib/diff/util.ts#L103-L117The
breakat the end of the inner body is unconditional, so the inner loop only ever executesj = 0. The singlereturn falseis guarded byj === lvalue.length - 1, which on that one iteration is only true when the array has exactly one element. For any array of length ≥ 2 the function therefore returnstrueno matter what the arrays contain — the length check is the only comparison that survives.deepEqualroutes everyDependsOnkey through this function, so the incorrecttruepropagates up todiffResourceand the change is dropped from the diff.Expected Behavior
Two
DependsOnlists should compare equal when they contain the same entries, in any order, and unequal otherwise:['A', 'B']['B', 'A']['A', 'B']['C', 'D']['A', 'B']['A', 'D']['A', 'B', 'C']['B', 'C', 'D']Current Behavior
Measured against
main(536ad69), diffing two templates that are identical except for one resource'sDependsOn:differenceCount['A']['B']['A', 'B']['A']['A', 'B']['B', 'A']['A', 'B']['C', 'D']['A', 'B']['A', 'D']['A', 'B', 'C']['B', 'C', 'D']So exactly one shape of change is detected — a single-element list, or a change in list length. Every other real
DependsOnchange is silently dropped.This is not a rare shape. CDK emits multi-element
DependsOnroutinely (alambda.Functionalone producesDependsOn: [FnServiceRoleDefaultPolicy…, FnServiceRole…], as noted in #1602), so any refactor that swaps one dependency for another while keeping the count the same shows up as an empty diff. The user sees "There were no differences" for a template that did change, and deployment ordering changes without ever being surfaced for review.Reproduction Steps
Possible Solution
Match each element on the left against a distinct element on the right, so duplicates are compared by multiplicity, and neither input array is reordered — the no-mutation property that #1575 / #1602 established:
All three existing
DependsOnbehaviours intemplate-and-changeset-diff-merger.test.tsare preserved: reordering stays equal, differing lengths stay unequal, and'A'vs['A']stays equal (handled by the branch above this one).On backwards compatibility: this makes
cdk diffsurface changes it currently hides. It does not invent differences — every newly reported change is a real edit to the template.Additional Information/Context
The existing test suite covers reordering, differing lengths, and the string-vs-single-element-array case, but never two same-length arrays with different contents, which is why this survived.
I have a fix with regression tests for each row of the table above; all 151 tests in the package pass with it. The four new tests fail on
mainand pass with the fix.CDK CLI Version
main (
536ad69), reproduced against@aws-cdk/cloudformation-diffin this repoNode.js Version
v24.1.0
OS
macOS 15 (Darwin 25.2.0)
Language
TypeScript