diff --git a/common/changes/@microsoft/rush/version-policy-exempt_2025-08-19-21-32.json b/common/changes/@microsoft/rush/version-policy-exempt_2025-08-19-21-32.json new file mode 100644 index 00000000000..610e4cf2210 --- /dev/null +++ b/common/changes/@microsoft/rush/version-policy-exempt_2025-08-19-21-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Ensure that `rush version` and `rush publish` preserve all fields in `version-policies-json`.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 798296124b0..f3ffee7a543 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -538,6 +538,12 @@ export interface IGetChangedProjectsOptions { export interface IGlobalCommand extends IRushCommand { } +// @public +export interface IIndividualVersionJson extends IVersionPolicyJson { + // (undocumented) + lockedMajor?: number; +} + // @beta export interface IInputsSnapshot { getOperationOwnStateHash(project: IRushConfigurationProjectForSnapshot, operationName?: string): string; @@ -556,6 +562,16 @@ export interface ILaunchOptions { terminalProvider?: ITerminalProvider; } +// @public +export interface ILockStepVersionJson extends IVersionPolicyJson { + // (undocumented) + mainProject?: string; + // (undocumented) + nextBump?: string; + // (undocumented) + version: string; +} + // @alpha export interface ILogFilePaths { error: string; @@ -575,15 +591,13 @@ export interface ILogger { // @public export class IndividualVersionPolicy extends VersionPolicy { - // Warning: (ae-forgotten-export) The symbol "IIndividualVersionJson" needs to be exported by the entry point index.d.ts - // // @internal constructor(versionPolicyJson: IIndividualVersionJson); bump(bumpType?: BumpType, identifier?: string): void; ensure(project: IPackageJson, force?: boolean): IPackageJson | undefined; - // @internal - get _json(): IIndividualVersionJson; - readonly lockedMajor: number | undefined; + // @internal (undocumented) + readonly _json: IIndividualVersionJson; + get lockedMajor(): number | undefined; validate(versionString: string, packageName: string): void; } @@ -922,6 +936,22 @@ export interface ITryFindRushJsonLocationOptions { startingFolder?: string; } +// @public +export interface IVersionPolicyJson { + // (undocumented) + definitionName: string; + // Warning: (ae-forgotten-export) The symbol "IVersionPolicyDependencyJson" needs to be exported by the entry point index.d.ts + // + // (undocumented) + dependencies?: IVersionPolicyDependencyJson; + // (undocumented) + exemptFromRushChange?: boolean; + // (undocumented) + includeEmailInChangeFile?: boolean; + // (undocumented) + policyName: string; +} + // @internal export interface _IYarnOptionsJson extends IPackageManagerOptionsJsonBase { ignoreEngines?: boolean; @@ -929,16 +959,14 @@ export interface _IYarnOptionsJson extends IPackageManagerOptionsJsonBase { // @public export class LockStepVersionPolicy extends VersionPolicy { - // Warning: (ae-forgotten-export) The symbol "ILockStepVersionJson" needs to be exported by the entry point index.d.ts - // // @internal constructor(versionPolicyJson: ILockStepVersionJson); bump(bumpType?: BumpType, identifier?: string): void; ensure(project: IPackageJson, force?: boolean): IPackageJson | undefined; - // @internal - get _json(): ILockStepVersionJson; - readonly mainProject: string | undefined; - readonly nextBump: BumpType | undefined; + // @internal (undocumented) + readonly _json: ILockStepVersionJson; + get mainProject(): string | undefined; + get nextBump(): BumpType | undefined; update(newVersionString: string): boolean; validate(versionString: string, packageName: string): void; get version(): string; @@ -1603,21 +1631,19 @@ export class SubspacesConfiguration { // @public export abstract class VersionPolicy { - // Warning: (ae-forgotten-export) The symbol "IVersionPolicyJson" needs to be exported by the entry point index.d.ts - // // @internal constructor(versionPolicyJson: IVersionPolicyJson); abstract bump(bumpType?: BumpType, identifier?: string): void; - readonly definitionName: VersionPolicyDefinitionName; + get definitionName(): VersionPolicyDefinitionName; abstract ensure(project: IPackageJson, force?: boolean): IPackageJson | undefined; - readonly exemptFromRushChange: boolean; - readonly includeEmailInChangeFile: boolean; + get exemptFromRushChange(): boolean; + get includeEmailInChangeFile(): boolean; get isLockstepped(): boolean; // @internal - abstract get _json(): IVersionPolicyJson; + readonly _json: IVersionPolicyJson; // @internal static load(versionPolicyJson: IVersionPolicyJson): VersionPolicy | undefined; - readonly policyName: string; + get policyName(): string; setDependenciesBeforeCommit(packageName: string, configuration: RushConfiguration): void; setDependenciesBeforePublish(packageName: string, configuration: RushConfiguration): void; abstract validate(versionString: string, packageName: string): void; diff --git a/libraries/rush-lib/src/api/VersionPolicy.ts b/libraries/rush-lib/src/api/VersionPolicy.ts index 2b1986c7943..f2a18967836 100644 --- a/libraries/rush-lib/src/api/VersionPolicy.ts +++ b/libraries/rush-lib/src/api/VersionPolicy.ts @@ -4,13 +4,12 @@ import * as semver from 'semver'; import { type IPackageJson, Enum } from '@rushstack/node-core-library'; -import { - type IVersionPolicyJson, - type ILockStepVersionJson, - type IIndividualVersionJson, +import type { + IVersionPolicyJson, + ILockStepVersionJson, + IIndividualVersionJson, VersionFormatForCommit, - VersionFormatForPublish, - type IVersionPolicyDependencyJson + VersionFormatForPublish } from './VersionPolicyConfiguration'; import type { PackageJsonEditor } from './PackageJsonEditor'; import type { RushConfiguration } from './RushConfiguration'; @@ -57,42 +56,54 @@ export enum VersionPolicyDefinitionName { * @public */ export abstract class VersionPolicy { - private _versionFormatForCommit: VersionFormatForCommit; - private _versionFormatForPublish: VersionFormatForPublish; + /** + * Serialized json for the policy + * + * @internal + */ + public readonly _json: IVersionPolicyJson; + + private get _versionFormatForCommit(): VersionFormatForCommit { + return this._json.dependencies?.versionFormatForCommit ?? 'original'; + } + + private get _versionFormatForPublish(): VersionFormatForPublish { + return this._json.dependencies?.versionFormatForPublish ?? 'original'; + } /** * Version policy name */ - public readonly policyName: string; + public get policyName(): string { + return this._json.policyName; + } /** * Version policy definition name */ - public readonly definitionName: VersionPolicyDefinitionName; + public get definitionName(): VersionPolicyDefinitionName { + return Enum.getValueByKey(VersionPolicyDefinitionName, this._json.definitionName); + } /** * Determines if a version policy wants to opt out of changelog files. */ - public readonly exemptFromRushChange: boolean; + public get exemptFromRushChange(): boolean { + return this._json.exemptFromRushChange ?? false; + } /** * Determines if a version policy wants to opt in to including email. */ - public readonly includeEmailInChangeFile: boolean; + public get includeEmailInChangeFile(): boolean { + return this._json.includeEmailInChangeFile ?? false; + } /** * @internal */ public constructor(versionPolicyJson: IVersionPolicyJson) { - this.policyName = versionPolicyJson.policyName; - this.definitionName = Enum.getValueByKey(VersionPolicyDefinitionName, versionPolicyJson.definitionName); - this.exemptFromRushChange = versionPolicyJson.exemptFromRushChange || false; - this.includeEmailInChangeFile = versionPolicyJson.includeEmailInChangeFile || false; - - const jsonDependencies: IVersionPolicyDependencyJson = versionPolicyJson.dependencies || {}; - this._versionFormatForCommit = jsonDependencies.versionFormatForCommit || VersionFormatForCommit.original; - this._versionFormatForPublish = - jsonDependencies.versionFormatForPublish || VersionFormatForPublish.original; + this._json = versionPolicyJson; } /** @@ -140,13 +151,6 @@ export abstract class VersionPolicy { */ public abstract bump(bumpType?: BumpType, identifier?: string): void; - /** - * Serialized json for the policy - * - * @internal - */ - public abstract get _json(): IVersionPolicyJson; - /** * Validates the specified version and throws if the version does not satisfy the policy. * @@ -160,7 +164,7 @@ export abstract class VersionPolicy { * to values used for publishing. */ public setDependenciesBeforePublish(packageName: string, configuration: RushConfiguration): void { - if (this._versionFormatForPublish === VersionFormatForPublish.exact) { + if (this._versionFormatForPublish === 'exact') { const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; @@ -186,7 +190,7 @@ export abstract class VersionPolicy { * to values used for checked-in source. */ public setDependenciesBeforeCommit(packageName: string, configuration: RushConfiguration): void { - if (this._versionFormatForCommit === VersionFormatForCommit.wildcard) { + if (this._versionFormatForCommit === 'wildcard') { const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; @@ -211,6 +215,10 @@ export abstract class VersionPolicy { * @public */ export class LockStepVersionPolicy extends VersionPolicy { + /** + * @internal + */ + public declare readonly _json: ILockStepVersionJson; private _version: semver.SemVer; /** @@ -218,7 +226,9 @@ export class LockStepVersionPolicy extends VersionPolicy { */ // nextBump is probably not needed. It can be prerelease only. // Other types of bumps can be passed in as a parameter to bump method, so can identifier. - public readonly nextBump: BumpType | undefined; + public get nextBump(): BumpType | undefined { + return this._json.nextBump !== undefined ? Enum.getValueByKey(BumpType, this._json.nextBump) : undefined; + } /** * The main project for the version policy. @@ -226,7 +236,9 @@ export class LockStepVersionPolicy extends VersionPolicy { * If the value is provided, change logs will only be generated in that project. * If the value is not provided, change logs will be hosted in each project associated with the policy. */ - public readonly mainProject: string | undefined; + public get mainProject(): string | undefined { + return this._json.mainProject; + } /** * @internal @@ -234,11 +246,6 @@ export class LockStepVersionPolicy extends VersionPolicy { public constructor(versionPolicyJson: ILockStepVersionJson) { super(versionPolicyJson); this._version = new semver.SemVer(versionPolicyJson.version); - this.nextBump = - versionPolicyJson.nextBump !== undefined - ? Enum.getValueByKey(BumpType, versionPolicyJson.nextBump) - : undefined; - this.mainProject = versionPolicyJson.mainProject; } /** @@ -248,26 +255,6 @@ export class LockStepVersionPolicy extends VersionPolicy { return this._version.format(); } - /** - * Serialized json for this policy - * - * @internal - */ - public get _json(): ILockStepVersionJson { - const json: ILockStepVersionJson = { - policyName: this.policyName, - definitionName: VersionPolicyDefinitionName[this.definitionName], - version: this.version - }; - if (this.nextBump !== undefined) { - json.nextBump = BumpType[this.nextBump]; - } - if (this.mainProject !== undefined) { - json.mainProject = this.mainProject; - } - return json; - } - /** * Returns an updated package json that satisfies the version policy. * @@ -303,6 +290,7 @@ export class LockStepVersionPolicy extends VersionPolicy { } this._version.inc(this._getReleaseType(nextBump), identifier); + this._json.version = this.version; } /** @@ -315,6 +303,7 @@ export class LockStepVersionPolicy extends VersionPolicy { return false; } this._version = newVersion; + this._json.version = this.version; return true; } @@ -349,32 +338,22 @@ export class LockStepVersionPolicy extends VersionPolicy { */ export class IndividualVersionPolicy extends VersionPolicy { /** - * The major version that has been locked + * @internal */ - public readonly lockedMajor: number | undefined; + public declare readonly _json: IIndividualVersionJson; /** - * @internal + * The major version that has been locked */ - public constructor(versionPolicyJson: IIndividualVersionJson) { - super(versionPolicyJson); - this.lockedMajor = versionPolicyJson.lockedMajor; + public get lockedMajor(): number | undefined { + return this._json.lockedMajor; } /** - * Serialized json for this policy - * * @internal */ - public get _json(): IIndividualVersionJson { - const json: IIndividualVersionJson = { - policyName: this.policyName, - definitionName: VersionPolicyDefinitionName[this.definitionName] - }; - if (this.lockedMajor !== undefined) { - json.lockedMajor = this.lockedMajor; - } - return json; + public constructor(versionPolicyJson: IIndividualVersionJson) { + super(versionPolicyJson); } /** diff --git a/libraries/rush-lib/src/api/VersionPolicyConfiguration.ts b/libraries/rush-lib/src/api/VersionPolicyConfiguration.ts index 01382f02852..eff69dc2faa 100644 --- a/libraries/rush-lib/src/api/VersionPolicyConfiguration.ts +++ b/libraries/rush-lib/src/api/VersionPolicyConfiguration.ts @@ -7,6 +7,12 @@ import { VersionPolicy, type BumpType, type LockStepVersionPolicy } from './Vers import type { RushConfigurationProject } from './RushConfigurationProject'; import schemaJson from '../schemas/version-policies.schema.json'; +/** + * This interface represents the raw version policy JSON object which allows repo + * maintainers to define how different groups of projects will be published by Rush, + * and how their version numbers will be determined. + * @public + */ export interface IVersionPolicyJson { policyName: string; definitionName: string; @@ -15,26 +21,42 @@ export interface IVersionPolicyJson { includeEmailInChangeFile?: boolean; } +/** + * This interface represents the raw lock-step version policy JSON object which extends the base version policy + * with additional fields specific to lock-step versioning. + * @public + */ export interface ILockStepVersionJson extends IVersionPolicyJson { version: string; nextBump?: string; mainProject?: string; } +/** + * This interface represents the raw individual version policy JSON object which extends the base version policy + * with additional fields specific to individual versioning. + * @public + */ export interface IIndividualVersionJson extends IVersionPolicyJson { lockedMajor?: number; } -export enum VersionFormatForPublish { - original = 'original', - exact = 'exact' -} +/** + * @public + */ +export type VersionFormatForPublish = 'original' | 'exact'; -export enum VersionFormatForCommit { - wildcard = 'wildcard', - original = 'original' -} +/** + * @public + */ +export type VersionFormatForCommit = 'wildcard' | 'original'; +/** + * This interface represents the `dependencies` field in a version policy JSON object, + * allowing repo maintainers to specify how dependencies' versions should be handled + * during publishing and committing. + * @public + */ export interface IVersionPolicyDependencyJson { versionFormatForPublish?: VersionFormatForPublish; versionFormatForCommit?: VersionFormatForCommit; diff --git a/libraries/rush-lib/src/api/test/VersionPolicy.test.ts b/libraries/rush-lib/src/api/test/VersionPolicy.test.ts index 5371bcff7b1..738798be560 100644 --- a/libraries/rush-lib/src/api/test/VersionPolicy.test.ts +++ b/libraries/rush-lib/src/api/test/VersionPolicy.test.ts @@ -3,7 +3,11 @@ import type { IPackageJson } from '@rushstack/node-core-library'; -import { VersionPolicyConfiguration } from '../VersionPolicyConfiguration'; +import { + type ILockStepVersionJson, + VersionPolicyConfiguration, + type IIndividualVersionJson +} from '../VersionPolicyConfiguration'; import { VersionPolicy, LockStepVersionPolicy, IndividualVersionPolicy, BumpType } from '../VersionPolicy'; describe(VersionPolicy.name, () => { @@ -113,6 +117,25 @@ describe(VersionPolicy.name, () => { lockStepVersionPolicy.update(newVersion); expect(lockStepVersionPolicy.version).toEqual(newVersion); }); + + it('preserves fields', () => { + const originalJson: ILockStepVersionJson = { + definitionName: 'lockStepVersion', + policyName: 'test', + dependencies: { + versionFormatForCommit: 'original', + versionFormatForPublish: 'original' + }, + exemptFromRushChange: true, + includeEmailInChangeFile: true, + version: '1.1.0', + mainProject: 'main-project', + nextBump: 'major' + }; + + const nextJson: ILockStepVersionJson = new LockStepVersionPolicy(originalJson)._json; + expect(nextJson).toMatchObject(originalJson); + }); }); describe(IndividualVersionPolicy.name, () => { @@ -159,5 +182,22 @@ describe(VersionPolicy.name, () => { individualVersionPolicy.ensure(originalPackageJson); }).toThrow(); }); + + it('preserves fields', () => { + const originalJson: IIndividualVersionJson = { + definitionName: 'individualVersion', + policyName: 'test', + dependencies: { + versionFormatForCommit: 'wildcard', + versionFormatForPublish: 'exact' + }, + exemptFromRushChange: true, + includeEmailInChangeFile: true, + lockedMajor: 3 + }; + + const nextJson: IIndividualVersionJson = new IndividualVersionPolicy(originalJson)._json; + expect(nextJson).toMatchObject(originalJson); + }); }); }); diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 685720e0629..85a59f9ad77 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -107,7 +107,12 @@ export { VersionPolicy } from './api/VersionPolicy'; -export { VersionPolicyConfiguration } from './api/VersionPolicyConfiguration'; +export { + VersionPolicyConfiguration, + type ILockStepVersionJson, + type IIndividualVersionJson, + type IVersionPolicyJson +} from './api/VersionPolicyConfiguration'; export { type ILaunchOptions, Rush } from './api/Rush'; export { RushInternals as _RushInternals } from './api/RushInternals';