Skip to content

Commit fca1ad5

Browse files
authored
[rush] Add an option to make the Rush operation hash depend on the NodeJS version. (#5630)
* feat(rush-lib): add dependsOnNodeVersion to operation settings in rush-project.json Add a new boolean `dependsOnNodeVersion` property to operation settings in rush-project.json. When set to true, the Node.js version (process.version) is included in the build cache hash, ensuring cached outputs are invalidated when the Node.js version changes. This is useful for projects that produce Node.js-version-specific outputs, such as native module builds. * feat(rush-lib): support granular Node.js version matching in dependsOnNodeVersion Extend dependsOnNodeVersion from a simple boolean to also accept 'major', 'minor', or 'patch' string values (with true as an alias for 'patch'). - 'major': hash includes only major version (e.g. 18) - 'minor': hash includes major.minor (e.g. 18.17) - 'patch'/true: hash includes full version (e.g. 18.17.1) Extract NodeVersionGranularity type alias for reuse. Pre-compute version strings at all granularity levels once per InputsSnapshot construction.
1 parent f83c6ad commit fca1ad5

8 files changed

Lines changed: 340 additions & 2 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Add a new `dependsOnNodeVersion` setting for operation entries in rush-project.json. When enabled, the Node.js version is included in the build cache hash, ensuring that cached outputs are invalidated when the Node.js version changes. Accepts `true` (alias for `\"patch\"`), `\"major\"`, `\"minor\"`, or `\"patch\"` to control the granularity of version matching.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/reviews/api/rush-lib.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@ export interface IOperationSettings {
669669
allowCobuildWithoutCache?: boolean;
670670
dependsOnAdditionalFiles?: string[];
671671
dependsOnEnvVars?: string[];
672+
dependsOnNodeVersion?: boolean | NodeVersionGranularity;
672673
disableBuildCacheForOperation?: boolean;
673674
ignoreChangedProjectsOnlyFlag?: boolean;
674675
operationName: string;
@@ -961,6 +962,9 @@ export class LockStepVersionPolicy extends VersionPolicy {
961962

962963
export { LookupByPath }
963964

965+
// @alpha
966+
export type NodeVersionGranularity = 'major' | 'minor' | 'patch';
967+
964968
// @public
965969
export class NpmOptionsConfiguration extends PackageManagerOptionsConfigurationBase {
966970
// @internal

libraries/rush-lib/src/api/RushProjectConfiguration.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ export interface IRushPhaseSharding {
7272
shardOperationSettings?: unknown;
7373
}
7474

75+
/**
76+
* The granularity at which the Node.js version is included in the build cache hash.
77+
*
78+
* - `"major"` — includes only the major version (e.g. `18`)
79+
* - `"minor"` — includes the major and minor version (e.g. `18.17`)
80+
* - `"patch"` — includes the full version (e.g. `18.17.1`)
81+
*
82+
* @alpha
83+
*/
84+
export type NodeVersionGranularity = 'major' | 'minor' | 'patch';
85+
7586
/**
7687
* @alpha
7788
*/
@@ -112,6 +123,20 @@ export interface IOperationSettings {
112123
*/
113124
dependsOnEnvVars?: string[];
114125

126+
/**
127+
* Specifies whether and at what granularity the Node.js version should be included in the hash
128+
* used for the build cache. When enabled, changing the Node.js version at the specified granularity
129+
* will invalidate cached outputs and cause the operation to be re-executed. This is useful for
130+
* projects that produce Node.js-version-specific outputs, such as native module builds.
131+
*
132+
* Allowed values:
133+
* - `true` — alias for `"patch"`, includes the full version (e.g. `18.17.1`)
134+
* - `"major"` — includes only the major version (e.g. `18`)
135+
* - `"minor"` — includes the major and minor version (e.g. `18.17`)
136+
* - `"patch"` — includes the full version (e.g. `18.17.1`)
137+
*/
138+
dependsOnNodeVersion?: boolean | NodeVersionGranularity;
139+
115140
/**
116141
* An optional list of glob (minimatch) patterns pointing to files that can affect this operation.
117142
* The hash values of the contents of these files will become part of the final hash when reading

libraries/rush-lib/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export { RushConfigurationProject } from './api/RushConfigurationProject';
7979
export {
8080
type IRushProjectJson as _IRushProjectJson,
8181
type IOperationSettings,
82+
type NodeVersionGranularity,
8283
RushProjectConfiguration,
8384
type IRushPhaseSharding
8485
} from './api/RushProjectConfiguration';

libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { type IReadonlyLookupByPath, LookupByPath } from '@rushstack/lookup-by-p
1010
import { InternalError, Path, Sort } from '@rushstack/node-core-library';
1111

1212
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
13-
import type { IOperationSettings, RushProjectConfiguration } from '../../api/RushProjectConfiguration';
13+
import type {
14+
IOperationSettings,
15+
NodeVersionGranularity,
16+
RushProjectConfiguration
17+
} from '../../api/RushProjectConfiguration';
1418
import { RushConstants } from '../RushConstants';
1519

1620
/**
@@ -90,6 +94,11 @@ export interface IInputsSnapshotParameters {
9094
* @defaultValue \{ ...process.env \}
9195
*/
9296
environment?: Record<string, string | undefined>;
97+
/**
98+
* The Node.js version string to use for `dependsOnNodeVersion`. Defaults to `process.version`.
99+
* @defaultValue process.version
100+
*/
101+
nodeVersion?: string;
93102
/**
94103
* File paths (keys into additionalHashes or hashes) to be included as part of every operation's dependencies.
95104
*/
@@ -205,6 +214,10 @@ export class InputsSnapshot implements IInputsSnapshot {
205214
* The environment to use for `dependsOnEnvVars`.
206215
*/
207216
private readonly _environment: Record<string, string | undefined>;
217+
/**
218+
* Pre-computed Node.js version strings at each granularity level for `dependsOnNodeVersion`.
219+
*/
220+
private readonly _nodeVersionByGranularity: Readonly<Record<NodeVersionGranularity, string>>;
208221

209222
/**
210223
*
@@ -219,6 +232,7 @@ export class InputsSnapshot implements IInputsSnapshot {
219232
hashes,
220233
hasUncommittedChanges,
221234
lookupByPath,
235+
nodeVersion = process.version,
222236
rootDir
223237
} = params;
224238
const projectMetadataMap: Map<
@@ -274,6 +288,8 @@ export class InputsSnapshot implements IInputsSnapshot {
274288
this._globalAdditionalHashes = globalAdditionalHashes;
275289
// Snapshot the environment so that queries are not impacted by when they happen
276290
this._environment = environment;
291+
// Parse Node.js version once so it doesn't need to be re-parsed per operation
292+
this._nodeVersionByGranularity = _parseNodeVersion(nodeVersion);
277293
this.hashes = hashes;
278294
this.hasUncommittedChanges = hasUncommittedChanges;
279295
this.rootDirectory = rootDir;
@@ -380,7 +396,7 @@ export class InputsSnapshot implements IInputsSnapshot {
380396
const operationSettings: Readonly<IOperationSettings> | undefined =
381397
record.projectConfig?.operationSettingsByOperationName.get(operationName);
382398
if (operationSettings) {
383-
const { dependsOnEnvVars, outputFolderNames } = operationSettings;
399+
const { dependsOnEnvVars, dependsOnNodeVersion, outputFolderNames } = operationSettings;
384400
if (dependsOnEnvVars) {
385401
// As long as we enumerate environment variables in a consistent order, we will get a stable hash.
386402
// Changing the order in rush-project.json will change the hash anyway since the file contents are part of the hash.
@@ -389,6 +405,12 @@ export class InputsSnapshot implements IInputsSnapshot {
389405
}
390406
}
391407

408+
if (dependsOnNodeVersion) {
409+
const granularity: NodeVersionGranularity =
410+
dependsOnNodeVersion === true ? 'patch' : dependsOnNodeVersion;
411+
hasher.update(`${hashDelimiter}nodeVersion=${this._nodeVersionByGranularity[granularity]}`);
412+
}
413+
392414
if (outputFolderNames) {
393415
hasher.update(`${hashDelimiter}${JSON.stringify(outputFolderNames)}`);
394416
}
@@ -421,6 +443,24 @@ export class InputsSnapshot implements IInputsSnapshot {
421443
}
422444
}
423445

446+
/**
447+
* Parses a Node.js version string once and returns pre-computed strings for each granularity level.
448+
*
449+
* @param rawVersion - The full Node.js version string (e.g. `v18.17.1`)
450+
* @returns An object with pre-computed version strings for `major`, `minor`, and `patch` granularities
451+
*/
452+
function _parseNodeVersion(rawVersion: string): Record<NodeVersionGranularity, string> {
453+
// Strip leading 'v' if present
454+
const version: string = rawVersion.startsWith('v') ? rawVersion.slice(1) : rawVersion;
455+
const [major, minor]: string[] = version.split('.');
456+
457+
return {
458+
major,
459+
minor: `${major}.${minor}`,
460+
patch: version
461+
};
462+
}
463+
424464
function getOrCreateProjectFilter(
425465
record: IInternalInputsSnapshotProjectMetadata
426466
): (filePath: string) => boolean {

0 commit comments

Comments
 (0)