feat(dev-infra): publish major versions to "next" NPM dist tag (#42133)
Previously, the dev-infra release tool would publish major versions directly to the NPM `@latest` dist tag. This is correct in theory, but rather unpractical given that we want to publish packages first as `@next` so that other dependent Angular packages can update too, allowing us to publish all main Angular packages (from FW, COMP and TOOL) at the same time to `@latest` on NPM. This involves creating a new release action for re-tagging the previously released major as `@latest` on NPM. PR Close #42133
This commit is contained in:
parent
48f49bacb4
commit
9d75687f62
|
@ -5954,7 +5954,7 @@ class ReleaseAction {
|
|||
this._cachedForkRepo = null;
|
||||
}
|
||||
/** Whether the release action is currently active. */
|
||||
static isActive(_trains) {
|
||||
static isActive(_trains, _config) {
|
||||
throw Error('Not implemented.');
|
||||
}
|
||||
/** Updates the version in the project top-level `package.json` file. */
|
||||
|
@ -6635,7 +6635,17 @@ class CutStableAction extends ReleaseAction {
|
|||
const isNewMajor = (_a = this.active.releaseCandidate) === null || _a === void 0 ? void 0 : _a.isMajor;
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(releaseNotes, branchName, 'latest');
|
||||
// If a new major version is published, we publish to the `next` NPM dist tag temporarily.
|
||||
// We do this because for major versions, we want all main Angular projects to have their
|
||||
// new major become available at the same time. Publishing immediately to the `latest` NPM
|
||||
// dist tag could cause inconsistent versions when users install packages with `@latest`.
|
||||
// For example: Consider Angular Framework releases v12. CLI and Components would need to
|
||||
// wait for that release to complete. Once done, they can update their dependencies to point
|
||||
// to v12. Afterwards they could start the release process. In the meanwhile though, the FW
|
||||
// dependencies were already available as `@latest`, so users could end up installing v12 while
|
||||
// still having the older (but currently still latest) CLI version that is incompatible.
|
||||
// The major release can be re-tagged to `latest` through a separate release action.
|
||||
yield this.buildAndPublish(releaseNotes, branchName, isNewMajor ? 'next' : 'latest');
|
||||
// If a new major version is published and becomes the "latest" release-train, we need
|
||||
// to set the LTS npm dist tag for the previous latest release-train (the current patch).
|
||||
if (isNewMajor) {
|
||||
|
@ -6760,6 +6770,54 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
/**
|
||||
* Release action that tags the recently published major as latest within the NPM
|
||||
* registry. Major versions are published to the `next` NPM dist tag initially and
|
||||
* can be re-tagged to the `latest` NPM dist tag. This allows caretakers to make major
|
||||
* releases available at the same time. e.g. Framework, Tooling and Components
|
||||
* are able to publish v12 to `@latest` at the same time. This wouldn't be possible if
|
||||
* we directly publish to `@latest` because Tooling and Components needs to wait
|
||||
* for the major framework release to be available on NPM.
|
||||
* @see {CutStableAction#perform} for more details.
|
||||
*/
|
||||
class TagRecentMajorAsLatest extends ReleaseAction {
|
||||
getDescription() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
return `Tag recently published major v${this.active.latest.version} as "next" in NPM.`;
|
||||
});
|
||||
}
|
||||
perform() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
yield this.checkoutUpstreamBranch(this.active.latest.branchName);
|
||||
yield invokeYarnInstallCommand(this.projectDir);
|
||||
yield invokeSetNpmDistCommand('latest', this.active.latest.version);
|
||||
});
|
||||
}
|
||||
static isActive({ latest }, config) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
// If the latest release-train does currently not have a major version as version. e.g.
|
||||
// the latest branch is `10.0.x` with the version being `10.0.2`. In such cases, a major
|
||||
// has not been released recently, and this action should never become active.
|
||||
if (latest.version.minor !== 0 || latest.version.patch !== 0) {
|
||||
return false;
|
||||
}
|
||||
const packageInfo = yield fetchProjectNpmPackageInfo(config);
|
||||
const npmLatestVersion = semver.parse(packageInfo['dist-tags']['latest']);
|
||||
// This action only becomes active if a major just has been released recently, but is
|
||||
// not set to the `latest` NPM dist tag in the NPM registry. Note that we only allow
|
||||
// re-tagging if the current `@latest` in NPM is the previous major version.
|
||||
return npmLatestVersion !== null && npmLatestVersion.major === latest.version.major - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -6772,6 +6830,7 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
* by priority. Actions which are selectable are sorted based on this declaration order.
|
||||
*/
|
||||
const actions = [
|
||||
TagRecentMajorAsLatest,
|
||||
CutStableAction,
|
||||
CutReleaseCandidateAction,
|
||||
CutNewPatchAction,
|
||||
|
@ -6860,7 +6919,7 @@ class ReleaseTool {
|
|||
const choices = [];
|
||||
// Find and instantiate all release actions which are currently valid.
|
||||
for (let actionType of actions) {
|
||||
if (yield actionType.isActive(activeTrains)) {
|
||||
if (yield actionType.isActive(activeTrains, this._config)) {
|
||||
const action = new actionType(activeTrains, this._git, this._config, this._projectRoot);
|
||||
choices.push({ name: yield action.getDescription(), value: action });
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export interface PullRequest {
|
|||
/** Constructor type for instantiating a release action */
|
||||
export interface ReleaseActionConstructor<T extends ReleaseAction = ReleaseAction> {
|
||||
/** Whether the release action is currently active. */
|
||||
isActive(active: ActiveReleaseTrains): Promise<boolean>;
|
||||
isActive(active: ActiveReleaseTrains, config: ReleaseConfig): Promise<boolean>;
|
||||
/** Constructs a release action. */
|
||||
new(...args: [ActiveReleaseTrains, GitClient<true>, ReleaseConfig, string]): T;
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export interface ReleaseActionConstructor<T extends ReleaseAction = ReleaseActio
|
|||
*/
|
||||
export abstract class ReleaseAction {
|
||||
/** Whether the release action is currently active. */
|
||||
static isActive(_trains: ActiveReleaseTrains): Promise<boolean> {
|
||||
static isActive(_trains: ActiveReleaseTrains, _config: ReleaseConfig): Promise<boolean> {
|
||||
throw Error('Not implemented.');
|
||||
}
|
||||
|
||||
|
|
|
@ -30,12 +30,22 @@ export class CutStableAction extends ReleaseAction {
|
|||
const newVersion = this._newVersion;
|
||||
const isNewMajor = this.active.releaseCandidate?.isMajor;
|
||||
|
||||
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(releaseNotes, branchName, 'latest');
|
||||
|
||||
// If a new major version is published, we publish to the `next` NPM dist tag temporarily.
|
||||
// We do this because for major versions, we want all main Angular projects to have their
|
||||
// new major become available at the same time. Publishing immediately to the `latest` NPM
|
||||
// dist tag could cause inconsistent versions when users install packages with `@latest`.
|
||||
// For example: Consider Angular Framework releases v12. CLI and Components would need to
|
||||
// wait for that release to complete. Once done, they can update their dependencies to point
|
||||
// to v12. Afterwards they could start the release process. In the meanwhile though, the FW
|
||||
// dependencies were already available as `@latest`, so users could end up installing v12 while
|
||||
// still having the older (but currently still latest) CLI version that is incompatible.
|
||||
// The major release can be re-tagged to `latest` through a separate release action.
|
||||
await this.buildAndPublish(releaseNotes, branchName, isNewMajor ? 'next' : 'latest');
|
||||
|
||||
// If a new major version is published and becomes the "latest" release-train, we need
|
||||
// to set the LTS npm dist tag for the previous latest release-train (the current patch).
|
||||
|
|
|
@ -14,12 +14,14 @@ import {CutNextPrereleaseAction} from './cut-next-prerelease';
|
|||
import {CutReleaseCandidateAction} from './cut-release-candidate';
|
||||
import {CutStableAction} from './cut-stable';
|
||||
import {MoveNextIntoFeatureFreezeAction} from './move-next-into-feature-freeze';
|
||||
import {TagRecentMajorAsLatest} from './tag-recent-major-as-latest';
|
||||
|
||||
/**
|
||||
* List of release actions supported by the release staging tool. These are sorted
|
||||
* by priority. Actions which are selectable are sorted based on this declaration order.
|
||||
*/
|
||||
export const actions: ReleaseActionConstructor[] = [
|
||||
TagRecentMajorAsLatest,
|
||||
CutStableAction,
|
||||
CutReleaseCandidateAction,
|
||||
CutNewPatchAction,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as semver from 'semver';
|
||||
|
||||
import {ReleaseConfig} from '../../config';
|
||||
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
||||
import {fetchProjectNpmPackageInfo} from '../../versioning/npm-registry';
|
||||
import {ReleaseAction} from '../actions';
|
||||
import {invokeSetNpmDistCommand, invokeYarnInstallCommand} from '../external-commands';
|
||||
|
||||
/**
|
||||
* Release action that tags the recently published major as latest within the NPM
|
||||
* registry. Major versions are published to the `next` NPM dist tag initially and
|
||||
* can be re-tagged to the `latest` NPM dist tag. This allows caretakers to make major
|
||||
* releases available at the same time. e.g. Framework, Tooling and Components
|
||||
* are able to publish v12 to `@latest` at the same time. This wouldn't be possible if
|
||||
* we directly publish to `@latest` because Tooling and Components needs to wait
|
||||
* for the major framework release to be available on NPM.
|
||||
* @see {CutStableAction#perform} for more details.
|
||||
*/
|
||||
export class TagRecentMajorAsLatest extends ReleaseAction {
|
||||
async getDescription() {
|
||||
return `Tag recently published major v${this.active.latest.version} as "next" in NPM.`;
|
||||
}
|
||||
|
||||
async perform() {
|
||||
await this.checkoutUpstreamBranch(this.active.latest.branchName);
|
||||
await invokeYarnInstallCommand(this.projectDir);
|
||||
await invokeSetNpmDistCommand('latest', this.active.latest.version);
|
||||
}
|
||||
|
||||
static async isActive({latest}: ActiveReleaseTrains, config: ReleaseConfig) {
|
||||
// If the latest release-train does currently not have a major version as version. e.g.
|
||||
// the latest branch is `10.0.x` with the version being `10.0.2`. In such cases, a major
|
||||
// has not been released recently, and this action should never become active.
|
||||
if (latest.version.minor !== 0 || latest.version.patch !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageInfo = await fetchProjectNpmPackageInfo(config);
|
||||
const npmLatestVersion = semver.parse(packageInfo['dist-tags']['latest']);
|
||||
// This action only becomes active if a major just has been released recently, but is
|
||||
// not set to the `latest` NPM dist tag in the NPM registry. Note that we only allow
|
||||
// re-tagging if the current `@latest` in NPM is the previous major version.
|
||||
return npmLatestVersion !== null && npmLatestVersion.major === latest.version.major - 1;
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ export class ReleaseTool {
|
|||
|
||||
// Find and instantiate all release actions which are currently valid.
|
||||
for (let actionType of actions) {
|
||||
if (await actionType.isActive(activeTrains)) {
|
||||
if (await actionType.isActive(activeTrains, this._config)) {
|
||||
const action: ReleaseAction =
|
||||
new actionType(activeTrains, this._git, this._config, this._projectRoot);
|
||||
choices.push({name: await action.getDescription(), value: action});
|
||||
|
|
|
@ -77,7 +77,9 @@ describe('cut stable action', () => {
|
|||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await expectStagingAndPublishWithCherryPick(action, '11.0.x', '11.0.0', 'latest');
|
||||
// Major is released to the `next` NPM dist tag initially. Can be re-tagged with
|
||||
// a separate release action. See `CutStableAction` for more details.
|
||||
await expectStagingAndPublishWithCherryPick(action, '11.0.x', '11.0.0', 'next');
|
||||
expect(externalCommands.invokeSetNpmDistCommand).toHaveBeenCalledTimes(1);
|
||||
expect(externalCommands.invokeSetNpmDistCommand)
|
||||
.toHaveBeenCalledWith('v10-lts', matchesVersion('10.0.3'));
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {matchesVersion} from '../../../utils/testing';
|
||||
import {ReleaseTrain} from '../../versioning/release-trains';
|
||||
import {TagRecentMajorAsLatest} from '../actions/tag-recent-major-as-latest';
|
||||
import * as externalCommands from '../external-commands';
|
||||
|
||||
import {fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting} from './test-utils';
|
||||
|
||||
describe('tag recent major as latest action', () => {
|
||||
it('should not be active if a patch has been published after major release', async () => {
|
||||
const {releaseConfig} = getTestingMocksForReleaseAction();
|
||||
expect(await TagRecentMajorAsLatest.isActive(
|
||||
{
|
||||
releaseCandidate: null,
|
||||
next: new ReleaseTrain('master', parse('10.1.0-next.0')),
|
||||
latest: new ReleaseTrain('10.0.x', parse('10.0.1')),
|
||||
},
|
||||
releaseConfig))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should not be active if a major has been released recently but "@latest" on NPM points to ' +
|
||||
'a more recent major',
|
||||
async () => {
|
||||
const {releaseConfig} = getTestingMocksForReleaseAction();
|
||||
|
||||
// NPM `@latest` will point to a patch release of a more recent major. This is unlikely
|
||||
// to happen (only with manual changes outside of the release tool), but should
|
||||
// prevent accidental overrides from the release action.
|
||||
fakeNpmPackageQueryRequest(
|
||||
releaseConfig.npmPackages[0], {'dist-tags': {'latest': '11.0.3'}});
|
||||
|
||||
expect(await TagRecentMajorAsLatest.isActive(
|
||||
{
|
||||
releaseCandidate: null,
|
||||
next: new ReleaseTrain('master', parse('10.1.0-next.0')),
|
||||
latest: new ReleaseTrain('10.0.x', parse('10.0.0')),
|
||||
},
|
||||
releaseConfig))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should not be active if a major has been released recently but "@latest" on NPM points to ' +
|
||||
'an older major',
|
||||
async () => {
|
||||
const {releaseConfig} = getTestingMocksForReleaseAction();
|
||||
|
||||
// NPM `@latest` will point to a patch release of an older major. This is unlikely to happen
|
||||
// (only with manual changes outside of the release tool), but should prevent accidental
|
||||
// changes from the release action that indicate mismatched version branches, or an
|
||||
// out-of-sync NPM registry.
|
||||
fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '8.4.7'}});
|
||||
|
||||
expect(await TagRecentMajorAsLatest.isActive(
|
||||
{
|
||||
releaseCandidate: null,
|
||||
next: new ReleaseTrain('master', parse('10.1.0-next.0')),
|
||||
latest: new ReleaseTrain('10.0.x', parse('10.0.0')),
|
||||
},
|
||||
releaseConfig))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should be active if a major has been released recently but is not published as ' +
|
||||
'"@latest" to NPM',
|
||||
async () => {
|
||||
const {releaseConfig} = getTestingMocksForReleaseAction();
|
||||
|
||||
// NPM `@latest` will point to a patch release of the previous major.
|
||||
fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '9.2.3'}});
|
||||
|
||||
expect(await TagRecentMajorAsLatest.isActive(
|
||||
{
|
||||
releaseCandidate: null,
|
||||
next: new ReleaseTrain('master', parse('10.1.0-next.0')),
|
||||
latest: new ReleaseTrain('10.0.x', parse('10.0.0')),
|
||||
},
|
||||
releaseConfig))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should be active if a major has been released recently but is not published as ' +
|
||||
'"@latest" to NPM',
|
||||
async () => {
|
||||
const {instance, gitClient, releaseConfig} =
|
||||
setupReleaseActionForTesting(TagRecentMajorAsLatest, {
|
||||
releaseCandidate: null,
|
||||
next: new ReleaseTrain('master', parse('10.1.0-next.0')),
|
||||
latest: new ReleaseTrain('10.0.x', parse('10.0.0')),
|
||||
});
|
||||
|
||||
// NPM `@latest` will point to a patch release of the previous major.
|
||||
fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '9.2.3'}});
|
||||
|
||||
await instance.perform();
|
||||
|
||||
// Ensure that the NPM dist tag is set only for packages that were available in the previous
|
||||
// major version. A spy has already been installed on the function.
|
||||
(externalCommands.invokeSetNpmDistCommand as jasmine.Spy).and.callFake(() => {
|
||||
expect(gitClient.head.ref?.name).toBe('10.0.x');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
expect(externalCommands.invokeSetNpmDistCommand).toHaveBeenCalledTimes(1);
|
||||
expect(externalCommands.invokeSetNpmDistCommand)
|
||||
.toHaveBeenCalledWith('latest', matchesVersion('10.0.0'));
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue