From e41eb391b0ec1caccd63544caedfca5b839278b7 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 17 Feb 2026 22:42:51 -0800 Subject: [PATCH 1/2] 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. --- ...dependsOnNodeVersion_2026-02-17-00-00.json | 10 ++ common/reviews/api/rush-lib.api.md | 1 + .../src/api/RushProjectConfiguration.ts | 8 ++ .../src/logic/incremental/InputsSnapshot.ts | 18 +++- .../incremental/test/InputsSnapshot.test.ts | 100 ++++++++++++++++++ .../__snapshots__/InputsSnapshot.test.ts.snap | 4 + .../src/schemas/rush-project.schema.json | 4 + 7 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json diff --git a/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json b/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json new file mode 100644 index 00000000000..ca13204a90d --- /dev/null +++ b/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add a new `dependsOnNodeVersion` setting for operation entries in rush-project.json. When set to `true`, the Node.js version is included in the build cache hash, ensuring that cached outputs are invalidated when the Node.js version changes.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 6254bc8cabf..2993b4f9cff 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -669,6 +669,7 @@ export interface IOperationSettings { allowCobuildWithoutCache?: boolean; dependsOnAdditionalFiles?: string[]; dependsOnEnvVars?: string[]; + dependsOnNodeVersion?: boolean; disableBuildCacheForOperation?: boolean; ignoreChangedProjectsOnlyFlag?: boolean; operationName: string; diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index 7dd28c3c789..ce9748e14c0 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -112,6 +112,14 @@ export interface IOperationSettings { */ dependsOnEnvVars?: string[]; + /** + * If set to true, the Node.js version (process.version) will be included in the hash used for the + * build cache. This ensures that if the Node.js version changes, cached outputs will be invalidated + * and the operation will be re-executed. This is useful for projects that produce + * Node.js-version-specific outputs, such as native module builds. + */ + dependsOnNodeVersion?: boolean; + /** * An optional list of glob (minimatch) patterns pointing to files that can affect this operation. * The hash values of the contents of these files will become part of the final hash when reading diff --git a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts index 8eeaa5ed224..6e2cbb04154 100644 --- a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts +++ b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts @@ -90,6 +90,11 @@ export interface IInputsSnapshotParameters { * @defaultValue \{ ...process.env \} */ environment?: Record; + /** + * The Node.js version string to use for `dependsOnNodeVersion`. Defaults to `process.version`. + * @defaultValue process.version + */ + nodeVersion?: string; /** * File paths (keys into additionalHashes or hashes) to be included as part of every operation's dependencies. */ @@ -205,6 +210,10 @@ export class InputsSnapshot implements IInputsSnapshot { * The environment to use for `dependsOnEnvVars`. */ private readonly _environment: Record; + /** + * The Node.js version string to use for `dependsOnNodeVersion`. + */ + private readonly _nodeVersion: string; /** * @@ -219,6 +228,7 @@ export class InputsSnapshot implements IInputsSnapshot { hashes, hasUncommittedChanges, lookupByPath, + nodeVersion = process.version, rootDir } = params; const projectMetadataMap: Map< @@ -274,6 +284,8 @@ export class InputsSnapshot implements IInputsSnapshot { this._globalAdditionalHashes = globalAdditionalHashes; // Snapshot the environment so that queries are not impacted by when they happen this._environment = environment; + // Snapshot the Node.js version so that queries are not impacted by when they happen + this._nodeVersion = nodeVersion; this.hashes = hashes; this.hasUncommittedChanges = hasUncommittedChanges; this.rootDirectory = rootDir; @@ -380,7 +392,7 @@ export class InputsSnapshot implements IInputsSnapshot { const operationSettings: Readonly | undefined = record.projectConfig?.operationSettingsByOperationName.get(operationName); if (operationSettings) { - const { dependsOnEnvVars, outputFolderNames } = operationSettings; + const { dependsOnEnvVars, dependsOnNodeVersion, outputFolderNames } = operationSettings; if (dependsOnEnvVars) { // As long as we enumerate environment variables in a consistent order, we will get a stable hash. // Changing the order in rush-project.json will change the hash anyway since the file contents are part of the hash. @@ -389,6 +401,10 @@ export class InputsSnapshot implements IInputsSnapshot { } } + if (dependsOnNodeVersion) { + hasher.update(`${hashDelimiter}nodeVersion=${this._nodeVersion}`); + } + if (outputFolderNames) { hasher.update(`${hashDelimiter}${JSON.stringify(outputFolderNames)}`); } diff --git a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts index 867a1f446f9..fee54937955 100644 --- a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts +++ b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts @@ -413,6 +413,106 @@ describe(InputsSnapshot.name, () => { expect(result2).not.toEqual(result1); }); + it('Respects dependsOnNodeVersion', () => { + const { project, options } = getTestConfig(); + const baseline: string = new InputsSnapshot(options).getOperationOwnStateHash(project, '_phase:build'); + + const projectConfig1: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build', + dependsOnNodeVersion: true + } + ] + ]) + }; + + const input1: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([ + [ + project, + { + projectConfig: projectConfig1 as RushProjectConfiguration + } + ] + ]), + nodeVersion: 'v18.17.0' + }); + + const result1: string = input1.getOperationOwnStateHash(project, '_phase:build'); + + expect(result1).toMatchSnapshot(); + expect(result1).not.toEqual(baseline); + + const input2: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([ + [ + project, + { + projectConfig: projectConfig1 as RushProjectConfiguration + } + ] + ]), + nodeVersion: 'v20.10.0' + }); + + const result2: string = input2.getOperationOwnStateHash(project, '_phase:build'); + + expect(result2).toMatchSnapshot(); + expect(result2).not.toEqual(baseline); + expect(result2).not.toEqual(result1); + }); + + it('Does not include node version when dependsOnNodeVersion is not set', () => { + const { project, options } = getTestConfig(); + + const projectConfig: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build' + } + ] + ]) + }; + + const input1: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([ + [ + project, + { + projectConfig: projectConfig as RushProjectConfiguration + } + ] + ]), + nodeVersion: 'v18.17.0' + }); + + const input2: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([ + [ + project, + { + projectConfig: projectConfig as RushProjectConfiguration + } + ] + ]), + nodeVersion: 'v20.10.0' + }); + + const result1: string = input1.getOperationOwnStateHash(project, '_phase:build'); + const result2: string = input2.getOperationOwnStateHash(project, '_phase:build'); + + expect(result1).toEqual(result2); + }); + it('Respects dependsOnEnvVars', () => { const { project, options } = getTestConfig(); const baseline: string = new InputsSnapshot(options).getOperationOwnStateHash(project, '_phase:build'); diff --git a/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap b/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap index a1db7fa9818..26f7371eb6a 100644 --- a/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap +++ b/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap @@ -10,6 +10,10 @@ exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnEnvVars 1`] = exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnEnvVars 2`] = `"2c68d56fc9278b6495496070a6a992b929c37a83"`; +exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 1`] = `"df8e519b405d7deca932c0085c1c6b48b57ae4fc"`; + +exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 2`] = `"8323bdc4729024e6f0e4b4378f0f3e6bef96dce2"`; + exports[`InputsSnapshot getOperationOwnStateHash Respects globalAdditionalFiles 1`] = `"0e0437ad1941bacd098b22da15dc673f86ca6003"`; exports[`InputsSnapshot getOperationOwnStateHash Respects incrementalBuildIgnoredGlobs 1`] = `"f7b5af9ffdaa39831ed3374f28d0f7dccbee9c8d"`; diff --git a/libraries/rush-lib/src/schemas/rush-project.schema.json b/libraries/rush-lib/src/schemas/rush-project.schema.json index acf1b20e5ae..12317e78a2a 100644 --- a/libraries/rush-lib/src/schemas/rush-project.schema.json +++ b/libraries/rush-lib/src/schemas/rush-project.schema.json @@ -98,6 +98,10 @@ } } }, + "dependsOnNodeVersion": { + "description": "If set to true, the Node.js version (process.version) will be included in the hash used for the build cache. This ensures that if the Node.js version changes, cached outputs will be invalidated and the operation will be re-executed. This is useful for projects that produce Node.js-version-specific outputs, such as native module builds.", + "type": "boolean" + }, "weight": { "description": "The number of concurrency units that this operation should take up. The maximum concurrency units is determined by the -p flag.", "type": "integer", From e8c049d5422dbb308a9eebd33c48e6d8f5f0619f Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 17 Feb 2026 22:54:18 -0800 Subject: [PATCH 2/2] 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. --- ...dependsOnNodeVersion_2026-02-17-00-00.json | 2 +- common/reviews/api/rush-lib.api.md | 5 +- .../src/api/RushProjectConfiguration.ts | 27 +++- libraries/rush-lib/src/index.ts | 1 + .../src/logic/incremental/InputsSnapshot.ts | 36 ++++- .../incremental/test/InputsSnapshot.test.ts | 147 ++++++++++++++++++ .../__snapshots__/InputsSnapshot.test.ts.snap | 4 +- .../src/schemas/rush-project.schema.json | 7 +- 8 files changed, 212 insertions(+), 17 deletions(-) diff --git a/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json b/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json index ca13204a90d..b18941f9ea4 100644 --- a/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json +++ b/common/changes/@microsoft/rush-lib/rush-project-dependsOnNodeVersion_2026-02-17-00-00.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Add a new `dependsOnNodeVersion` setting for operation entries in rush-project.json. When set to `true`, the Node.js version is included in the build cache hash, ensuring that cached outputs are invalidated when the Node.js version changes.", + "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.", "type": "none" } ], diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 2993b4f9cff..1f8def84f93 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -669,7 +669,7 @@ export interface IOperationSettings { allowCobuildWithoutCache?: boolean; dependsOnAdditionalFiles?: string[]; dependsOnEnvVars?: string[]; - dependsOnNodeVersion?: boolean; + dependsOnNodeVersion?: boolean | NodeVersionGranularity; disableBuildCacheForOperation?: boolean; ignoreChangedProjectsOnlyFlag?: boolean; operationName: string; @@ -962,6 +962,9 @@ export class LockStepVersionPolicy extends VersionPolicy { export { LookupByPath } +// @alpha +export type NodeVersionGranularity = 'major' | 'minor' | 'patch'; + // @public export class NpmOptionsConfiguration extends PackageManagerOptionsConfigurationBase { // @internal diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index ce9748e14c0..528680feb50 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -72,6 +72,17 @@ export interface IRushPhaseSharding { shardOperationSettings?: unknown; } +/** + * The granularity at which the Node.js version is included in the build cache hash. + * + * - `"major"` — includes only the major version (e.g. `18`) + * - `"minor"` — includes the major and minor version (e.g. `18.17`) + * - `"patch"` — includes the full version (e.g. `18.17.1`) + * + * @alpha + */ +export type NodeVersionGranularity = 'major' | 'minor' | 'patch'; + /** * @alpha */ @@ -113,12 +124,18 @@ export interface IOperationSettings { dependsOnEnvVars?: string[]; /** - * If set to true, the Node.js version (process.version) will be included in the hash used for the - * build cache. This ensures that if the Node.js version changes, cached outputs will be invalidated - * and the operation will be re-executed. This is useful for projects that produce - * Node.js-version-specific outputs, such as native module builds. + * Specifies whether and at what granularity the Node.js version should be included in the hash + * used for the build cache. When enabled, changing the Node.js version at the specified granularity + * will invalidate cached outputs and cause the operation to be re-executed. This is useful for + * projects that produce Node.js-version-specific outputs, such as native module builds. + * + * Allowed values: + * - `true` — alias for `"patch"`, includes the full version (e.g. `18.17.1`) + * - `"major"` — includes only the major version (e.g. `18`) + * - `"minor"` — includes the major and minor version (e.g. `18.17`) + * - `"patch"` — includes the full version (e.g. `18.17.1`) */ - dependsOnNodeVersion?: boolean; + dependsOnNodeVersion?: boolean | NodeVersionGranularity; /** * An optional list of glob (minimatch) patterns pointing to files that can affect this operation. diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 88dfb89789e..a91cea23a98 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -79,6 +79,7 @@ export { RushConfigurationProject } from './api/RushConfigurationProject'; export { type IRushProjectJson as _IRushProjectJson, type IOperationSettings, + type NodeVersionGranularity, RushProjectConfiguration, type IRushPhaseSharding } from './api/RushProjectConfiguration'; diff --git a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts index 6e2cbb04154..181a602ab13 100644 --- a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts +++ b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts @@ -10,7 +10,11 @@ import { type IReadonlyLookupByPath, LookupByPath } from '@rushstack/lookup-by-p import { InternalError, Path, Sort } from '@rushstack/node-core-library'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import type { IOperationSettings, RushProjectConfiguration } from '../../api/RushProjectConfiguration'; +import type { + IOperationSettings, + NodeVersionGranularity, + RushProjectConfiguration +} from '../../api/RushProjectConfiguration'; import { RushConstants } from '../RushConstants'; /** @@ -211,9 +215,9 @@ export class InputsSnapshot implements IInputsSnapshot { */ private readonly _environment: Record; /** - * The Node.js version string to use for `dependsOnNodeVersion`. + * Pre-computed Node.js version strings at each granularity level for `dependsOnNodeVersion`. */ - private readonly _nodeVersion: string; + private readonly _nodeVersionByGranularity: Readonly>; /** * @@ -284,8 +288,8 @@ export class InputsSnapshot implements IInputsSnapshot { this._globalAdditionalHashes = globalAdditionalHashes; // Snapshot the environment so that queries are not impacted by when they happen this._environment = environment; - // Snapshot the Node.js version so that queries are not impacted by when they happen - this._nodeVersion = nodeVersion; + // Parse Node.js version once so it doesn't need to be re-parsed per operation + this._nodeVersionByGranularity = _parseNodeVersion(nodeVersion); this.hashes = hashes; this.hasUncommittedChanges = hasUncommittedChanges; this.rootDirectory = rootDir; @@ -402,7 +406,9 @@ export class InputsSnapshot implements IInputsSnapshot { } if (dependsOnNodeVersion) { - hasher.update(`${hashDelimiter}nodeVersion=${this._nodeVersion}`); + const granularity: NodeVersionGranularity = + dependsOnNodeVersion === true ? 'patch' : dependsOnNodeVersion; + hasher.update(`${hashDelimiter}nodeVersion=${this._nodeVersionByGranularity[granularity]}`); } if (outputFolderNames) { @@ -437,6 +443,24 @@ export class InputsSnapshot implements IInputsSnapshot { } } +/** + * Parses a Node.js version string once and returns pre-computed strings for each granularity level. + * + * @param rawVersion - The full Node.js version string (e.g. `v18.17.1`) + * @returns An object with pre-computed version strings for `major`, `minor`, and `patch` granularities + */ +function _parseNodeVersion(rawVersion: string): Record { + // Strip leading 'v' if present + const version: string = rawVersion.startsWith('v') ? rawVersion.slice(1) : rawVersion; + const [major, minor]: string[] = version.split('.'); + + return { + major, + minor: `${major}.${minor}`, + patch: version + }; +} + function getOrCreateProjectFilter( record: IInternalInputsSnapshotProjectMetadata ): (filePath: string) => boolean { diff --git a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts index fee54937955..9b18c99efaf 100644 --- a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts +++ b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts @@ -467,6 +467,153 @@ describe(InputsSnapshot.name, () => { expect(result2).not.toEqual(result1); }); + it('Respects dependsOnNodeVersion with major granularity', () => { + const { project, options } = getTestConfig(); + + const projectConfig: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build', + dependsOnNodeVersion: 'major' + } + ] + ]) + }; + + // Same major, different minor — should produce the same hash + const input1: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.0' + }); + + const input2: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.20.3' + }); + + const result1: string = input1.getOperationOwnStateHash(project, '_phase:build'); + const result2: string = input2.getOperationOwnStateHash(project, '_phase:build'); + + expect(result1).toEqual(result2); + + // Different major — should produce a different hash + const input3: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v20.10.0' + }); + + const result3: string = input3.getOperationOwnStateHash(project, '_phase:build'); + + expect(result3).not.toEqual(result1); + }); + + it('Respects dependsOnNodeVersion with minor granularity', () => { + const { project, options } = getTestConfig(); + + const projectConfig: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build', + dependsOnNodeVersion: 'minor' + } + ] + ]) + }; + + // Same major.minor, different patch — should produce the same hash + const input1: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.0' + }); + + const input2: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.5' + }); + + const result1: string = input1.getOperationOwnStateHash(project, '_phase:build'); + const result2: string = input2.getOperationOwnStateHash(project, '_phase:build'); + + expect(result1).toEqual(result2); + + // Different minor — should produce a different hash + const input3: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.20.0' + }); + + const result3: string = input3.getOperationOwnStateHash(project, '_phase:build'); + + expect(result3).not.toEqual(result1); + }); + + it('Respects dependsOnNodeVersion with patch granularity', () => { + const { project, options } = getTestConfig(); + + const projectConfig: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build', + dependsOnNodeVersion: 'patch' + } + ] + ]) + }; + + // true and 'patch' should produce identical hashes + const projectConfigTrue: Pick = { + operationSettingsByOperationName: new Map([ + [ + '_phase:build', + { + operationName: '_phase:build', + dependsOnNodeVersion: true + } + ] + ]) + }; + + const inputPatch: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.1' + }); + + const inputTrue: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfigTrue as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.1' + }); + + const resultPatch: string = inputPatch.getOperationOwnStateHash(project, '_phase:build'); + const resultTrue: string = inputTrue.getOperationOwnStateHash(project, '_phase:build'); + + expect(resultPatch).toEqual(resultTrue); + + // Different patch — should produce a different hash + const input2: InputsSnapshot = new InputsSnapshot({ + ...options, + projectMap: new Map([[project, { projectConfig: projectConfig as RushProjectConfiguration }]]), + nodeVersion: 'v18.17.2' + }); + + const result2: string = input2.getOperationOwnStateHash(project, '_phase:build'); + + expect(result2).not.toEqual(resultPatch); + }); + it('Does not include node version when dependsOnNodeVersion is not set', () => { const { project, options } = getTestConfig(); diff --git a/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap b/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap index 26f7371eb6a..eab9d7c76b8 100644 --- a/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap +++ b/libraries/rush-lib/src/logic/incremental/test/__snapshots__/InputsSnapshot.test.ts.snap @@ -10,9 +10,9 @@ exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnEnvVars 1`] = exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnEnvVars 2`] = `"2c68d56fc9278b6495496070a6a992b929c37a83"`; -exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 1`] = `"df8e519b405d7deca932c0085c1c6b48b57ae4fc"`; +exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 1`] = `"bdfb861c2d1106b68b604b74e13f1c3f95095df6"`; -exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 2`] = `"8323bdc4729024e6f0e4b4378f0f3e6bef96dce2"`; +exports[`InputsSnapshot getOperationOwnStateHash Respects dependsOnNodeVersion 2`] = `"cc182bde6aa81c0410ede7db22f3af2f0a24d3e3"`; exports[`InputsSnapshot getOperationOwnStateHash Respects globalAdditionalFiles 1`] = `"0e0437ad1941bacd098b22da15dc673f86ca6003"`; diff --git a/libraries/rush-lib/src/schemas/rush-project.schema.json b/libraries/rush-lib/src/schemas/rush-project.schema.json index 12317e78a2a..33fb7933403 100644 --- a/libraries/rush-lib/src/schemas/rush-project.schema.json +++ b/libraries/rush-lib/src/schemas/rush-project.schema.json @@ -99,8 +99,11 @@ } }, "dependsOnNodeVersion": { - "description": "If set to true, the Node.js version (process.version) will be included in the hash used for the build cache. This ensures that if the Node.js version changes, cached outputs will be invalidated and the operation will be re-executed. This is useful for projects that produce Node.js-version-specific outputs, such as native module builds.", - "type": "boolean" + "description": "Specifies whether and at what granularity the Node.js version should be included in the hash used for the build cache. When enabled, changing the Node.js version at the specified granularity will invalidate cached outputs and cause the operation to be re-executed. This is useful for projects that produce Node.js-version-specific outputs, such as native module builds. Allowed values: true (alias for 'patch'), 'major' (e.g. '18'), 'minor' (e.g. '18.17'), or 'patch' (e.g. '18.17.1').", + "oneOf": [ + { "type": "boolean", "enum": [true] }, + { "type": "string", "enum": ["major", "minor", "patch"] } + ] }, "weight": { "description": "The number of concurrency units that this operation should take up. The maximum concurrency units is determined by the -p flag.",