Skip to content

Commit

Permalink
feat(dev-infra): introduce release action for directly branching-off …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
devversion authored and TeriGlover committed Sep 22, 2021
1 parent ba9ef9e commit edbae22
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 210 deletions.
95 changes: 76 additions & 19 deletions dev-infra/ng-dev.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand All @@ -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* () {
Expand All @@ -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* () {
Expand All @@ -7273,19 +7281,67 @@ 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);
info(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`));
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 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* () {
// 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;
// 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;
});
}
}
Expand Down Expand Up @@ -7352,10 +7408,11 @@ class TagRecentMajorAsLatest extends ReleaseAction {
const actions = [
TagRecentMajorAsLatest,
CutStableAction,
CutReleaseCandidateAction,
CutReleaseCandidateForFeatureFreezeAction,
CutNewPatchAction,
CutNextPrereleaseAction,
MoveNextIntoFeatureFreezeAction,
MoveNextIntoReleaseCandidateAction,
CutLongTermSupportPatchAction,
];

Expand Down
120 changes: 120 additions & 0 deletions 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}.`));
}
}
Expand Up @@ -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() {
Expand Down
6 changes: 4 additions & 2 deletions dev-infra/release/publish/actions/index.ts
Expand Up @@ -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';

/**
Expand All @@ -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,
];

0 comments on commit edbae22

Please sign in to comment.