rushstack icon indicating copy to clipboard operation
rushstack copied to clipboard

[rush] `allowedAlternativeVersions` is too permissive and allows versions that should not be allowed

Open lukaskl opened this issue 1 month ago • 1 comments

Summary

rush check or rush update may incorrectly pass even if some packages use versions outside the allowed alternatives specified in common-versions.json.

Repro steps

I've created a minimal repository to reproduce this issue I suggest checking out this repository and trying running rush update.


Step 1: Set up allowedAlternativeVersions

Let's say at allowedAlternativeVersions we have:

"react": ["19.0.1", "19.2.1"]

Step 2: Include one matching and one non-matching version

I'm controlling which versions are referenced by the workspaces via rush.json projects configuration, simply by commenting/uncommenting the relevant projects.

For example, let's include packages with React versions 19.0.0 and 19.0.1:

"projects": [
  { "packageName": "example-19-0-0", "projectFolder": "packages/example-19-0-0" },
  { "packageName": "example-19-0-1", "projectFolder": "packages/example-19-0-1" }
  // { "packageName": "example-19-2-0", "projectFolder": "packages/example-19-2-0" },
  // { "packageName": "example-19-2-1", "projectFolder": "packages/example-19-2-1" }
]

Step 3: Run rush check or rush update

Expected outcome:

The command should fail, indicating that example-19-0-0 is using a disallowed version of [email protected].

Actual outcome:

The command passes (incorrectly), even though example-19-0-0 is using a version of [email protected] that is not listed in the allowed alternatives.

Variations

Let's simplify our version notation, let's say we have:

  • A representing version 19.0.0
  • B representing version 19.0.1
  • C representing version 19.2.0
  • D representing version 19.2.1

We have allowed versions: B, D, then the following table summarizes various combinations of workspace referenced versions and the expected vs actual results from rush check or rush update:

Workspace referenced
versions
Allowed
versions
rush check
Expected result
rush check
Actual result
A B & D ❌*
B B & D
C B & D ❌*
D B & D
A & B B & D
A & C B & D
A & D B & D
B & C B & D
B & D B & D
C & D B & D
A & B & C B & D
A & B & D B & D
A & C & D B & D
B & C & D B & D
A & B & C & D B & D

* Regarding single A and single C cases, reluctantly I accept that Rush may not flag them as errors since there are no "alternative" versions present. However, I believe it would be more consistent if these cases were also flagged as errors, given that they do not comply with the allowed versions policy.

Standard questions

Please answer these questions to help us investigate your issue more quickly:

Question Answer
@microsoft/rush globally installed version? 5.163.0
rushVersion from rush.json? 5.163.0
useWorkspaces from rush.json? true at pnpm-config.json
Operating system? Mac
Would you consider contributing a PR? With guidance - Yes
Node.js version (node -v)? v22.20.0

lukaskl avatar Dec 07 '25 22:12 lukaskl

What does preferredVersions list for the examples in the table? If there is no entry in preferredVersions, than the first version that is not a match for allowedAlternativeVersions will be assumed to be the preferred version.

The spec for allowedAlternativeVersions is "if these exact version strings are encountered, pass them through as-is; for any other version, validate it against the workspace's preferred version.

dmichon-msft avatar Dec 10 '25 19:12 dmichon-msft

Thank you for your reply! In my case, the preferredVersions list was empty.

Just FYI: I've recreated the validation table
Workspace referenced versions Assumed Preferred Version Allowed alternative versions Missing / Offending versions rush check Result
A A B || D - ✅ Pass
B (None) B || D - ✅ Pass
C C B || D - ✅ Pass
D (None) B || D - ✅ Pass
A & B A B || D - ✅ Pass
A & C A (or C*) B || D C (or A*) ❌ Fail
A & D A B || D - ✅ Pass
B & C C B || D - ✅ Pass
B & D (None) B || D - ✅ Pass
C & D C B || D - ✅ Pass
A & B & C A (or C*) B || D C (or A*) ❌ Fail
A & B & D A B || D - ✅ Pass
A & C & D A (or C*) B || D C (or A*) ❌ Fail
B & C & D C B || D - ✅ Pass
A & B & C & D A (or C*) B || D C (or A*) ❌ Fail

*Note on "A (or C)": Whichever version Rush encounters first in its processing order becomes the Assumed Preferred Version; the subsequent one becomes the "offending" version that triggers the error.

After your explanation and more extensive trial and error, I believe I understand why it works like this, but I believe that it shouldn't work like this or there should be alternative ways to configure it more strictly. As of now, it shows symptoms of a footgun.

But let me present more examples:


Scenario A:

{
  "preferredVersions": { "react": "19.0.1" },
  "allowedAlternativeVersions": {
    "react": ["19.0.1", "19.2.1"]
  }
}

Workspace references: [email protected] and [email protected]

Expected: ❌ - validation fails because 19.0.0 is neither preferred nor in allowed alternatives. Actual: ✅ - validation passes


Scenario B:

{
  "implicitlyPreferredVersions": false,
  "allowedAlternativeVersions": {
    "react": ["19.0.1", "19.2.1"]
  }
}

Workspace references: [email protected] and [email protected]

Expected: ❌ - validation fails because 19.0.0 is not in allowed alternatives and there are no implicit preferred versions. Actual: ✅ - validation passes


Scenario C: (This is the only scenario where I've made it "work")

{
  "preferredVersions": { "react": "19.0.1" },
  "allowedAlternativeVersions": {
    "react": ["19.2.1"]
  }
}

Workspace references: [email protected] and [email protected]

Expected: ❌ - validation fails because 19.0.0 is neither preferred nor in allowed alternatives. Actual: ❌ - validation fails as expected.


Why it is a footgun

Currently, rush check creates the expectation of a strict validator that ensures consistency. However, the current configuration logic makes it surprisingly brittle to enforce specific versions.

  1. Implicit Pass: If we forget to set a preferredVersion, Rush’s inference allows any single version to pass, even if it wasn't what we intended.
  2. The Cancellation Paradox: As shown in Scenario A, explicitly adding a version to both preferredVersions and allowedAlternativeVersions effectively removes that version from the validation pool. This "cancels out" the check and allows unauthorized versions to pass as the new "assumed default."

Suggestion

It is possible to come up with multiple solutions here. I'm sharing one from my team:

We suggest introducing a new field, allowedVersions, which would replace the split logic of preferredVersions and allowedAlternativeVersions. For example, this:

{
  "preferredVersions": { "react": "19.0.1" },
  "allowedAlternativeVersions": {
    "react": ["19.2.1"]
  }
}

would be replaced by:

{
  "allowedVersions": {
    "react": ["19.0.1", "19.2.1"]
  }
}

and this would follow this logic:

  • The first item in the array is treated as the Preferred Version (for common/temp/package.json generation).
  • All items in the array are treated as Allowed (for rush check validation).
  • Any version not in this array triggers a rush check error.
  • Default Behavior: If a package is not listed in allowedVersions, Rush falls back to its standard behavior: it enforces a single consistent version across the scope.
  • Legacy check: To prevent confusion, defining allowedVersions alongside the legacy fields (preferredVersions or allowedAlternativeVersions) should throw a config error.

lukaskl avatar Dec 11 '25 16:12 lukaskl

👋 FWIW we ran into a similar issue about a year ago - we mistreated allowedAlternativeVersions as allowedVersions and it became a place to list all existing versions of a dependency which left a floating version that could be anything. In your example above where A and B are in allowedAlternativeVersions, C/D would be the floating version. What helped us was a check to make sure that the list containing the preferred version + allowedAlternatives completely matched those defined in the repo - happy to upstream that logic if there's interest. Now that we have the protection against over defining allowed versions, we've had no issues 😁

aramissennyeydd avatar Dec 12 '25 20:12 aramissennyeydd

What @aramissennyeydd said. Admittedly, this feature is confusing. We should consider tweaking the naming or provide a plugin that tweaks the behavior.

iclanton avatar Dec 17 '25 19:12 iclanton