feat(dev-infra): Set up new common release notes generation tooling (#41905)
Enables the new common release notes generation within the ng-dev release publishing tooling. PR Close #41905
This commit is contained in:
parent
90d4b2277b
commit
393ce94718
|
@ -691,8 +691,8 @@ function getReleaseConfig(config = getConfig()) {
|
|||
if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) {
|
||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||
}
|
||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.generateReleaseNotesForHead) === undefined) {
|
||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.releaseNotes) === undefined) {
|
||||
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||
}
|
||||
assertNoErrors(errors);
|
||||
return config.release;
|
||||
|
|
|
@ -25,7 +25,7 @@ var os = require('os');
|
|||
var shelljs = require('shelljs');
|
||||
var minimatch = require('minimatch');
|
||||
var ora = require('ora');
|
||||
require('ejs');
|
||||
var ejs = require('ejs');
|
||||
var glob = require('glob');
|
||||
var ts = require('typescript');
|
||||
|
||||
|
@ -647,6 +647,17 @@ function promptConfirm(message, defaultValue) {
|
|||
});
|
||||
});
|
||||
}
|
||||
/** Prompts the user for one line of input. */
|
||||
function promptInput(message) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, inquirer.prompt({ type: 'input', name: 'result', message: message })];
|
||||
case 1: return [2 /*return*/, (_a.sent()).result];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
|
@ -5108,8 +5119,8 @@ function getReleaseConfig(config = getConfig()) {
|
|||
if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) {
|
||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||
}
|
||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.generateReleaseNotesForHead) === undefined) {
|
||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.releaseNotes) === undefined) {
|
||||
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||
}
|
||||
assertNoErrors(errors);
|
||||
return config.release;
|
||||
|
@ -5742,21 +5753,183 @@ function isCommitClosingPullRequest(api, sha, id) {
|
|||
const typesToIncludeInReleaseNotes = Object.values(COMMIT_TYPES)
|
||||
.filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible)
|
||||
.map(type => type.name);
|
||||
|
||||
/**
|
||||
* Gets the default pattern for extracting release notes for the given version.
|
||||
* This pattern matches for the conventional-changelog Angular preset.
|
||||
*/
|
||||
function getDefaultExtractReleaseNotesPattern(version) {
|
||||
const escapedVersion = version.format().replace('.', '\\.');
|
||||
// TODO: Change this once we have a canonical changelog generation tool. Also update this
|
||||
// based on the conventional-changelog version. They removed anchors in more recent versions.
|
||||
return new RegExp(`(<a name="${escapedVersion}"></a>.*?)(?:<a name="|$)`, 's');
|
||||
/** Context class used for rendering release notes. */
|
||||
class RenderContext {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
/** An array of group names in sort order if defined. */
|
||||
this.groupOrder = this.data.groupOrder || [];
|
||||
/** An array of scopes to hide from the release entry output. */
|
||||
this.hiddenScopes = this.data.hiddenScopes || [];
|
||||
/** The title of the release, or `false` if no title should be used. */
|
||||
this.title = this.data.title;
|
||||
/** An array of commits in the release period. */
|
||||
this.commits = this.data.commits;
|
||||
/** The version of the release. */
|
||||
this.version = this.data.version;
|
||||
/** The date stamp string for use in the release notes entry. */
|
||||
this.dateStamp = buildDateStamp(this.data.date);
|
||||
}
|
||||
/**
|
||||
* Organizes and sorts the commits into groups of commits.
|
||||
*
|
||||
* Groups are sorted either by default `Array.sort` order, or using the provided group order from
|
||||
* the configuration. Commits are order in the same order within each groups commit list as they
|
||||
* appear in the provided list of commits.
|
||||
* */
|
||||
asCommitGroups(commits) {
|
||||
/** The discovered groups to organize into. */
|
||||
const groups = new Map();
|
||||
// Place each commit in the list into its group.
|
||||
commits.forEach(commit => {
|
||||
const key = commit.npmScope ? `${commit.npmScope}/${commit.scope}` : commit.scope;
|
||||
const groupCommits = groups.get(key) || [];
|
||||
groups.set(key, groupCommits);
|
||||
groupCommits.push(commit);
|
||||
});
|
||||
/**
|
||||
* Array of CommitGroups containing the discovered commit groups. Sorted in alphanumeric order
|
||||
* of the group title.
|
||||
*/
|
||||
const commitGroups = Array.from(groups.entries())
|
||||
.map(([title, commits]) => ({ title, commits }))
|
||||
.sort((a, b) => a.title > b.title ? 1 : a.title < b.title ? -1 : 0);
|
||||
// If the configuration provides a sorting order, updated the sorted list of group keys to
|
||||
// satisfy the order of the groups provided in the list with any groups not found in the list at
|
||||
// the end of the sorted list.
|
||||
if (this.groupOrder.length) {
|
||||
for (const groupTitle of this.groupOrder.reverse()) {
|
||||
const currentIdx = commitGroups.findIndex(k => k.title === groupTitle);
|
||||
if (currentIdx !== -1) {
|
||||
const removedGroups = commitGroups.splice(currentIdx, 1);
|
||||
commitGroups.splice(0, 0, ...removedGroups);
|
||||
}
|
||||
}
|
||||
}
|
||||
return commitGroups;
|
||||
}
|
||||
/**
|
||||
* A filter function for filtering a list of commits to only include commits which should appear
|
||||
* in release notes.
|
||||
*/
|
||||
includeInReleaseNotes() {
|
||||
return (commit) => {
|
||||
if (!typesToIncludeInReleaseNotes.includes(commit.type)) {
|
||||
return false;
|
||||
}
|
||||
if (this.hiddenScopes.includes(commit.scope)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* A filter function for filtering a list of commits to only include commits which contain a
|
||||
* truthy value, or for arrays an array with 1 or more elements, for the provided field.
|
||||
*/
|
||||
contains(field) {
|
||||
return (commit) => {
|
||||
const fieldValue = commit[field];
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(fieldValue) && fieldValue.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* A filter function for filtering a list of commits to only include commits which contain a
|
||||
* unique value for the provided field across all commits in the list.
|
||||
*/
|
||||
unique(field) {
|
||||
const set = new Set();
|
||||
return (commit) => {
|
||||
const include = !set.has(commit[field]);
|
||||
set.add(commit[field]);
|
||||
return include;
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Builds a date stamp for stamping in release notes.
|
||||
*
|
||||
* Uses the current date, or a provided date in the format of YYYY-MM-DD, i.e. 1970-11-05.
|
||||
*/
|
||||
function buildDateStamp(date = new Date()) {
|
||||
const year = `${date.getFullYear()}`;
|
||||
const month = `${(date.getMonth() + 1)}`.padStart(2, '0');
|
||||
const day = `${date.getDate()}`.padStart(2, '0');
|
||||
return [year, month, day].join('-');
|
||||
}
|
||||
|
||||
/** Gets the path for the changelog file in a given project. */
|
||||
function getLocalChangelogFilePath(projectDir) {
|
||||
return path.join(projectDir, changelogPath);
|
||||
}
|
||||
/** Release note generation. */
|
||||
class ReleaseNotes {
|
||||
constructor(version, config) {
|
||||
this.version = version;
|
||||
this.config = config;
|
||||
/** An instance of GitClient. */
|
||||
this.git = GitClient.getInstance();
|
||||
/** A promise resolving to a list of Commits since the latest semver tag on the branch. */
|
||||
this.commits = getCommitsInRange(this.git.getLatestSemverTag().format(), 'HEAD');
|
||||
}
|
||||
/** Construct a release note generation instance. */
|
||||
static fromLatestTagToHead(version, config) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
return new ReleaseNotes(version, config);
|
||||
});
|
||||
}
|
||||
/** Retrieve the release note generated for a Github Release. */
|
||||
getGithubReleaseEntry() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
return ejs.renderFile(path.join(__dirname, 'templates/github-release.ejs'), yield this.generateRenderContext(), { rmWhitespace: true });
|
||||
});
|
||||
}
|
||||
/** Retrieve the release note generated for a CHANGELOG entry. */
|
||||
getChangelogEntry() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
return ejs.renderFile(path.join(__dirname, 'templates/changelog.ejs'), yield this.generateRenderContext(), { rmWhitespace: true });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Prompt the user for a title for the release, if the project's configuration is defined to use a
|
||||
* title.
|
||||
*/
|
||||
promptForReleaseTitle() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
if (this.title === undefined) {
|
||||
if (this.config.releaseNotes.useReleaseTitle) {
|
||||
this.title = yield promptInput('Please provide a title for the release:');
|
||||
}
|
||||
else {
|
||||
this.title = false;
|
||||
}
|
||||
}
|
||||
return this.title;
|
||||
});
|
||||
}
|
||||
/** Build the render context data object for constructing the RenderContext instance. */
|
||||
generateRenderContext() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
if (!this.renderContext) {
|
||||
this.renderContext = new RenderContext({
|
||||
commits: yield this.commits,
|
||||
github: this.git.remoteConfig,
|
||||
version: this.version.format(),
|
||||
groupOrder: this.config.releaseNotes.groupOrder,
|
||||
hiddenScopes: this.config.releaseNotes.hiddenScopes,
|
||||
title: yield this.promptForReleaseTitle(),
|
||||
});
|
||||
}
|
||||
return this.renderContext;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
|
@ -5831,22 +6004,6 @@ class ReleaseAction {
|
|||
info(green(' ✓ Upstream commit is passing all github status checks.'));
|
||||
});
|
||||
}
|
||||
/** Generates the changelog for the specified for the current `HEAD`. */
|
||||
_generateReleaseNotesForHead(version) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const changelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||
yield this.config.generateReleaseNotesForHead(changelogPath);
|
||||
info(green(` ✓ Updated the changelog to capture changes for "${version}".`));
|
||||
});
|
||||
}
|
||||
/** Extract the release notes for the given version from the changelog file. */
|
||||
_extractReleaseNotesForVersion(changelogContent, version) {
|
||||
const pattern = this.config.extractReleaseNotesPattern !== undefined ?
|
||||
this.config.extractReleaseNotesPattern(version) :
|
||||
getDefaultExtractReleaseNotesPattern(version);
|
||||
const matchedNotes = pattern.exec(changelogContent);
|
||||
return matchedNotes === null ? null : matchedNotes[1];
|
||||
}
|
||||
/**
|
||||
* Prompts the user for potential release notes edits that need to be made. Once
|
||||
* confirmed, a new commit for the release point is created.
|
||||
|
@ -6019,27 +6176,13 @@ class ReleaseAction {
|
|||
* the current Git `HEAD`. This is useful for cherry-picking the changelog.
|
||||
* @returns A boolean indicating whether the release notes have been prepended.
|
||||
*/
|
||||
prependReleaseNotesFromVersionBranch(version, containingBranch) {
|
||||
prependReleaseNotesToChangelog(releaseNotes) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const { data } = yield this.git.github.repos.getContents(Object.assign(Object.assign({}, this.git.remoteParams), { path: '/' + changelogPath, ref: containingBranch }));
|
||||
const branchChangelog = Buffer.from(data.content, 'base64').toString();
|
||||
let releaseNotes = this._extractReleaseNotesForVersion(branchChangelog, version);
|
||||
// If no release notes could be extracted, return "false" so that the caller
|
||||
// can tell that changelog prepending failed.
|
||||
if (releaseNotes === null) {
|
||||
return false;
|
||||
}
|
||||
const localChangelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||
const localChangelog = yield fs.promises.readFile(localChangelogPath, 'utf8');
|
||||
// If the extracted release notes do not have any new lines at the end and the
|
||||
// local changelog is not empty, we add lines manually so that there is space
|
||||
// between the previous and cherry-picked release notes.
|
||||
if (!/[\r\n]+$/.test(releaseNotes) && localChangelog !== '') {
|
||||
releaseNotes = `${releaseNotes}\n\n`;
|
||||
}
|
||||
// Prepend the extracted release notes to the local changelog and write it back.
|
||||
yield fs.promises.writeFile(localChangelogPath, releaseNotes + localChangelog);
|
||||
return true;
|
||||
const releaseNotesEntry = yield releaseNotes.getChangelogEntry();
|
||||
yield fs.promises.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`);
|
||||
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
|
||||
});
|
||||
}
|
||||
/** Checks out an upstream branch with a detached head. */
|
||||
|
@ -6059,25 +6202,6 @@ class ReleaseAction {
|
|||
this.git.run(['commit', '--no-verify', '-m', message, ...files]);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates a cherry-pick commit for the release notes of the specified version that
|
||||
* has been pushed to the given branch.
|
||||
* @returns a boolean indicating whether the commit has been created successfully.
|
||||
*/
|
||||
createCherryPickReleaseNotesCommitFrom(version, branchName) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(version);
|
||||
// Fetch, extract and prepend the release notes to the local changelog. If that is not
|
||||
// possible, abort so that we can ask the user to manually cherry-pick the changelog.
|
||||
if (!(yield this.prependReleaseNotesFromVersionBranch(version, branchName))) {
|
||||
return false;
|
||||
}
|
||||
// Create a changelog cherry-pick commit.
|
||||
yield this.createCommit(commitMessage, [changelogPath]);
|
||||
info(green(` ✓ Created changelog cherry-pick commit for: "${version}".`));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Stages the specified new version for the current branch and creates a
|
||||
* pull request that targets the given base branch.
|
||||
|
@ -6085,13 +6209,14 @@ class ReleaseAction {
|
|||
*/
|
||||
stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const releaseNotes = yield ReleaseNotes.fromLatestTagToHead(newVersion, this.config);
|
||||
yield this.updateProjectVersion(newVersion);
|
||||
yield this._generateReleaseNotesForHead(newVersion);
|
||||
yield this.prependReleaseNotesToChangelog(releaseNotes);
|
||||
yield this.waitForEditsAndCreateReleaseCommit(newVersion);
|
||||
const pullRequest = yield this.pushChangesToForkAndCreatePullRequest(pullRequestBaseBranch, `release-stage-${newVersion}`, `Bump version to "v${newVersion}" with changelog.`);
|
||||
info(green(' ✓ Release staging pull request has been created.'));
|
||||
info(yellow(` Please ask team members to review: ${pullRequest.url}.`));
|
||||
return pullRequest;
|
||||
return { releaseNotes, pullRequest };
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
@ -6111,21 +6236,18 @@ class ReleaseAction {
|
|||
* into the `next` primary development branch. A pull request is created for this.
|
||||
* @returns a boolean indicating successful creation of the cherry-pick pull request.
|
||||
*/
|
||||
cherryPickChangelogIntoNextBranch(newVersion, stagingBranch) {
|
||||
cherryPickChangelogIntoNextBranch(releaseNotes, stagingBranch) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const nextBranch = this.active.next.branchName;
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(newVersion);
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version);
|
||||
// Checkout the next branch.
|
||||
yield this.checkoutUpstreamBranch(nextBranch);
|
||||
// Cherry-pick the release notes into the current branch. If it fails,
|
||||
// ask the user to manually copy the release notes into the next branch.
|
||||
if (!(yield this.createCherryPickReleaseNotesCommitFrom(newVersion, stagingBranch))) {
|
||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
||||
error(yellow(` Please copy the release notes manually into the "${nextBranch}" branch.`));
|
||||
return false;
|
||||
}
|
||||
yield this.prependReleaseNotesToChangelog(releaseNotes);
|
||||
// Create a changelog cherry-pick commit.
|
||||
yield this.createCommit(commitMessage, [changelogPath]);
|
||||
info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`));
|
||||
// Create a cherry-pick pull request that should be merged by the caretaker.
|
||||
const { url, id } = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `changelog-cherry-pick-${newVersion}`, commitMessage, `Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` +
|
||||
const { url, id } = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `changelog-cherry-pick-${releaseNotes.version}`, commitMessage, `Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` +
|
||||
`branch (${nextBranch}).`);
|
||||
info(green(` ✓ Pull request for cherry-picking the changelog into "${nextBranch}" ` +
|
||||
'has been created.'));
|
||||
|
@ -6253,10 +6375,10 @@ class CutLongTermSupportPatchAction extends ReleaseAction {
|
|||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const ltsBranch = yield this._promptForTargetLtsBranch();
|
||||
const newVersion = semverInc(ltsBranch.version, 'patch');
|
||||
const { id } = yield this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name);
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, ltsBranch.name, ltsBranch.npmDistTag);
|
||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, ltsBranch.name);
|
||||
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, ltsBranch.name);
|
||||
});
|
||||
}
|
||||
/** Prompts the user to select an LTS branch for which a patch should but cut. */
|
||||
|
@ -6330,10 +6452,10 @@ class CutNewPatchAction extends ReleaseAction {
|
|||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const { branchName } = this.active.latest;
|
||||
const newVersion = this._newVersion;
|
||||
const { id } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
});
|
||||
}
|
||||
static isActive(active) {
|
||||
|
@ -6400,14 +6522,14 @@ class CutNextPrereleaseAction extends ReleaseAction {
|
|||
const releaseTrain = this._getActivePrereleaseTrain();
|
||||
const { branchName } = releaseTrain;
|
||||
const newVersion = yield this._newVersion;
|
||||
const { id } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, branchName, 'next');
|
||||
// If the pre-release has been cut from a branch that is not corresponding
|
||||
// to the next release-train, cherry-pick the changelog into the primary
|
||||
// development branch. i.e. the `next` branch that is usually `master`.
|
||||
if (releaseTrain !== this.active.next) {
|
||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -6467,10 +6589,10 @@ class CutReleaseCandidateAction extends ReleaseAction {
|
|||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const { branchName } = this.active.releaseCandidate;
|
||||
const newVersion = this._newVersion;
|
||||
const { id } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, branchName, 'next');
|
||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
});
|
||||
}
|
||||
static isActive(active) {
|
||||
|
@ -6511,7 +6633,7 @@ class CutStableAction extends ReleaseAction {
|
|||
const { branchName } = this.active.releaseCandidate;
|
||||
const newVersion = this._newVersion;
|
||||
const isNewMajor = (_a = this.active.releaseCandidate) === null || _a === void 0 ? void 0 : _a.isMajor;
|
||||
const { id } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const { pullRequest: { id }, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
||||
// If a new major version is published and becomes the "latest" release-train, we need
|
||||
|
@ -6529,7 +6651,7 @@ class CutStableAction extends ReleaseAction {
|
|||
yield invokeYarnInstallCommand(this.projectDir);
|
||||
yield invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version);
|
||||
}
|
||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
});
|
||||
}
|
||||
/** Gets the new stable version of the release candidate release-train. */
|
||||
|
@ -6581,13 +6703,13 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
// 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 stagingPullRequest = yield this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch);
|
||||
const { pullRequest: { id }, releaseNotes } = yield 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.
|
||||
yield this.waitForPullRequestToBeMerged(stagingPullRequest.id);
|
||||
yield this.waitForPullRequestToBeMerged(id);
|
||||
yield this.buildAndPublish(newVersion, newBranch, 'next');
|
||||
yield this._createNextBranchUpdatePullRequest(newVersion, newBranch);
|
||||
yield this._createNextBranchUpdatePullRequest(releaseNotes, newVersion);
|
||||
});
|
||||
}
|
||||
/** Creates a new version branch from the next branch. */
|
||||
|
@ -6605,7 +6727,7 @@ 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.
|
||||
*/
|
||||
_createNextBranchUpdatePullRequest(newVersion, newBranch) {
|
||||
_createNextBranchUpdatePullRequest(releaseNotes, newVersion) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const { branchName: nextBranch, version } = this.active.next;
|
||||
// We increase the version for the next branch to the next minor. The team can decide
|
||||
|
@ -6617,18 +6739,13 @@ class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
// 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.
|
||||
yield this.createCommit(bumpCommitMessage, [packageJsonPath]);
|
||||
yield this.prependReleaseNotesToChangelog(releaseNotes);
|
||||
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 ` +
|
||||
`release-train.`;
|
||||
const hasChangelogCherryPicked = yield this.createCherryPickReleaseNotesCommitFrom(newVersion, newBranch);
|
||||
if (hasChangelogCherryPicked) {
|
||||
nextPullRequestMessage += `\n\nAlso this PR cherry-picks the changelog for ` +
|
||||
`v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`;
|
||||
}
|
||||
else {
|
||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
||||
error(yellow(` Please copy the release note manually into "${nextBranch}".`));
|
||||
}
|
||||
`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}.`));
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('default target labels', () => {
|
|||
releaseConfig = {
|
||||
npmPackages: ['@angular/dev-infra-test-pkg'],
|
||||
buildPackages: async () => [],
|
||||
generateReleaseNotesForHead: async () => {},
|
||||
releaseNotes: {}
|
||||
};
|
||||
|
||||
// The label determination will print warn messages. These should not be
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('ng-dev release build', () => {
|
|||
/** Invokes the build command handler. */
|
||||
async function invokeBuild({json}: {json?: boolean} = {}) {
|
||||
spyOn(releaseConfig, 'getReleaseConfig')
|
||||
.and.returnValue({npmPackages, buildPackages, generateReleaseNotesForHead: async () => {}});
|
||||
.and.returnValue({npmPackages, buildPackages, releaseNotes: {}});
|
||||
await ReleaseBuildCommandModule.handler({json: !!json, $0: '', _: []});
|
||||
}
|
||||
|
||||
|
|
|
@ -26,20 +26,10 @@ export interface ReleaseConfig {
|
|||
npmPackages: string[];
|
||||
/** Builds release packages and returns a list of paths pointing to the output. */
|
||||
buildPackages: () => Promise<BuiltPackage[]|null>;
|
||||
/** Generates the release notes from the most recent tag to `HEAD`. */
|
||||
generateReleaseNotesForHead: (outputPath: string) => Promise<void>;
|
||||
/**
|
||||
* Gets a pattern for extracting the release notes of the a given version.
|
||||
* @returns A pattern matching the notes for a given version (including the header).
|
||||
*/
|
||||
// TODO: Remove this in favor of a canonical changelog format across the Angular organization.
|
||||
extractReleaseNotesPattern?: (version: semver.SemVer) => RegExp;
|
||||
/** The list of github labels to add to the release PRs. */
|
||||
releasePrLabels?: string[];
|
||||
/** Configuration for creating release notes during publishing. */
|
||||
// TODO(josephperrott): Make releaseNotes a required attribute on the interface when tooling is
|
||||
// integrated.
|
||||
releaseNotes?: ReleaseNotesConfig;
|
||||
releaseNotes: ReleaseNotesConfig;
|
||||
}
|
||||
|
||||
/** Configuration for creating release notes during publishing. */
|
||||
|
@ -75,8 +65,8 @@ export function getReleaseConfig(config: Partial<DevInfraReleaseConfig> = getCon
|
|||
if (config.release?.buildPackages === undefined) {
|
||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||
}
|
||||
if (config.release?.generateReleaseNotesForHead === undefined) {
|
||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
||||
if (config.release?.releaseNotes === undefined) {
|
||||
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||
}
|
||||
|
||||
assertNoErrors(errors);
|
||||
|
|
|
@ -24,7 +24,7 @@ import {changelogPath, packageJsonPath, waitForPullRequestInterval} from './cons
|
|||
import {invokeBazelCleanCommand, invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands';
|
||||
import {findOwnedForksOfRepoQuery} from './graphql-queries';
|
||||
import {getPullRequestState} from './pull-request-state';
|
||||
import {getDefaultExtractReleaseNotesPattern, getLocalChangelogFilePath} from './release-notes/release-notes';
|
||||
import {getLocalChangelogFilePath, ReleaseNotes} from './release-notes/release-notes';
|
||||
|
||||
/** Interface describing a Github repository. */
|
||||
export interface GithubRepo {
|
||||
|
@ -132,22 +132,6 @@ export abstract class ReleaseAction {
|
|||
info(green(' ✓ Upstream commit is passing all github status checks.'));
|
||||
}
|
||||
|
||||
/** Generates the changelog for the specified for the current `HEAD`. */
|
||||
private async _generateReleaseNotesForHead(version: semver.SemVer) {
|
||||
const changelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||
await this.config.generateReleaseNotesForHead(changelogPath);
|
||||
info(green(` ✓ Updated the changelog to capture changes for "${version}".`));
|
||||
}
|
||||
|
||||
/** Extract the release notes for the given version from the changelog file. */
|
||||
private _extractReleaseNotesForVersion(changelogContent: string, version: semver.SemVer): string
|
||||
|null {
|
||||
const pattern = this.config.extractReleaseNotesPattern !== undefined ?
|
||||
this.config.extractReleaseNotesPattern(version) :
|
||||
getDefaultExtractReleaseNotesPattern(version);
|
||||
const matchedNotes = pattern.exec(changelogContent);
|
||||
return matchedNotes === null ? null : matchedNotes[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user for potential release notes edits that need to be made. Once
|
||||
|
@ -334,28 +318,12 @@ export abstract class ReleaseAction {
|
|||
* the current Git `HEAD`. This is useful for cherry-picking the changelog.
|
||||
* @returns A boolean indicating whether the release notes have been prepended.
|
||||
*/
|
||||
protected async prependReleaseNotesFromVersionBranch(
|
||||
version: semver.SemVer, containingBranch: string): Promise<boolean> {
|
||||
const {data} = await this.git.github.repos.getContents(
|
||||
{...this.git.remoteParams, path: '/' + changelogPath, ref: containingBranch});
|
||||
const branchChangelog = Buffer.from(data.content, 'base64').toString();
|
||||
let releaseNotes = this._extractReleaseNotesForVersion(branchChangelog, version);
|
||||
// If no release notes could be extracted, return "false" so that the caller
|
||||
// can tell that changelog prepending failed.
|
||||
if (releaseNotes === null) {
|
||||
return false;
|
||||
}
|
||||
protected async prependReleaseNotesToChangelog(releaseNotes: ReleaseNotes): Promise<void> {
|
||||
const localChangelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||
const localChangelog = await fs.readFile(localChangelogPath, 'utf8');
|
||||
// If the extracted release notes do not have any new lines at the end and the
|
||||
// local changelog is not empty, we add lines manually so that there is space
|
||||
// between the previous and cherry-picked release notes.
|
||||
if (!/[\r\n]+$/.test(releaseNotes) && localChangelog !== '') {
|
||||
releaseNotes = `${releaseNotes}\n\n`;
|
||||
}
|
||||
// Prepend the extracted release notes to the local changelog and write it back.
|
||||
await fs.writeFile(localChangelogPath, releaseNotes + localChangelog);
|
||||
return true;
|
||||
const releaseNotesEntry = await releaseNotes.getChangelogEntry();
|
||||
await fs.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`);
|
||||
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
|
||||
}
|
||||
|
||||
/** Checks out an upstream branch with a detached head. */
|
||||
|
@ -373,27 +341,6 @@ export abstract class ReleaseAction {
|
|||
this.git.run(['commit', '--no-verify', '-m', message, ...files]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cherry-pick commit for the release notes of the specified version that
|
||||
* has been pushed to the given branch.
|
||||
* @returns a boolean indicating whether the commit has been created successfully.
|
||||
*/
|
||||
protected async createCherryPickReleaseNotesCommitFrom(
|
||||
version: semver.SemVer, branchName: string): Promise<boolean> {
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(version);
|
||||
|
||||
// Fetch, extract and prepend the release notes to the local changelog. If that is not
|
||||
// possible, abort so that we can ask the user to manually cherry-pick the changelog.
|
||||
if (!await this.prependReleaseNotesFromVersionBranch(version, branchName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a changelog cherry-pick commit.
|
||||
await this.createCommit(commitMessage, [changelogPath]);
|
||||
|
||||
info(green(` ✓ Created changelog cherry-pick commit for: "${version}".`));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stages the specified new version for the current branch and creates a
|
||||
|
@ -401,9 +348,11 @@ export abstract class ReleaseAction {
|
|||
* @returns an object describing the created pull request.
|
||||
*/
|
||||
protected async stageVersionForBranchAndCreatePullRequest(
|
||||
newVersion: semver.SemVer, pullRequestBaseBranch: string): Promise<PullRequest> {
|
||||
newVersion: semver.SemVer, pullRequestBaseBranch: string):
|
||||
Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> {
|
||||
const releaseNotes = await ReleaseNotes.fromLatestTagToHead(newVersion, this.config);
|
||||
await this.updateProjectVersion(newVersion);
|
||||
await this._generateReleaseNotesForHead(newVersion);
|
||||
await this.prependReleaseNotesToChangelog(releaseNotes);
|
||||
await this.waitForEditsAndCreateReleaseCommit(newVersion);
|
||||
|
||||
const pullRequest = await this.pushChangesToForkAndCreatePullRequest(
|
||||
|
@ -413,7 +362,7 @@ export abstract class ReleaseAction {
|
|||
info(green(' ✓ Release staging pull request has been created.'));
|
||||
info(yellow(` Please ask team members to review: ${pullRequest.url}.`));
|
||||
|
||||
return pullRequest;
|
||||
return {releaseNotes, pullRequest};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -422,7 +371,7 @@ export abstract class ReleaseAction {
|
|||
* @returns an object describing the created pull request.
|
||||
*/
|
||||
protected async checkoutBranchAndStageVersion(newVersion: semver.SemVer, stagingBranch: string):
|
||||
Promise<PullRequest> {
|
||||
Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> {
|
||||
await this.verifyPassingGithubStatus(stagingBranch);
|
||||
await this.checkoutUpstreamBranch(stagingBranch);
|
||||
return await this.stageVersionForBranchAndCreatePullRequest(newVersion, stagingBranch);
|
||||
|
@ -434,25 +383,22 @@ export abstract class ReleaseAction {
|
|||
* @returns a boolean indicating successful creation of the cherry-pick pull request.
|
||||
*/
|
||||
protected async cherryPickChangelogIntoNextBranch(
|
||||
newVersion: semver.SemVer, stagingBranch: string): Promise<boolean> {
|
||||
releaseNotes: ReleaseNotes, stagingBranch: string): Promise<boolean> {
|
||||
const nextBranch = this.active.next.branchName;
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(newVersion);
|
||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version);
|
||||
|
||||
// Checkout the next branch.
|
||||
await this.checkoutUpstreamBranch(nextBranch);
|
||||
|
||||
// Cherry-pick the release notes into the current branch. If it fails,
|
||||
// ask the user to manually copy the release notes into the next branch.
|
||||
if (!await this.createCherryPickReleaseNotesCommitFrom(newVersion, stagingBranch)) {
|
||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
||||
error(
|
||||
yellow(` Please copy the release notes manually into the "${nextBranch}" branch.`));
|
||||
return false;
|
||||
}
|
||||
await this.prependReleaseNotesToChangelog(releaseNotes);
|
||||
|
||||
// Create a changelog cherry-pick commit.
|
||||
await this.createCommit(commitMessage, [changelogPath]);
|
||||
info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`));
|
||||
|
||||
// Create a cherry-pick pull request that should be merged by the caretaker.
|
||||
const {url, id} = await this.pushChangesToForkAndCreatePullRequest(
|
||||
nextBranch, `changelog-cherry-pick-${newVersion}`, commitMessage,
|
||||
nextBranch, `changelog-cherry-pick-${releaseNotes.version}`, commitMessage,
|
||||
`Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` +
|
||||
`branch (${nextBranch}).`);
|
||||
|
||||
|
|
|
@ -41,11 +41,12 @@ export class CutLongTermSupportPatchAction extends ReleaseAction {
|
|||
async perform() {
|
||||
const ltsBranch = await this._promptForTargetLtsBranch();
|
||||
const newVersion = semverInc(ltsBranch.version, 'patch');
|
||||
const {id} = await this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name);
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, ltsBranch.name, ltsBranch.npmDistTag);
|
||||
await this.cherryPickChangelogIntoNextBranch(newVersion, ltsBranch.name);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, ltsBranch.name);
|
||||
}
|
||||
|
||||
/** Prompts the user to select an LTS branch for which a patch should but cut. */
|
||||
|
|
|
@ -28,11 +28,12 @@ export class CutNewPatchAction extends ReleaseAction {
|
|||
const {branchName} = this.active.latest;
|
||||
const newVersion = this._newVersion;
|
||||
|
||||
const {id} = await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, branchName, 'latest');
|
||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
}
|
||||
|
||||
static async isActive(active: ActiveReleaseTrains) {
|
||||
|
|
|
@ -32,7 +32,8 @@ export class CutNextPrereleaseAction extends ReleaseAction {
|
|||
const {branchName} = releaseTrain;
|
||||
const newVersion = await this._newVersion;
|
||||
|
||||
const {id} = await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, branchName, 'next');
|
||||
|
@ -41,7 +42,7 @@ export class CutNextPrereleaseAction extends ReleaseAction {
|
|||
// to the next release-train, cherry-pick the changelog into the primary
|
||||
// development branch. i.e. the `next` branch that is usually `master`.
|
||||
if (releaseTrain !== this.active.next) {
|
||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,11 +26,12 @@ export class CutReleaseCandidateAction extends ReleaseAction {
|
|||
const {branchName} = this.active.releaseCandidate!;
|
||||
const newVersion = this._newVersion;
|
||||
|
||||
const {id} = await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, branchName, 'next');
|
||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
}
|
||||
|
||||
static async isActive(active: ActiveReleaseTrains) {
|
||||
|
|
|
@ -31,7 +31,8 @@ export class CutStableAction extends ReleaseAction {
|
|||
const isNewMajor = this.active.releaseCandidate?.isMajor;
|
||||
|
||||
|
||||
const {id} = await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
const {pullRequest: {id}, releaseNotes} =
|
||||
await this.checkoutBranchAndStageVersion(newVersion, branchName);
|
||||
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, branchName, 'latest');
|
||||
|
@ -53,7 +54,7 @@ export class CutStableAction extends ReleaseAction {
|
|||
await invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version);
|
||||
}
|
||||
|
||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||
}
|
||||
|
||||
/** Gets the new stable version of the release candidate release-train. */
|
||||
|
|
|
@ -8,12 +8,13 @@
|
|||
|
||||
import * as semver from 'semver';
|
||||
|
||||
import {error, green, info, yellow} from '../../../utils/console';
|
||||
import {green, info, yellow} from '../../../utils/console';
|
||||
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
||||
import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version';
|
||||
import {ReleaseAction} from '../actions';
|
||||
import {getCommitMessageForExceptionalNextVersionBump} from '../commit-message';
|
||||
import {packageJsonPath} from '../constants';
|
||||
import {getCommitMessageForExceptionalNextVersionBump, getReleaseNoteCherryPickCommitMessage} from '../commit-message';
|
||||
import {changelogPath, packageJsonPath} from '../constants';
|
||||
import {ReleaseNotes} from '../release-notes/release-notes';
|
||||
|
||||
/**
|
||||
* Release action that moves the next release-train into the feature-freeze phase. This means
|
||||
|
@ -39,15 +40,15 @@ export class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
// 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 stagingPullRequest =
|
||||
const {pullRequest: {id}, 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(stagingPullRequest.id);
|
||||
await this.waitForPullRequestToBeMerged(id);
|
||||
await this.buildAndPublish(newVersion, newBranch, 'next');
|
||||
await this._createNextBranchUpdatePullRequest(newVersion, newBranch);
|
||||
await this._createNextBranchUpdatePullRequest(releaseNotes, newVersion);
|
||||
}
|
||||
|
||||
/** Creates a new version branch from the next branch. */
|
||||
|
@ -64,7 +65,8 @@ export 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.
|
||||
*/
|
||||
private async _createNextBranchUpdatePullRequest(newVersion: semver.SemVer, newBranch: string) {
|
||||
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.
|
||||
|
@ -78,19 +80,16 @@ export class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
|
|||
// 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.`;
|
||||
const hasChangelogCherryPicked =
|
||||
await this.createCherryPickReleaseNotesCommitFrom(newVersion, newBranch);
|
||||
|
||||
if (hasChangelogCherryPicked) {
|
||||
nextPullRequestMessage += `\n\nAlso this PR cherry-picks the changelog for ` +
|
||||
`v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`;
|
||||
} else {
|
||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
||||
error(yellow(` Please copy the release note manually into "${nextBranch}".`));
|
||||
}
|
||||
`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}`,
|
||||
|
|
|
@ -10,25 +10,12 @@ import {join} from 'path';
|
|||
import * as semver from 'semver';
|
||||
|
||||
import {getCommitsInRange} from '../../../commit-message/utils';
|
||||
import {getConfig} from '../../../utils/config';
|
||||
import {promptInput} from '../../../utils/console';
|
||||
import {GitClient} from '../../../utils/git/index';
|
||||
import {getReleaseConfig} from '../../config/index';
|
||||
import {ReleaseConfig} from '../../config/index';
|
||||
import {changelogPath} from '../constants';
|
||||
import {RenderContext} from './context';
|
||||
|
||||
|
||||
/**
|
||||
* Gets the default pattern for extracting release notes for the given version.
|
||||
* This pattern matches for the conventional-changelog Angular preset.
|
||||
*/
|
||||
export function getDefaultExtractReleaseNotesPattern(version: semver.SemVer): RegExp {
|
||||
const escapedVersion = version.format().replace('.', '\\.');
|
||||
// TODO: Change this once we have a canonical changelog generation tool. Also update this
|
||||
// based on the conventional-changelog version. They removed anchors in more recent versions.
|
||||
return new RegExp(`(<a name="${escapedVersion}"></a>.*?)(?:<a name="|$)`, 's');
|
||||
}
|
||||
|
||||
/** Gets the path for the changelog file in a given project. */
|
||||
export function getLocalChangelogFilePath(projectDir: string): string {
|
||||
return join(projectDir, changelogPath);
|
||||
|
@ -37,33 +24,35 @@ export function getLocalChangelogFilePath(projectDir: string): string {
|
|||
|
||||
/** Release note generation. */
|
||||
export class ReleaseNotes {
|
||||
/** Construct a release note generation instance. */
|
||||
static async fromLatestTagToHead(version: semver.SemVer, config: ReleaseConfig):
|
||||
Promise<ReleaseNotes> {
|
||||
return new ReleaseNotes(version, config);
|
||||
}
|
||||
|
||||
/** An instance of GitClient. */
|
||||
private git = GitClient.getInstance();
|
||||
/** The github configuration. */
|
||||
private readonly github = getConfig().github;
|
||||
/** The configuration for the release notes generation. */
|
||||
// TODO(josephperrott): Remove non-null assertion after usage of ReleaseNotes is integrated into
|
||||
// release publish tooling.
|
||||
private readonly config = getReleaseConfig().releaseNotes! || {};
|
||||
/** A promise resolving to a list of Commits since the latest semver tag on the branch. */
|
||||
private commits = getCommitsInRange(this.git.getLatestSemverTag().format(), 'HEAD');
|
||||
/** The RenderContext to be used during rendering. */
|
||||
private renderContext: RenderContext|undefined;
|
||||
/** The title to use for the release. */
|
||||
private title: string|false|undefined;
|
||||
/** A promise resolving to a list of Commits since the latest semver tag on the branch. */
|
||||
private commits = getCommitsInRange(this.git.getLatestSemverTag().format(), 'HEAD');
|
||||
|
||||
constructor(private version: semver.SemVer) {}
|
||||
private constructor(public readonly version: semver.SemVer, private config: ReleaseConfig) {}
|
||||
|
||||
/** Retrieve the release note generated for a Github Release. */
|
||||
async getGithubReleaseEntry(): Promise<string> {
|
||||
return await renderFile(
|
||||
join(__dirname, 'templates/github-release.ejs'), await this.generateRenderContext());
|
||||
return renderFile(
|
||||
join(__dirname, 'templates/github-release.ejs'), await this.generateRenderContext(),
|
||||
{rmWhitespace: true});
|
||||
}
|
||||
|
||||
/** Retrieve the release note generated for a CHANGELOG entry. */
|
||||
async getChangelogEntry() {
|
||||
return await renderFile(
|
||||
join(__dirname, 'templates/changelog.ejs'), await this.generateRenderContext());
|
||||
return renderFile(
|
||||
join(__dirname, 'templates/changelog.ejs'), await this.generateRenderContext(),
|
||||
{rmWhitespace: true});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,7 +61,7 @@ export class ReleaseNotes {
|
|||
*/
|
||||
async promptForReleaseTitle() {
|
||||
if (this.title === undefined) {
|
||||
if (this.config.useReleaseTitle) {
|
||||
if (this.config.releaseNotes.useReleaseTitle) {
|
||||
this.title = await promptInput('Please provide a title for the release:');
|
||||
} else {
|
||||
this.title = false;
|
||||
|
@ -86,10 +75,10 @@ export class ReleaseNotes {
|
|||
if (!this.renderContext) {
|
||||
this.renderContext = new RenderContext({
|
||||
commits: await this.commits,
|
||||
github: getConfig().github,
|
||||
github: this.git.remoteConfig,
|
||||
version: this.version.format(),
|
||||
groupOrder: this.config.groupOrder,
|
||||
hiddenScopes: this.config.hiddenScopes,
|
||||
groupOrder: this.config.releaseNotes.groupOrder,
|
||||
hiddenScopes: this.config.releaseNotes.hiddenScopes,
|
||||
title: await this.promptForReleaseTitle(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,5 +33,8 @@ jasmine_node_test(
|
|||
# enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional.
|
||||
# https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa.
|
||||
args = ["--nobazel_patch_module_resolver"],
|
||||
data = [
|
||||
"//dev-infra/release/publish/release-notes/templates",
|
||||
],
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@ import {ReleaseTrain} from '../../versioning/release-trains';
|
|||
import {ReleaseAction} from '../actions';
|
||||
import {actions} from '../actions/index';
|
||||
import {changelogPath} from '../constants';
|
||||
import {ReleaseNotes} from '../release-notes/release-notes';
|
||||
|
||||
import {fakeNpmPackageQueryRequest, getChangelogForVersion, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';
|
||||
|
||||
|
@ -89,7 +90,7 @@ describe('common release action logic', () => {
|
|||
const fakeReleaseNotes = getChangelogForVersion(version.format());
|
||||
const forkBranchName = `changelog-cherry-pick-${version}`;
|
||||
|
||||
it('should prepend fetched changelog', async () => {
|
||||
it('should prepend the changelog to the next branch', async () => {
|
||||
const {repo, fork, instance, testTmpDir} =
|
||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||
|
||||
|
@ -109,62 +110,6 @@ describe('common release action logic', () => {
|
|||
expect(changelogContent).toEqual(`${fakeReleaseNotes}Existing changelog`);
|
||||
});
|
||||
|
||||
it('should respect a custom release note extraction pattern', async () => {
|
||||
const {repo, fork, instance, testTmpDir, releaseConfig} =
|
||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||
|
||||
// Custom pattern matching changelog output sections grouped through
|
||||
// basic level-1 markdown headers (compared to the default anchor pattern).
|
||||
releaseConfig.extractReleaseNotesPattern = version =>
|
||||
new RegExp(`(# v${version} \\("[^"]+"\\).*?)(?:# v|$)`, 's');
|
||||
|
||||
const customReleaseNotes = `# v${version} ("newton-kepler")\n\nNew Content!`;
|
||||
|
||||
// Expect the changelog to be fetched and return a fake changelog to test that
|
||||
// it is properly appended. Also expect a pull request to be created in the fork.
|
||||
repo.expectChangelogFetch(branchName, customReleaseNotes)
|
||||
.expectFindForkRequest(fork)
|
||||
.expectPullRequestToBeCreated('master', fork, forkBranchName, 200)
|
||||
.expectPullRequestWait(200);
|
||||
|
||||
// Simulate that the fork branch name is available.
|
||||
fork.expectBranchRequest(forkBranchName, null);
|
||||
|
||||
await instance.testCherryPickWithPullRequest(version, branchName);
|
||||
|
||||
const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
|
||||
expect(changelogContent).toEqual(`${customReleaseNotes}\n\nExisting changelog`);
|
||||
});
|
||||
|
||||
it('should print an error if release notes cannot be extracted', async () => {
|
||||
const {repo, fork, instance, testTmpDir, releaseConfig} =
|
||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||
|
||||
// Expect the changelog to be fetched and return a fake changelog to test that
|
||||
// it is properly appended. Also expect a pull request to be created in the fork.
|
||||
repo.expectChangelogFetch(branchName, `non analyzable changelog`)
|
||||
.expectFindForkRequest(fork)
|
||||
.expectPullRequestToBeCreated('master', fork, forkBranchName, 200)
|
||||
.expectPullRequestWait(200);
|
||||
|
||||
// Simulate that the fork branch name is available.
|
||||
fork.expectBranchRequest(forkBranchName, null);
|
||||
|
||||
spyOn(console, 'error');
|
||||
|
||||
await instance.testCherryPickWithPullRequest(version, branchName);
|
||||
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(`Could not cherry-pick release notes for v${version}`));
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(jasmine.stringMatching(
|
||||
`Please copy the release notes manually into the "master" branch.`));
|
||||
|
||||
const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
|
||||
expect(changelogContent).toEqual(`Existing changelog`);
|
||||
});
|
||||
|
||||
it('should push changes to a fork for creating a pull request', async () => {
|
||||
const {repo, fork, instance, gitClient} =
|
||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||
|
@ -214,6 +159,7 @@ class TestAction extends ReleaseAction {
|
|||
}
|
||||
|
||||
async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) {
|
||||
await this.cherryPickChangelogIntoNextBranch(version, branch);
|
||||
const releaseNotes = await ReleaseNotes.fromLatestTagToHead(version, this.config);
|
||||
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branch);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,6 @@ describe('move next into feature-freeze action', () => {
|
|||
'Expected next release-train update branch be created in fork.');
|
||||
|
||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||
expect(releaseConfig.generateReleaseNotesForHead).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);
|
||||
|
|
|
@ -11,9 +11,10 @@ import * as nock from 'nock';
|
|||
import {join} from 'path';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import * as commitMessageUtils from '../../../commit-message/utils';
|
||||
import {GithubConfig} from '../../../utils/config';
|
||||
import * as console from '../../../utils/console';
|
||||
import {getBranchPushMatcher, VirtualGitClient} from '../../../utils/testing';
|
||||
import {getBranchPushMatcher, installVirtualGitClientSpies, VirtualGitClient} from '../../../utils/testing';
|
||||
import {ReleaseConfig} from '../../config/index';
|
||||
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
||||
import * as npm from '../../versioning/npm-publish';
|
||||
|
@ -21,6 +22,7 @@ import {_npmPackageInfoCache, NpmPackageInfo} from '../../versioning/npm-registr
|
|||
import {ReleaseAction, ReleaseActionConstructor} from '../actions';
|
||||
import * as constants from '../constants';
|
||||
import * as externalCommands from '../external-commands';
|
||||
import {buildDateStamp} from '../release-notes/context';
|
||||
|
||||
import {GithubTestingRepo} from './github-api-testing';
|
||||
|
||||
|
@ -50,7 +52,7 @@ export function getTestingMocksForReleaseAction() {
|
|||
'@angular/pkg1',
|
||||
'@angular/pkg2',
|
||||
],
|
||||
generateReleaseNotesForHead: jasmine.createSpy('generateReleaseNotesForHead').and.resolveTo(),
|
||||
releaseNotes: {},
|
||||
buildPackages: () => {
|
||||
throw Error('Not implemented');
|
||||
},
|
||||
|
@ -67,6 +69,9 @@ export function getTestingMocksForReleaseAction() {
|
|||
export function setupReleaseActionForTesting<T extends ReleaseAction>(
|
||||
actionCtor: ReleaseActionConstructor<T>, active: ActiveReleaseTrains,
|
||||
isNextPublishedToNpm = true): TestReleaseAction<T> {
|
||||
installVirtualGitClientSpies();
|
||||
spyOn(commitMessageUtils, 'getCommitsInRange').and.returnValue(Promise.resolve([]));
|
||||
|
||||
// Reset existing HTTP interceptors.
|
||||
nock.cleanAll();
|
||||
|
||||
|
@ -121,7 +126,7 @@ export function parse(version: string): semver.SemVer {
|
|||
|
||||
/** Gets a changelog for the specified version. */
|
||||
export function getChangelogForVersion(version: string): string {
|
||||
return `<a name="${version}"></a>Changelog\n\n`;
|
||||
return `<a name="${version}"></a>\n# ${version} (${buildDateStamp()})\n\n\n`;
|
||||
}
|
||||
|
||||
export async function expectStagingAndPublishWithoutCherryPick(
|
||||
|
@ -166,7 +171,6 @@ export async function expectStagingAndPublishWithoutCherryPick(
|
|||
'Expected release staging branch to be created in fork.');
|
||||
|
||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||
expect(releaseConfig.generateReleaseNotesForHead).toHaveBeenCalledTimes(1);
|
||||
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
||||
expect(npm.runNpmPublish)
|
||||
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
||||
|
@ -235,7 +239,6 @@ export async function expectStagingAndPublishWithCherryPick(
|
|||
'Expected cherry-pick branch to be created in fork.');
|
||||
|
||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||
expect(releaseConfig.generateReleaseNotesForHead).toHaveBeenCalledTimes(1);
|
||||
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
||||
expect(npm.runNpmPublish)
|
||||
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('ng-dev release set-dist-tag', () => {
|
|||
npmPackages,
|
||||
publishRegistry,
|
||||
buildPackages: async () => [],
|
||||
generateReleaseNotesForHead: async () => {}
|
||||
releaseNotes: {},
|
||||
});
|
||||
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ ts_library(
|
|||
"@npm//@types/jasmine",
|
||||
"@npm//@types/minimist",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/semver",
|
||||
"@npm//minimist",
|
||||
"@npm//semver",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import {SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||
import * as parseArgs from 'minimist';
|
||||
import {SemVer} from 'semver';
|
||||
|
||||
import {NgDevConfig} from '../config';
|
||||
import {GitClient} from '../git/index';
|
||||
|
@ -82,6 +83,15 @@ export class VirtualGitClient<Authenticated extends boolean> extends GitClient<A
|
|||
/** List of pushed heads to a given remote ref. */
|
||||
pushed: {remote: RemoteRef, head: GitHead}[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* Override the actual GitClient getLatestSemverTag, as an actual tag cannot be retrieved in
|
||||
* testing.
|
||||
*/
|
||||
getLatestSemverTag() {
|
||||
return new SemVer('0.0.0');
|
||||
}
|
||||
|
||||
/** Override for the actual Git client command execution. */
|
||||
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
|
||||
const [command, ...rawArgs] = args;
|
||||
|
|
Loading…
Reference in New Issue