Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
ba9ef9e
commit edbae22
Showing
10 changed files
with
419 additions
and
210 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
dev-infra/release/publish/actions/branch-off-next-branch.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}.`)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.