From 047994b0483f9c2bcca5c77d6bb58ce0c274c63f Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 27 Jul 2021 21:45:28 +0200 Subject: [PATCH] feat(dev-infra): introduce release action for directly branching-off into RC (#42973) Introduces a new release action for cutting a release-action by directly moving the next release-train into the `release-candidate` phase. This allows the Angular team to release minor versions without needing to branch-off first into the feature-freeze phase. For minors this phase can be skipped. Switching into the feature-freeze phase beforehand as a workaround would have allowed for branching-off but has the downside that `target: minor` would no longer point to the branched-off release train (only `target: rc` would work then). PR Close #42973 --- dev-infra/ng-dev.js | 95 +++++++++++--- .../publish/actions/branch-off-next-branch.ts | 120 ++++++++++++++++++ ...t-release-candidate-for-feature-freeze.ts} | 2 +- dev-infra/release/publish/actions/index.ts | 6 +- .../actions/move-next-into-feature-freeze.ts | 98 ++------------ .../move-next-into-release-candidate.ts | 27 ++++ .../test/branch-off-next-branch-testing.ts | 103 +++++++++++++++ ...ease-candidate-for-feature-freeze.spec.ts} | 12 +- .../move-next-into-feature-freeze.spec.ts | 108 +++------------- .../move-next-into-release-candidate.spec.ts | 58 +++++++++ 10 files changed, 419 insertions(+), 210 deletions(-) create mode 100644 dev-infra/release/publish/actions/branch-off-next-branch.ts rename dev-infra/release/publish/actions/{cut-release-candidate.ts => cut-release-candidate-for-feature-freeze.ts} (95%) create mode 100644 dev-infra/release/publish/actions/move-next-into-release-candidate.ts create mode 100644 dev-infra/release/publish/test/branch-off-next-branch-testing.ts rename dev-infra/release/publish/test/{cut-release-candidate.spec.ts => cut-release-candidate-for-feature-freeze.spec.ts} (80%) create mode 100644 dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js index d97df3d361..52f3604c3e 100755 --- a/dev-infra/ng-dev.js +++ b/dev-infra/ng-dev.js @@ -7094,7 +7094,7 @@ class CutNextPrereleaseAction extends ReleaseAction { * Cuts the first release candidate for a release-train currently in the * feature-freeze phase. The version is bumped from `next` to `rc.0`. */ -class CutReleaseCandidateAction extends ReleaseAction { +class CutReleaseCandidateForFeatureFreezeAction extends ReleaseAction { constructor() { super(...arguments); this._newVersion = semverInc(this.active.releaseCandidate.version, 'prerelease', 'rc'); @@ -7208,27 +7208,23 @@ class CutStableAction extends ReleaseAction { * found in the LICENSE file at https://angular.io/license */ /** - * Release action that moves the next release-train into the feature-freeze phase. This means - * that a new version branch is created from the next branch, and a new next pre-release is - * cut indicating the started feature-freeze. + * Base action that can be used to move the next release-train into the feature-freeze or + * release-candidate phase. This means that a new version branch is created from the next + * branch, and a new pre-release (either RC or another `next`) is cut indicating the new phase. */ -class MoveNextIntoFeatureFreezeAction extends ReleaseAction { - constructor() { - super(...arguments); - this._newVersion = computeNewPrereleaseVersionForNext(this.active, this.config); - } +class BranchOffNextBranchBaseAction extends ReleaseAction { getDescription() { return tslib.__awaiter(this, void 0, void 0, function* () { const { branchName } = this.active.next; - const newVersion = yield this._newVersion; - return `Move the "${branchName}" branch into feature-freeze phase (v${newVersion}).`; + const newVersion = yield this._computeNewVersion(); + return `Move the "${branchName}" branch into ${this.newPhaseName} phase (v${newVersion}).`; }); } perform() { return tslib.__awaiter(this, void 0, void 0, function* () { - const newVersion = yield this._newVersion; + const newVersion = yield this._computeNewVersion(); const newBranch = `${newVersion.major}.${newVersion.minor}.x`; - // Branch-off the next branch into a feature-freeze branch. + // Branch-off the next branch into a new version branch. yield this._createNewVersionBranchFromNext(newBranch); // Stage the new version for the newly created branch, and push changes to a // fork in order to create a staging pull request. Note that we re-use the newly @@ -7242,6 +7238,17 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction { yield this._createNextBranchUpdatePullRequest(releaseNotes, newVersion); }); } + /** Computes the new version for the release-train being branched-off. */ + _computeNewVersion() { + return tslib.__awaiter(this, void 0, void 0, function* () { + if (this.newPhaseName === 'feature-freeze') { + return computeNewPrereleaseVersionForNext(this.active, this.config); + } + else { + return semverInc(this.active.next.version, 'prerelease', 'rc'); + } + }); + } /** Creates a new version branch from the next branch. */ _createNewVersionBranchFromNext(newBranch) { return tslib.__awaiter(this, void 0, void 0, function* () { @@ -7255,7 +7262,8 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction { } /** * Creates a pull request for the next branch that bumps the version to the next - * minor, and cherry-picks the changelog for the newly branched-off feature-freeze version. + * minor, and cherry-picks the changelog for the newly branched-off release-candidate + * or feature-freeze version. */ _createNextBranchUpdatePullRequest(releaseNotes, newVersion) { return tslib.__awaiter(this, void 0, void 0, function* () { @@ -7273,7 +7281,7 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction { const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); yield this.createCommit(commitMessage, [changelogPath]); let nextPullRequestMessage = `The previous "next" release-train has moved into the ` + - `release-candidate phase. This PR updates the next branch to the subsequent ` + + `${this.newPhaseName} phase. This PR updates the next branch to the subsequent ` + `release-train.\n\nAlso this PR cherry-picks the changelog for ` + `v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`; const nextUpdatePullRequest = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `next-release-train-${newNextVersion}`, `Update next branch to reflect new release-train "v${newNextVersion}".`, nextPullRequestMessage); @@ -7281,11 +7289,59 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction { info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`)); }); } +} + +/** + * @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 moves the next release-train into the feature-freeze phase. This means + * that a new version branch is created from the next branch, and a new next pre-release is + * cut indicating the started feature-freeze. + */ +class MoveNextIntoFeatureFreezeAction extends BranchOffNextBranchBaseAction { + constructor() { + super(...arguments); + this.newPhaseName = 'feature-freeze'; + } static isActive(active) { return tslib.__awaiter(this, void 0, void 0, function* () { - // A new feature-freeze/release-candidate branch can only be created if there - // is no active release-train in feature-freeze/release-candidate phase. - return active.releaseCandidate === null; + // A new feature-freeze branch can only be created if there is no active + // release-train in feature-freeze/release-candidate phase and the version + // currently in the `next` branch is for a major. The feature-freeze phase + // is not foreseen for minor versions. + return active.releaseCandidate === null && active.next.isMajor; + }); + } +} + +/** + * @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 moves the next release-train into the release-candidate phase. This means + * that a new version branch is created from the next branch, and the first release candidate + * version is cut indicating the new phase. + */ +class MoveNextIntoReleaseCandidateAction extends BranchOffNextBranchBaseAction { + constructor() { + super(...arguments); + this.newPhaseName = 'release-candidate'; + } + static isActive(active) { + return tslib.__awaiter(this, void 0, void 0, function* () { + // Directly switching a next release-train into the `release-candidate` + // phase is only allowed for minor releases. Major version always need to + // go through the `feature-freeze` phase. + return active.releaseCandidate === null && !active.next.isMajor; }); } } @@ -7352,10 +7408,11 @@ class TagRecentMajorAsLatest extends ReleaseAction { const actions = [ TagRecentMajorAsLatest, CutStableAction, - CutReleaseCandidateAction, + CutReleaseCandidateForFeatureFreezeAction, CutNewPatchAction, CutNextPrereleaseAction, MoveNextIntoFeatureFreezeAction, + MoveNextIntoReleaseCandidateAction, CutLongTermSupportPatchAction, ]; diff --git a/dev-infra/release/publish/actions/branch-off-next-branch.ts b/dev-infra/release/publish/actions/branch-off-next-branch.ts new file mode 100644 index 0000000000..5b64b3a6e1 --- /dev/null +++ b/dev-infra/release/publish/actions/branch-off-next-branch.ts @@ -0,0 +1,120 @@ +/** + * @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 {green, info, yellow} from '../../../utils/console'; +import {semverInc} from '../../../utils/semver'; +import {ReleaseNotes} from '../../notes/release-notes'; +import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; +import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version'; +import {ReleaseAction} from '../actions'; +import {getCommitMessageForExceptionalNextVersionBump, getReleaseNoteCherryPickCommitMessage} from '../commit-message'; +import {changelogPath, packageJsonPath} from '../constants'; + +/** + * Base action that can be used to move the next release-train into the feature-freeze or + * release-candidate phase. This means that a new version branch is created from the next + * branch, and a new pre-release (either RC or another `next`) is cut indicating the new phase. + */ +export abstract class BranchOffNextBranchBaseAction extends ReleaseAction { + /** + * Phase which the release-train currently in the `next` phase will move into. + * + * Note that we only allow for a next version to branch into feature-freeze or + * directly into the release-candidate phase. A stable version cannot be released + * without release-candidate. + */ + abstract newPhaseName: 'feature-freeze'|'release-candidate'; + + override async getDescription() { + const {branchName} = this.active.next; + const newVersion = await this._computeNewVersion(); + return `Move the "${branchName}" branch into ${this.newPhaseName} phase (v${newVersion}).`; + } + + override async perform() { + const newVersion = await this._computeNewVersion(); + const newBranch = `${newVersion.major}.${newVersion.minor}.x`; + + // Branch-off the next branch into a new version branch. + await this._createNewVersionBranchFromNext(newBranch); + + // Stage the new version for the newly created branch, and push changes to a + // fork in order to create a staging pull request. Note that we re-use the newly + // created branch instead of re-fetching from the upstream. + const {pullRequest, releaseNotes} = + await this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch); + + // Wait for the staging PR to be merged. Then build and publish the feature-freeze next + // pre-release. Finally, cherry-pick the release notes into the next branch in combination + // with bumping the version to the next minor too. + await this.waitForPullRequestToBeMerged(pullRequest); + await this.buildAndPublish(releaseNotes, newBranch, 'next'); + await this._createNextBranchUpdatePullRequest(releaseNotes, newVersion); + } + + /** Computes the new version for the release-train being branched-off. */ + private async _computeNewVersion() { + if (this.newPhaseName === 'feature-freeze') { + return computeNewPrereleaseVersionForNext(this.active, this.config); + } else { + return semverInc(this.active.next.version, 'prerelease', 'rc'); + } + } + + /** Creates a new version branch from the next branch. */ + private async _createNewVersionBranchFromNext(newBranch: string) { + const {branchName: nextBranch} = this.active.next; + await this.verifyPassingGithubStatus(nextBranch); + await this.checkoutUpstreamBranch(nextBranch); + await this.createLocalBranchFromHead(newBranch); + await this.pushHeadToRemoteBranch(newBranch); + info(green(` ✓ Version branch "${newBranch}" created.`)); + } + + /** + * Creates a pull request for the next branch that bumps the version to the next + * minor, and cherry-picks the changelog for the newly branched-off release-candidate + * or feature-freeze version. + */ + private async _createNextBranchUpdatePullRequest( + releaseNotes: ReleaseNotes, newVersion: semver.SemVer) { + const {branchName: nextBranch, version} = this.active.next; + // We increase the version for the next branch to the next minor. The team can decide + // later if they want next to be a major through the `Configure Next as Major` release action. + const newNextVersion = semver.parse(`${version.major}.${version.minor + 1}.0-next.0`)!; + const bumpCommitMessage = getCommitMessageForExceptionalNextVersionBump(newNextVersion); + + await this.checkoutUpstreamBranch(nextBranch); + await this.updateProjectVersion(newNextVersion); + + // Create an individual commit for the next version bump. The changelog should go into + // a separate commit that makes it clear where the changelog is cherry-picked from. + await this.createCommit(bumpCommitMessage, [packageJsonPath]); + + await this.prependReleaseNotesToChangelog(releaseNotes); + + const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); + + await this.createCommit(commitMessage, [changelogPath]); + + let nextPullRequestMessage = `The previous "next" release-train has moved into the ` + + `${this.newPhaseName} phase. This PR updates the next branch to the subsequent ` + + `release-train.\n\nAlso this PR cherry-picks the changelog for ` + + `v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`; + + const nextUpdatePullRequest = await this.pushChangesToForkAndCreatePullRequest( + nextBranch, `next-release-train-${newNextVersion}`, + `Update next branch to reflect new release-train "v${newNextVersion}".`, + nextPullRequestMessage); + + info(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`)); + info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`)); + } +} diff --git a/dev-infra/release/publish/actions/cut-release-candidate.ts b/dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts similarity index 95% rename from dev-infra/release/publish/actions/cut-release-candidate.ts rename to dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts index 60b22ec36f..bf3d8e6c8a 100644 --- a/dev-infra/release/publish/actions/cut-release-candidate.ts +++ b/dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts @@ -14,7 +14,7 @@ import {ReleaseAction} from '../actions'; * Cuts the first release candidate for a release-train currently in the * feature-freeze phase. The version is bumped from `next` to `rc.0`. */ -export class CutReleaseCandidateAction extends ReleaseAction { +export class CutReleaseCandidateForFeatureFreezeAction extends ReleaseAction { private _newVersion = semverInc(this.active.releaseCandidate!.version, 'prerelease', 'rc'); override async getDescription() { diff --git a/dev-infra/release/publish/actions/index.ts b/dev-infra/release/publish/actions/index.ts index 67352466a7..d3f7558591 100644 --- a/dev-infra/release/publish/actions/index.ts +++ b/dev-infra/release/publish/actions/index.ts @@ -11,9 +11,10 @@ import {ReleaseActionConstructor} from '../actions'; import {CutLongTermSupportPatchAction} from './cut-lts-patch'; import {CutNewPatchAction} from './cut-new-patch'; import {CutNextPrereleaseAction} from './cut-next-prerelease'; -import {CutReleaseCandidateAction} from './cut-release-candidate'; +import {CutReleaseCandidateForFeatureFreezeAction} from './cut-release-candidate-for-feature-freeze'; import {CutStableAction} from './cut-stable'; import {MoveNextIntoFeatureFreezeAction} from './move-next-into-feature-freeze'; +import {MoveNextIntoReleaseCandidateAction} from './move-next-into-release-candidate'; import {TagRecentMajorAsLatest} from './tag-recent-major-as-latest'; /** @@ -23,9 +24,10 @@ import {TagRecentMajorAsLatest} from './tag-recent-major-as-latest'; export const actions: ReleaseActionConstructor[] = [ TagRecentMajorAsLatest, CutStableAction, - CutReleaseCandidateAction, + CutReleaseCandidateForFeatureFreezeAction, CutNewPatchAction, CutNextPrereleaseAction, MoveNextIntoFeatureFreezeAction, + MoveNextIntoReleaseCandidateAction, CutLongTermSupportPatchAction, ]; diff --git a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts b/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts index 2259cced9e..90ccd3d827 100644 --- a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts +++ b/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts @@ -6,103 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import * as semver from 'semver'; +import {ActiveReleaseTrains} from '../../versioning'; -import {green, info, yellow} from '../../../utils/console'; -import {ReleaseNotes} from '../../notes/release-notes'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version'; -import {ReleaseAction} from '../actions'; -import {getCommitMessageForExceptionalNextVersionBump, getReleaseNoteCherryPickCommitMessage} from '../commit-message'; -import {changelogPath, packageJsonPath} from '../constants'; +import {BranchOffNextBranchBaseAction} from './branch-off-next-branch'; /** * Release action that moves the next release-train into the feature-freeze phase. This means * that a new version branch is created from the next branch, and a new next pre-release is * cut indicating the started feature-freeze. */ -export class MoveNextIntoFeatureFreezeAction extends ReleaseAction { - private _newVersion = computeNewPrereleaseVersionForNext(this.active, this.config); - - override async getDescription() { - const {branchName} = this.active.next; - const newVersion = await this._newVersion; - return `Move the "${branchName}" branch into feature-freeze phase (v${newVersion}).`; - } - - override async perform() { - const newVersion = await this._newVersion; - const newBranch = `${newVersion.major}.${newVersion.minor}.x`; - - // Branch-off the next branch into a feature-freeze branch. - await this._createNewVersionBranchFromNext(newBranch); - - // Stage the new version for the newly created branch, and push changes to a - // fork in order to create a staging pull request. Note that we re-use the newly - // created branch instead of re-fetching from the upstream. - const {pullRequest, releaseNotes} = - await this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch); - - // Wait for the staging PR to be merged. Then build and publish the feature-freeze next - // pre-release. Finally, cherry-pick the release notes into the next branch in combination - // with bumping the version to the next minor too. - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, newBranch, 'next'); - await this._createNextBranchUpdatePullRequest(releaseNotes, newVersion); - } - - /** Creates a new version branch from the next branch. */ - private async _createNewVersionBranchFromNext(newBranch: string) { - const {branchName: nextBranch} = this.active.next; - await this.verifyPassingGithubStatus(nextBranch); - await this.checkoutUpstreamBranch(nextBranch); - await this.createLocalBranchFromHead(newBranch); - await this.pushHeadToRemoteBranch(newBranch); - info(green(` ✓ Version branch "${newBranch}" created.`)); - } - - /** - * Creates a pull request for the next branch that bumps the version to the next - * minor, and cherry-picks the changelog for the newly branched-off feature-freeze version. - */ - private async _createNextBranchUpdatePullRequest( - releaseNotes: ReleaseNotes, newVersion: semver.SemVer) { - const {branchName: nextBranch, version} = this.active.next; - // We increase the version for the next branch to the next minor. The team can decide - // later if they want next to be a major through the `Configure Next as Major` release action. - const newNextVersion = semver.parse(`${version.major}.${version.minor + 1}.0-next.0`)!; - const bumpCommitMessage = getCommitMessageForExceptionalNextVersionBump(newNextVersion); - - await this.checkoutUpstreamBranch(nextBranch); - await this.updateProjectVersion(newNextVersion); - - // Create an individual commit for the next version bump. The changelog should go into - // a separate commit that makes it clear where the changelog is cherry-picked from. - await this.createCommit(bumpCommitMessage, [packageJsonPath]); - - await this.prependReleaseNotesToChangelog(releaseNotes); - - const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); - - await this.createCommit(commitMessage, [changelogPath]); - - let nextPullRequestMessage = `The previous "next" release-train has moved into the ` + - `release-candidate phase. This PR updates the next branch to the subsequent ` + - `release-train.\n\nAlso this PR cherry-picks the changelog for ` + - `v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`; - - const nextUpdatePullRequest = await this.pushChangesToForkAndCreatePullRequest( - nextBranch, `next-release-train-${newNextVersion}`, - `Update next branch to reflect new release-train "v${newNextVersion}".`, - nextPullRequestMessage); - - info(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`)); - info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`)); - } +export class MoveNextIntoFeatureFreezeAction extends BranchOffNextBranchBaseAction { + override newPhaseName = 'feature-freeze' as const; static override async isActive(active: ActiveReleaseTrains) { - // A new feature-freeze/release-candidate branch can only be created if there - // is no active release-train in feature-freeze/release-candidate phase. - return active.releaseCandidate === null; + // A new feature-freeze branch can only be created if there is no active + // release-train in feature-freeze/release-candidate phase and the version + // currently in the `next` branch is for a major. The feature-freeze phase + // is not foreseen for minor versions. + return active.releaseCandidate === null && active.next.isMajor; } } diff --git a/dev-infra/release/publish/actions/move-next-into-release-candidate.ts b/dev-infra/release/publish/actions/move-next-into-release-candidate.ts new file mode 100644 index 0000000000..4d9b7280ed --- /dev/null +++ b/dev-infra/release/publish/actions/move-next-into-release-candidate.ts @@ -0,0 +1,27 @@ +/** + * @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 {ActiveReleaseTrains} from '../../versioning'; + +import {BranchOffNextBranchBaseAction} from './branch-off-next-branch'; + +/** + * Release action that moves the next release-train into the release-candidate phase. This means + * that a new version branch is created from the next branch, and the first release candidate + * version is cut indicating the new phase. + */ +export class MoveNextIntoReleaseCandidateAction extends BranchOffNextBranchBaseAction { + override newPhaseName = 'release-candidate' as const; + + static override async isActive(active: ActiveReleaseTrains) { + // Directly switching a next release-train into the `release-candidate` + // phase is only allowed for minor releases. Major version always need to + // go through the `feature-freeze` phase. + return active.releaseCandidate === null && !active.next.isMajor; + } +} diff --git a/dev-infra/release/publish/test/branch-off-next-branch-testing.ts b/dev-infra/release/publish/test/branch-off-next-branch-testing.ts new file mode 100644 index 0000000000..1934d872f4 --- /dev/null +++ b/dev-infra/release/publish/test/branch-off-next-branch-testing.ts @@ -0,0 +1,103 @@ +/** + * @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 {getBranchPushMatcher} from '../../../utils/testing'; +import {ActiveReleaseTrains} from '../../versioning'; +import * as npm from '../../versioning/npm-publish'; +import {ReleaseActionConstructor} from '../actions'; +import {BranchOffNextBranchBaseAction} from '../actions/branch-off-next-branch'; +import * as externalCommands from '../external-commands'; + +import {setupReleaseActionForTesting, testTmpDir} from './test-utils'; + +/** + * Performs the given branch-off release action and expects versions and + * branches to be determined and created properly. + */ +export async function expectBranchOffActionToRun( + action: ReleaseActionConstructor, active: ActiveReleaseTrains, + isNextPublishedToNpm: boolean, expectedNextVersion: string, expectedVersion: string, + expectedNewBranch: string) { + const {repo, fork, instance, gitClient} = + setupReleaseActionForTesting(action, active, isNextPublishedToNpm); + + const expectedNextUpdateBranch = `next-release-train-${expectedNextVersion}`; + const expectedStagingForkBranch = `release-stage-${expectedVersion}`; + const expectedTagName = expectedVersion; + + // We first mock the commit status check for the next branch, then expect two pull + // requests from a fork that are targeting next and the new feature-freeze branch. + repo.expectBranchRequest('master', 'MASTER_COMMIT_SHA') + .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') + .expectFindForkRequest(fork) + .expectPullRequestToBeCreated(expectedNewBranch, fork, expectedStagingForkBranch, 200) + .expectPullRequestWait(200) + .expectBranchRequest(expectedNewBranch, 'STAGING_COMMIT_SHA') + .expectCommitRequest( + 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) + .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') + .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) + .expectPullRequestToBeCreated('master', fork, expectedNextUpdateBranch, 100); + + // In the fork, we make the following branches appear as non-existent, + // so that the PRs can be created properly without collisions. + fork.expectBranchRequest(expectedStagingForkBranch, null) + .expectBranchRequest(expectedNextUpdateBranch, null); + + await instance.perform(); + + expect(gitClient.pushed.length).toBe(3); + expect(gitClient.pushed[0]) + .toEqual( + getBranchPushMatcher({ + baseRepo: repo, + baseBranch: 'master', + targetRepo: repo, + targetBranch: expectedNewBranch, + expectedCommits: [], + }), + 'Expected new version-branch to be created upstream and based on "master".'); + expect(gitClient.pushed[1]) + .toEqual( + getBranchPushMatcher({ + baseBranch: 'master', + baseRepo: repo, + targetBranch: expectedStagingForkBranch, + targetRepo: fork, + expectedCommits: [{ + message: `release: cut the v${expectedVersion} release`, + files: ['package.json', 'CHANGELOG.md'], + }], + }), + 'Expected release staging branch to be created in fork.'); + + expect(gitClient.pushed[2]) + .toEqual( + getBranchPushMatcher({ + baseBranch: 'master', + baseRepo: repo, + targetBranch: expectedNextUpdateBranch, + targetRepo: fork, + expectedCommits: [ + { + message: `release: bump the next branch to v${expectedNextVersion}`, + files: ['package.json'] + }, + { + message: `docs: release notes for the v${expectedVersion} release`, + files: ['CHANGELOG.md'] + }, + ], + }), + 'Expected next release-train update branch be created in fork.'); + + expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1); + expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); + expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'next', undefined); + expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, 'next', undefined); +} diff --git a/dev-infra/release/publish/test/cut-release-candidate.spec.ts b/dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts similarity index 80% rename from dev-infra/release/publish/test/cut-release-candidate.spec.ts rename to dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts index 589efa1df0..abb913f090 100644 --- a/dev-infra/release/publish/test/cut-release-candidate.spec.ts +++ b/dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts @@ -7,13 +7,13 @@ */ import {ReleaseTrain} from '../../versioning/release-trains'; -import {CutReleaseCandidateAction} from '../actions/cut-release-candidate'; +import {CutReleaseCandidateForFeatureFreezeAction} from '../actions/cut-release-candidate-for-feature-freeze'; import {expectStagingAndPublishWithCherryPick, parse, setupReleaseActionForTesting} from './test-utils'; -describe('cut release candidate action', () => { +describe('cut release candidate for feature-freeze action', () => { it('should activate if a feature-freeze release-train is active', async () => { - expect(await CutReleaseCandidateAction.isActive({ + expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), next: new ReleaseTrain('master', parse('10.2.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), @@ -21,7 +21,7 @@ describe('cut release candidate action', () => { }); it('should not activate if release-candidate release-train is active', async () => { - expect(await CutReleaseCandidateAction.isActive({ + expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ // No longer in feature-freeze but in release-candidate phase. releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), next: new ReleaseTrain('master', parse('10.2.0-next.0')), @@ -30,7 +30,7 @@ describe('cut release candidate action', () => { }); it('should not activate if no FF/RC release-train is active', async () => { - expect(await CutReleaseCandidateAction.isActive({ + expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ releaseCandidate: null, next: new ReleaseTrain('master', parse('10.1.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), @@ -38,7 +38,7 @@ describe('cut release candidate action', () => { }); it('should create a proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutReleaseCandidateAction, { + const action = setupReleaseActionForTesting(CutReleaseCandidateForFeatureFreezeAction, { releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), next: new ReleaseTrain('master', parse('10.2.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), diff --git a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts index fe61b4a975..bf84b90b6b 100644 --- a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts +++ b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts @@ -6,14 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {getBranchPushMatcher} from '../../../utils/testing'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import * as npm from '../../versioning/npm-publish'; import {ReleaseTrain} from '../../versioning/release-trains'; import {MoveNextIntoFeatureFreezeAction} from '../actions/move-next-into-feature-freeze'; -import * as externalCommands from '../external-commands'; -import {parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; +import {expectBranchOffActionToRun} from './branch-off-next-branch-testing'; +import {parse} from './test-utils'; describe('move next into feature-freeze action', () => { it('should not activate if a feature-freeze release-train is active', async () => { @@ -33,17 +30,25 @@ describe('move next into feature-freeze action', () => { })).toBe(false); }); + it('should not activate if the next release-train is for a minor', async () => { + expect(await MoveNextIntoFeatureFreezeAction.isActive({ + releaseCandidate: null, + next: new ReleaseTrain('master', parse('10.1.0-next.2')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + })).toBe(false); + }); + it('should activate if no FF/RC release-train is active', async () => { expect(await MoveNextIntoFeatureFreezeAction.isActive({ releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), + next: new ReleaseTrain('master', parse('11.0.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), })).toBe(true); }); it('should create pull requests and feature-freeze branch', async () => { - await expectVersionAndBranchToBeCreated( - { + await expectBranchOffActionToRun( + MoveNextIntoFeatureFreezeAction, { releaseCandidate: null, next: new ReleaseTrain('master', parse('10.2.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), @@ -52,95 +57,12 @@ describe('move next into feature-freeze action', () => { }); it('should not increment the version if "next" version is not yet published', async () => { - await expectVersionAndBranchToBeCreated( - { + await expectBranchOffActionToRun( + MoveNextIntoFeatureFreezeAction, { releaseCandidate: null, next: new ReleaseTrain('master', parse('10.2.0-next.0')), latest: new ReleaseTrain('10.0.x', parse('10.0.3')), }, /* isNextPublishedToNpm */ false, '10.3.0-next.0', '10.2.0-next.0', '10.2.x'); }); - - /** Performs the action and expects versions and branches to be determined properly. */ - async function expectVersionAndBranchToBeCreated( - active: ActiveReleaseTrains, isNextPublishedToNpm: boolean, expectedNextVersion: string, - expectedVersion: string, expectedNewBranch: string) { - const {repo, fork, instance, gitClient, releaseConfig} = - setupReleaseActionForTesting(MoveNextIntoFeatureFreezeAction, active, isNextPublishedToNpm); - - const expectedNextUpdateBranch = `next-release-train-${expectedNextVersion}`; - const expectedStagingForkBranch = `release-stage-${expectedVersion}`; - const expectedTagName = expectedVersion; - - // We first mock the commit status check for the next branch, then expect two pull - // requests from a fork that are targeting next and the new feature-freeze branch. - repo.expectBranchRequest('master', 'MASTER_COMMIT_SHA') - .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') - .expectFindForkRequest(fork) - .expectPullRequestToBeCreated(expectedNewBranch, fork, expectedStagingForkBranch, 200) - .expectPullRequestWait(200) - .expectBranchRequest(expectedNewBranch, 'STAGING_COMMIT_SHA') - .expectCommitRequest( - 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) - .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') - .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) - .expectPullRequestToBeCreated('master', fork, expectedNextUpdateBranch, 100); - - // In the fork, we make the following branches appear as non-existent, - // so that the PRs can be created properly without collisions. - fork.expectBranchRequest(expectedStagingForkBranch, null) - .expectBranchRequest(expectedNextUpdateBranch, null); - - await instance.perform(); - - expect(gitClient.pushed.length).toBe(3); - expect(gitClient.pushed[0]) - .toEqual( - getBranchPushMatcher({ - baseRepo: repo, - baseBranch: 'master', - targetRepo: repo, - targetBranch: expectedNewBranch, - expectedCommits: [], - }), - 'Expected feature-freeze branch to be created upstream and based on "master".'); - expect(gitClient.pushed[1]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedStagingForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `release: cut the v${expectedVersion} release`, - files: ['package.json', 'CHANGELOG.md'], - }], - }), - 'Expected release staging branch to be created in fork.'); - - expect(gitClient.pushed[2]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedNextUpdateBranch, - targetRepo: fork, - expectedCommits: [ - { - message: `release: bump the next branch to v${expectedNextVersion}`, - files: ['package.json'] - }, - { - message: `docs: release notes for the v${expectedVersion} release`, - files: ['CHANGELOG.md'] - }, - ], - }), - 'Expected next release-train update branch be created in fork.'); - - expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1); - expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); - expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'next', undefined); - expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, 'next', undefined); - } }); diff --git a/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts b/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts new file mode 100644 index 0000000000..d12b2ae159 --- /dev/null +++ b/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts @@ -0,0 +1,58 @@ +/** + * @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 {ReleaseTrain} from '../../versioning/release-trains'; +import {MoveNextIntoReleaseCandidateAction} from '../actions/move-next-into-release-candidate'; + +import {expectBranchOffActionToRun} from './branch-off-next-branch-testing'; +import {parse} from './test-utils'; + +describe('move next into release-candidate action', () => { + it('should not activate if a feature-freeze release-train is active', async () => { + expect(await MoveNextIntoReleaseCandidateAction.isActive({ + releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), + next: new ReleaseTrain('master', parse('10.2.0-next.0')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + })).toBe(false); + }); + + it('should not activate if release-candidate release-train is active', async () => { + expect(await MoveNextIntoReleaseCandidateAction.isActive({ + // No longer in feature-freeze but in release-candidate phase. + releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), + next: new ReleaseTrain('master', parse('10.2.0-next.0')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + })).toBe(false); + }); + + it('should not activate if the next release-train is for a major', async () => { + expect(await MoveNextIntoReleaseCandidateAction.isActive({ + releaseCandidate: null, + next: new ReleaseTrain('master', parse('11.0.0-next.0')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + })).toBe(false); + }); + + it('should activate if no FF/RC release-train is active', async () => { + expect(await MoveNextIntoReleaseCandidateAction.isActive({ + releaseCandidate: null, + next: new ReleaseTrain('master', parse('10.1.0-next.0')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + })).toBe(true); + }); + + it('should create pull requests and new version-branch', async () => { + await expectBranchOffActionToRun( + MoveNextIntoReleaseCandidateAction, { + releaseCandidate: null, + next: new ReleaseTrain('master', parse('10.2.0-next.0')), + latest: new ReleaseTrain('10.0.x', parse('10.0.3')), + }, + /* isNextPublishedToNpm */ true, '10.3.0-next.0', '10.2.0-rc.0', '10.2.x'); + }); +});