[rush] `allowedAlternativeVersions` is too permissive and allows versions that should not be allowed
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:
-
Arepresenting version19.0.0 -
Brepresenting version19.0.1 -
Crepresenting version19.2.0 -
Drepresenting version19.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 |
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.
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.
-
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. -
The Cancellation Paradox: As shown in Scenario A, explicitly adding a version to both
preferredVersionsandallowedAlternativeVersionseffectively 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.jsongeneration). -
All items in the array are treated as Allowed (for
rush checkvalidation). - Any version not in this array triggers a
rush checkerror. -
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
allowedVersionsalongside the legacy fields (preferredVersionsorallowedAlternativeVersions) should throw a config error.
👋 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 😁
What @aramissennyeydd said. Admittedly, this feature is confusing. We should consider tweaking the naming or provide a plugin that tweaks the behavior.