Skip to content

Dry run reports no change when only a field inside an array item differs #1451

Description

@mbogunovic

Version

auth0-deploy-cli 8.42.0, and present on main at the time of writing.

What happens

A dry run reports no change at all when the only difference between the local assets and the tenant is the value of a field inside an array item. The asset is not listed in the dry-run table, its diff log is empty, and a subsequent import writes the change anyway.

We hit this on organization connections, where every meaningful flag lives inside organizations[].connections[]: flipping assign_membership_on_login, show_as_button or is_signup_enabled in the mirror produces a completely clean dry run.

Reproduction

No tenant needed, the comparison is pure:

const {
  hasObjectDifferences,
  calculateDryRunChanges,
} = require('auth0-deploy-cli/lib/tools/calculateDryRunChanges');

const local = {
  name: 'acme',
  connections: [{ connection_id: 'con_abc', assign_membership_on_login: true }],
};
const remote = {
  name: 'acme',
  connections: [{ connection_id: 'con_abc', assign_membership_on_login: false }],
};

hasObjectDifferences(local, remote, 'acme', 'organizations');
// false

calculateDryRunChanges({
  type: 'organizations',
  assets: [local],
  existing: [remote],
  identifiers: ['id', 'name'],
});
// { create: 0, update: 0, del: 0, conflicts: 0 }

End to end: export a tenant, change assign_membership_on_login on an organization connection in the exported file, a0deploy import --dry-run reports nothing, a0deploy import applies it.

Root cause

getObjectDifferences discards any value difference whose path contains an array index:

// src/tools/calculateDryRunChanges.ts:276-281
// Compare primitive values - omit array indices from path to reduce log noise
if (localValue !== remoteValue) {
  let arrayPathRegex = new RegExp(/\[\d+\]/g);
  if (!arrayPathRegex.test(currentPath)) {
    const message = `Value difference for [${currentPath}]: local:${localValue} vs remote:${remoteValue}`;
    differences.push(message);
  }
}

Element-wise array comparison recurses with `${currentPath}[${index}]` (line 219-233), so every primitive comparison inside an array item carries an index in its path and every one of them is suppressed. Nothing else covers the case: Array length difference is only emitted when the lengths differ, and Array content difference is only emitted from the primitive-array branch (line 240-252), never for an array of objects.

Because differences comes back empty, hasObjectDifferences returns false (line 300-308), so the updatedAssets filter drops the asset at line 419-425 and it never reaches update.

The suppression is also inconsistent within the same function. A key that exists only inside the local array item is reported, because that branch sits above the guard:

Key [acme.connections[0].show_as_button] found in 'localObj' but not in 'remoteObj'.

So adding show_as_button is reported, while changing it from true to false is silent. That asymmetry suggests the guard is an oversight rather than a deliberate trade-off.

Expected

A changed field inside an array item is reported as a difference, and the asset is classified as an update.

Suggested fix

Remove the index guard, so the recursion reports value differences at their full path (acme.connections[0].assign_membership_on_login: local:true vs remote:false).

If the guard exists to suppress duplicate noise for primitive arrays, note that primitive arrays never reach this line: they are handled by the order-insensitive branch above and reported once as Array content difference found for key [...]. Narrowing the guard to that case, or dropping it entirely, both leave the noisy path covered.

Whatever the rendering, the important half is that differences is non-empty so the asset is classified as an update. A dry run that reports clean and an import that then writes is the failure mode worth closing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions