Skip to content

fix: sort object keys before JSON.stringify in transformProjectGraphForRust#35298

Open
omer-za wants to merge 2 commits intonrwl:masterfrom
omer-za:fix/deterministic-project-configuration-hash
Open

fix: sort object keys before JSON.stringify in transformProjectGraphForRust#35298
omer-za wants to merge 2 commits intonrwl:masterfrom
omer-za:fix/deterministic-project-configuration-hash

Conversation

@omer-za
Copy link
Copy Markdown
Contributor

@omer-za omer-za commented Apr 15, 2026

Summary

Fixes #35297

JSON.stringify() preserves JavaScript object key insertion order. In transformProjectGraphForRust, target options and configurations are serialized with JSON.stringify() and passed to the Rust native hasher, which hashes them as-is for ProjectConfiguration.

If any Nx plugin or the target-defaults merge step produces these objects with keys in a different insertion order on different CI runners, the serialized string changes, causing the ProjectConfiguration hash to become non-deterministic. This leads to:

  • Nx Cloud cache invalidation on CI reruns
  • Previously succeeded tasks being re-executed unnecessarily
  • Wasted CI compute time

The Fix

Adds a recursive sortObjectKeys helper that normalizes key order before serialization, making the ProjectConfiguration hash independent of JS object construction order.

Note: The Rust hasher already sorts target names before hashing (making the hash independent of target insertion order), but it does not normalize the content of the stringified options/configurations. This fix closes that gap.

Evidence

Tested locally using the native HashPlanner + TaskHasher APIs:

Without fix

Options string ProjectConfiguration hash
{"cwd":"...","env":{...},"command":"jest","passWithNoTests":true} 10839486168096120338
{"passWithNoTests":true,"command":"jest","env":{...},"cwd":"..."} 14689511571626992510

With fix

Reversed ALL target orders AND ALL options/configurations key orders for ALL projects across the entire project graph:

  • 530 ProjectConfiguration entries, 0 mismatches
  • Identical task hashes

Related

This is analogous to the fix in commit db31f30 which sorted keys before hashing for AllExternalDependencies via hashObject.

@omer-za omer-za requested a review from a team as a code owner April 15, 2026 09:54
@omer-za omer-za requested a review from FrozenPandaz April 15, 2026 09:54
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

👷 Deploy request for nx-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit e2bacb0

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

👷 Deploy request for nx-dev pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit e2bacb0

@omer-za omer-za force-pushed the fix/deterministic-project-configuration-hash branch from b2ef079 to 0a44149 Compare April 15, 2026 10:08
@FrozenPandaz FrozenPandaz self-assigned this Apr 23, 2026
@FrozenPandaz FrozenPandaz added the priority: high High Priority (important issues which affect many people severely) label Apr 23, 2026
@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented Apr 23, 2026

View your CI Pipeline Execution ↗ for commit 0a44149

Command Status Duration Result
nx-cloud record -- nx format:check ❌ Failed 14s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 23s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-23 21:03:12 UTC

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud is proposing a fix for your failed CI:

We applied a prettier formatting fix to packages/nx/src/native/transform-objects.ts to resolve the format:check failure. The configurations line introduced by the PR exceeded prettier's line length limit and needed to be wrapped across multiple lines. This change is purely cosmetic and does not affect the runtime behavior of the sortObjectKeys fix.

Tip

We verified this fix by re-running nx-cloud record -- nx format:check.

diff --git a/packages/nx/src/native/transform-objects.ts b/packages/nx/src/native/transform-objects.ts
index c4164ea1..3f4e03af 100644
--- a/packages/nx/src/native/transform-objects.ts
+++ b/packages/nx/src/native/transform-objects.ts
@@ -36,7 +36,9 @@ export function transformProjectGraphForRust(
         inputs: targetConfig.inputs,
         outputs: targetConfig.outputs,
         options: JSON.stringify(sortObjectKeys(targetConfig.options)),
-        configurations: JSON.stringify(sortObjectKeys(targetConfig.configurations)),
+        configurations: JSON.stringify(
+          sortObjectKeys(targetConfig.configurations)
+        ),
         parallelism: targetConfig.parallelism,
       };
     }

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.

Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.

Apply changes locally with:

npx nx-cloud apply-locally ois5-3knp

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@omer-za
Copy link
Copy Markdown
Contributor Author

omer-za commented May 4, 2026

Update — extended the fix to cover inputs, outputs, and namedInputs.

We rolled out the original two-field fix (options, configurations) as a patch-package patch in our monorepo and re-ran the same GitHub Actions workflow twice with identical commit SHAs. We were still seeing ProjectConfiguration cache misses on typecheck (and a handful of other targets) on the second run.

Root cause: three more fields that flow into the same transformProjectGraphForRust codepath were still preserving JS key insertion order:

  • target.inputs — entries can be objects: { env }, { runtime }, { externalDependencies }, { dependentTasksOutputFiles }, { fileset }
  • target.outputs — rare, but objects are valid
  • projectNode.data.namedInputs — top-level keys plus nested objects (same shape as target.inputs)

These get reordered between CI runs whenever a plugin iterates a Map vs. a plain object, async resolution races, or a tsconfig.json with extends is walked in a different order. The 5-field normalisation closes the remaining gap.

Latest commit:

  • Applies sortObjectKeys to all five fields
  • Adds packages/nx/src/native/tests/transform-objects.spec.ts with per-field determinism tests plus a full-graph reverse-key case

Local verification: applying the patched JS to our 1500-project workspace and running nx graph twice now produces a byte-identical ProjectConfiguration hash.

omer-za and others added 2 commits May 4, 2026 15:20
…orRust

JSON.stringify() preserves JavaScript object key insertion order. The Rust
native hasher receives these serialized strings and hashes them as-is for
ProjectConfiguration. If any plugin or the target-defaults merge produces
options/configurations objects with keys in a different insertion order on
different CI runners, the serialized string changes, causing the
ProjectConfiguration hash to become non-deterministic.

This leads to Nx Cloud cache invalidation and unnecessary task re-execution
when rerunning failed CI pipelines.

The fix adds a recursive sortObjectKeys helper that normalizes key order
before serialization, making the hash independent of JS object construction
order.

Fixes nrwl#35297
The original change only sorted `target.options` and `target.configurations`
before `JSON.stringify`. Three other fields that flow into the hash via
the same `transformProjectGraphForRust` codepath were still ordered by JS
key insertion:

- `target.inputs`            (entries can be objects: { env }, { runtime },
                              { externalDependencies })
- `target.outputs`           (rare, but objects are valid)
- `projectNode.data.namedInputs` (top-level keys + nested objects)

In production we still see ProjectConfiguration cache misses across CI
runs that build the same logical graph in different orders (Map vs.
plain-object iteration in inferred plugins, async resolution races, etc.).
Extending `sortObjectKeys` to these fields restores cache hits.

Also adds tests asserting determinism for each field individually plus
the full-graph case.

Co-authored-by: Cursor <cursoragent@cursor.com>
@omer-za omer-za force-pushed the fix/deterministic-project-configuration-hash branch from f7606bc to e2bacb0 Compare May 4, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: high High Priority (important issues which affect many people severely)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-deterministic ProjectConfiguration hash due to JSON.stringify key-order sensitivity in transformProjectGraphForRust

2 participants