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) {
|
if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) {
|
||||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||||
}
|
}
|
||||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.generateReleaseNotesForHead) === undefined) {
|
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.releaseNotes) === undefined) {
|
||||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||||
}
|
}
|
||||||
assertNoErrors(errors);
|
assertNoErrors(errors);
|
||||||
return config.release;
|
return config.release;
|
||||||
|
@ -25,7 +25,7 @@ var os = require('os');
|
|||||||
var shelljs = require('shelljs');
|
var shelljs = require('shelljs');
|
||||||
var minimatch = require('minimatch');
|
var minimatch = require('minimatch');
|
||||||
var ora = require('ora');
|
var ora = require('ora');
|
||||||
require('ejs');
|
var ejs = require('ejs');
|
||||||
var glob = require('glob');
|
var glob = require('glob');
|
||||||
var ts = require('typescript');
|
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.
|
* 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) {
|
if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) {
|
||||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||||
}
|
}
|
||||||
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.generateReleaseNotesForHead) === undefined) {
|
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.releaseNotes) === undefined) {
|
||||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||||
}
|
}
|
||||||
assertNoErrors(errors);
|
assertNoErrors(errors);
|
||||||
return config.release;
|
return config.release;
|
||||||
@ -5742,21 +5753,183 @@ function isCommitClosingPullRequest(api, sha, id) {
|
|||||||
const typesToIncludeInReleaseNotes = Object.values(COMMIT_TYPES)
|
const typesToIncludeInReleaseNotes = Object.values(COMMIT_TYPES)
|
||||||
.filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible)
|
.filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible)
|
||||||
.map(type => type.name);
|
.map(type => type.name);
|
||||||
|
/** Context class used for rendering release notes. */
|
||||||
/**
|
class RenderContext {
|
||||||
* Gets the default pattern for extracting release notes for the given version.
|
constructor(data) {
|
||||||
* This pattern matches for the conventional-changelog Angular preset.
|
this.data = data;
|
||||||
*/
|
/** An array of group names in sort order if defined. */
|
||||||
function getDefaultExtractReleaseNotesPattern(version) {
|
this.groupOrder = this.data.groupOrder || [];
|
||||||
const escapedVersion = version.format().replace('.', '\\.');
|
/** An array of scopes to hide from the release entry output. */
|
||||||
// TODO: Change this once we have a canonical changelog generation tool. Also update this
|
this.hiddenScopes = this.data.hiddenScopes || [];
|
||||||
// based on the conventional-changelog version. They removed anchors in more recent versions.
|
/** The title of the release, or `false` if no title should be used. */
|
||||||
return new RegExp(`(<a name="${escapedVersion}"></a>.*?)(?:<a name="|$)`, 's');
|
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. */
|
/** Gets the path for the changelog file in a given project. */
|
||||||
function getLocalChangelogFilePath(projectDir) {
|
function getLocalChangelogFilePath(projectDir) {
|
||||||
return path.join(projectDir, changelogPath);
|
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
|
* @license
|
||||||
@ -5831,22 +6004,6 @@ class ReleaseAction {
|
|||||||
info(green(' ✓ Upstream commit is passing all github status checks.'));
|
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
|
* Prompts the user for potential release notes edits that need to be made. Once
|
||||||
* confirmed, a new commit for the release point is created.
|
* 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.
|
* the current Git `HEAD`. This is useful for cherry-picking the changelog.
|
||||||
* @returns A boolean indicating whether the release notes have been prepended.
|
* @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* () {
|
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 localChangelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||||
const localChangelog = yield fs.promises.readFile(localChangelogPath, 'utf8');
|
const localChangelog = yield fs.promises.readFile(localChangelogPath, 'utf8');
|
||||||
// If the extracted release notes do not have any new lines at the end and the
|
const releaseNotesEntry = yield releaseNotes.getChangelogEntry();
|
||||||
// local changelog is not empty, we add lines manually so that there is space
|
yield fs.promises.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`);
|
||||||
// between the previous and cherry-picked release notes.
|
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/** Checks out an upstream branch with a detached head. */
|
/** Checks out an upstream branch with a detached head. */
|
||||||
@ -6059,25 +6202,6 @@ class ReleaseAction {
|
|||||||
this.git.run(['commit', '--no-verify', '-m', message, ...files]);
|
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
|
* Stages the specified new version for the current branch and creates a
|
||||||
* pull request that targets the given base branch.
|
* pull request that targets the given base branch.
|
||||||
@ -6085,13 +6209,14 @@ class ReleaseAction {
|
|||||||
*/
|
*/
|
||||||
stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) {
|
stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
|
const releaseNotes = yield ReleaseNotes.fromLatestTagToHead(newVersion, this.config);
|
||||||
yield this.updateProjectVersion(newVersion);
|
yield this.updateProjectVersion(newVersion);
|
||||||
yield this._generateReleaseNotesForHead(newVersion);
|
yield this.prependReleaseNotesToChangelog(releaseNotes);
|
||||||
yield this.waitForEditsAndCreateReleaseCommit(newVersion);
|
yield this.waitForEditsAndCreateReleaseCommit(newVersion);
|
||||||
const pullRequest = yield this.pushChangesToForkAndCreatePullRequest(pullRequestBaseBranch, `release-stage-${newVersion}`, `Bump version to "v${newVersion}" with changelog.`);
|
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(green(' ✓ Release staging pull request has been created.'));
|
||||||
info(yellow(` Please ask team members to review: ${pullRequest.url}.`));
|
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.
|
* 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.
|
* @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* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const nextBranch = this.active.next.branchName;
|
const nextBranch = this.active.next.branchName;
|
||||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(newVersion);
|
const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version);
|
||||||
// Checkout the next branch.
|
// Checkout the next branch.
|
||||||
yield this.checkoutUpstreamBranch(nextBranch);
|
yield this.checkoutUpstreamBranch(nextBranch);
|
||||||
// Cherry-pick the release notes into the current branch. If it fails,
|
yield this.prependReleaseNotesToChangelog(releaseNotes);
|
||||||
// ask the user to manually copy the release notes into the next branch.
|
// Create a changelog cherry-pick commit.
|
||||||
if (!(yield this.createCherryPickReleaseNotesCommitFrom(newVersion, stagingBranch))) {
|
yield this.createCommit(commitMessage, [changelogPath]);
|
||||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`));
|
||||||
error(yellow(` Please copy the release notes manually into the "${nextBranch}" branch.`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Create a cherry-pick pull request that should be merged by the caretaker.
|
// 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}).`);
|
`branch (${nextBranch}).`);
|
||||||
info(green(` ✓ Pull request for cherry-picking the changelog into "${nextBranch}" ` +
|
info(green(` ✓ Pull request for cherry-picking the changelog into "${nextBranch}" ` +
|
||||||
'has been created.'));
|
'has been created.'));
|
||||||
@ -6253,10 +6375,10 @@ class CutLongTermSupportPatchAction extends ReleaseAction {
|
|||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const ltsBranch = yield this._promptForTargetLtsBranch();
|
const ltsBranch = yield this._promptForTargetLtsBranch();
|
||||||
const newVersion = semverInc(ltsBranch.version, 'patch');
|
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.waitForPullRequestToBeMerged(id);
|
||||||
yield this.buildAndPublish(newVersion, ltsBranch.name, ltsBranch.npmDistTag);
|
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. */
|
/** 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* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const { branchName } = this.active.latest;
|
const { branchName } = this.active.latest;
|
||||||
const newVersion = this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
||||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
static isActive(active) {
|
static isActive(active) {
|
||||||
@ -6400,14 +6522,14 @@ class CutNextPrereleaseAction extends ReleaseAction {
|
|||||||
const releaseTrain = this._getActivePrereleaseTrain();
|
const releaseTrain = this._getActivePrereleaseTrain();
|
||||||
const { branchName } = releaseTrain;
|
const { branchName } = releaseTrain;
|
||||||
const newVersion = yield this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
yield this.buildAndPublish(newVersion, branchName, 'next');
|
yield this.buildAndPublish(newVersion, branchName, 'next');
|
||||||
// If the pre-release has been cut from a branch that is not corresponding
|
// 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
|
// to the next release-train, cherry-pick the changelog into the primary
|
||||||
// development branch. i.e. the `next` branch that is usually `master`.
|
// development branch. i.e. the `next` branch that is usually `master`.
|
||||||
if (releaseTrain !== this.active.next) {
|
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* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const { branchName } = this.active.releaseCandidate;
|
const { branchName } = this.active.releaseCandidate;
|
||||||
const newVersion = this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
yield this.buildAndPublish(newVersion, branchName, 'next');
|
yield this.buildAndPublish(newVersion, branchName, 'next');
|
||||||
yield this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
static isActive(active) {
|
static isActive(active) {
|
||||||
@ -6511,7 +6633,7 @@ class CutStableAction extends ReleaseAction {
|
|||||||
const { branchName } = this.active.releaseCandidate;
|
const { branchName } = this.active.releaseCandidate;
|
||||||
const newVersion = this._newVersion;
|
const newVersion = this._newVersion;
|
||||||
const isNewMajor = (_a = this.active.releaseCandidate) === null || _a === void 0 ? void 0 : _a.isMajor;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
yield this.buildAndPublish(newVersion, branchName, 'latest');
|
||||||
// If a new major version is published and becomes the "latest" release-train, we need
|
// 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 invokeYarnInstallCommand(this.projectDir);
|
||||||
yield invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version);
|
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. */
|
/** 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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// pre-release. Finally, cherry-pick the release notes into the next branch in combination
|
||||||
// with bumping the version to the next minor too.
|
// 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.buildAndPublish(newVersion, newBranch, 'next');
|
||||||
yield this._createNextBranchUpdatePullRequest(newVersion, newBranch);
|
yield this._createNextBranchUpdatePullRequest(releaseNotes, newVersion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/** Creates a new version branch from the next branch. */
|
/** 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
|
* 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 feature-freeze version.
|
||||||
*/
|
*/
|
||||||
_createNextBranchUpdatePullRequest(newVersion, newBranch) {
|
_createNextBranchUpdatePullRequest(releaseNotes, newVersion) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const { branchName: nextBranch, version } = this.active.next;
|
const { branchName: nextBranch, version } = this.active.next;
|
||||||
// We increase the version for the next branch to the next minor. The team can decide
|
// 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
|
// 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.
|
// a separate commit that makes it clear where the changelog is cherry-picked from.
|
||||||
yield this.createCommit(bumpCommitMessage, [packageJsonPath]);
|
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 ` +
|
let nextPullRequestMessage = `The previous "next" release-train has moved into the ` +
|
||||||
`release-candidate phase. This PR updates the next branch to the subsequent ` +
|
`release-candidate phase. This PR updates the next branch to the subsequent ` +
|
||||||
`release-train.`;
|
`release-train.\n\nAlso this PR cherry-picks the changelog for ` +
|
||||||
const hasChangelogCherryPicked = yield this.createCherryPickReleaseNotesCommitFrom(newVersion, newBranch);
|
`v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`;
|
||||||
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}".`));
|
|
||||||
}
|
|
||||||
const nextUpdatePullRequest = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `next-release-train-${newNextVersion}`, `Update next branch to reflect new release-train "v${newNextVersion}".`, nextPullRequestMessage);
|
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(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`));
|
||||||
info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`));
|
info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`));
|
||||||
|
@ -31,7 +31,7 @@ describe('default target labels', () => {
|
|||||||
releaseConfig = {
|
releaseConfig = {
|
||||||
npmPackages: ['@angular/dev-infra-test-pkg'],
|
npmPackages: ['@angular/dev-infra-test-pkg'],
|
||||||
buildPackages: async () => [],
|
buildPackages: async () => [],
|
||||||
generateReleaseNotesForHead: async () => {},
|
releaseNotes: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The label determination will print warn messages. These should not be
|
// 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. */
|
/** Invokes the build command handler. */
|
||||||
async function invokeBuild({json}: {json?: boolean} = {}) {
|
async function invokeBuild({json}: {json?: boolean} = {}) {
|
||||||
spyOn(releaseConfig, 'getReleaseConfig')
|
spyOn(releaseConfig, 'getReleaseConfig')
|
||||||
.and.returnValue({npmPackages, buildPackages, generateReleaseNotesForHead: async () => {}});
|
.and.returnValue({npmPackages, buildPackages, releaseNotes: {}});
|
||||||
await ReleaseBuildCommandModule.handler({json: !!json, $0: '', _: []});
|
await ReleaseBuildCommandModule.handler({json: !!json, $0: '', _: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,20 +26,10 @@ export interface ReleaseConfig {
|
|||||||
npmPackages: string[];
|
npmPackages: string[];
|
||||||
/** Builds release packages and returns a list of paths pointing to the output. */
|
/** Builds release packages and returns a list of paths pointing to the output. */
|
||||||
buildPackages: () => Promise<BuiltPackage[]|null>;
|
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. */
|
/** The list of github labels to add to the release PRs. */
|
||||||
releasePrLabels?: string[];
|
releasePrLabels?: string[];
|
||||||
/** Configuration for creating release notes during publishing. */
|
/** Configuration for creating release notes during publishing. */
|
||||||
// TODO(josephperrott): Make releaseNotes a required attribute on the interface when tooling is
|
releaseNotes: ReleaseNotesConfig;
|
||||||
// integrated.
|
|
||||||
releaseNotes?: ReleaseNotesConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration for creating release notes during publishing. */
|
/** Configuration for creating release notes during publishing. */
|
||||||
@ -75,8 +65,8 @@ export function getReleaseConfig(config: Partial<DevInfraReleaseConfig> = getCon
|
|||||||
if (config.release?.buildPackages === undefined) {
|
if (config.release?.buildPackages === undefined) {
|
||||||
errors.push(`No "buildPackages" function configured for releasing.`);
|
errors.push(`No "buildPackages" function configured for releasing.`);
|
||||||
}
|
}
|
||||||
if (config.release?.generateReleaseNotesForHead === undefined) {
|
if (config.release?.releaseNotes === undefined) {
|
||||||
errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`);
|
errors.push(`No "releaseNotes" configured for releasing.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNoErrors(errors);
|
assertNoErrors(errors);
|
||||||
|
@ -24,7 +24,7 @@ import {changelogPath, packageJsonPath, waitForPullRequestInterval} from './cons
|
|||||||
import {invokeBazelCleanCommand, invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands';
|
import {invokeBazelCleanCommand, invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands';
|
||||||
import {findOwnedForksOfRepoQuery} from './graphql-queries';
|
import {findOwnedForksOfRepoQuery} from './graphql-queries';
|
||||||
import {getPullRequestState} from './pull-request-state';
|
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. */
|
/** Interface describing a Github repository. */
|
||||||
export interface GithubRepo {
|
export interface GithubRepo {
|
||||||
@ -132,22 +132,6 @@ export abstract class ReleaseAction {
|
|||||||
info(green(' ✓ Upstream commit is passing all github status checks.'));
|
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
|
* 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.
|
* the current Git `HEAD`. This is useful for cherry-picking the changelog.
|
||||||
* @returns A boolean indicating whether the release notes have been prepended.
|
* @returns A boolean indicating whether the release notes have been prepended.
|
||||||
*/
|
*/
|
||||||
protected async prependReleaseNotesFromVersionBranch(
|
protected async prependReleaseNotesToChangelog(releaseNotes: ReleaseNotes): Promise<void> {
|
||||||
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;
|
|
||||||
}
|
|
||||||
const localChangelogPath = getLocalChangelogFilePath(this.projectDir);
|
const localChangelogPath = getLocalChangelogFilePath(this.projectDir);
|
||||||
const localChangelog = await fs.readFile(localChangelogPath, 'utf8');
|
const localChangelog = await fs.readFile(localChangelogPath, 'utf8');
|
||||||
// If the extracted release notes do not have any new lines at the end and the
|
const releaseNotesEntry = await releaseNotes.getChangelogEntry();
|
||||||
// local changelog is not empty, we add lines manually so that there is space
|
await fs.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`);
|
||||||
// between the previous and cherry-picked release notes.
|
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks out an upstream branch with a detached head. */
|
/** 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]);
|
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
|
* 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.
|
* @returns an object describing the created pull request.
|
||||||
*/
|
*/
|
||||||
protected async stageVersionForBranchAndCreatePullRequest(
|
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.updateProjectVersion(newVersion);
|
||||||
await this._generateReleaseNotesForHead(newVersion);
|
await this.prependReleaseNotesToChangelog(releaseNotes);
|
||||||
await this.waitForEditsAndCreateReleaseCommit(newVersion);
|
await this.waitForEditsAndCreateReleaseCommit(newVersion);
|
||||||
|
|
||||||
const pullRequest = await this.pushChangesToForkAndCreatePullRequest(
|
const pullRequest = await this.pushChangesToForkAndCreatePullRequest(
|
||||||
@ -413,7 +362,7 @@ export abstract class ReleaseAction {
|
|||||||
info(green(' ✓ Release staging pull request has been created.'));
|
info(green(' ✓ Release staging pull request has been created.'));
|
||||||
info(yellow(` Please ask team members to review: ${pullRequest.url}.`));
|
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.
|
* @returns an object describing the created pull request.
|
||||||
*/
|
*/
|
||||||
protected async checkoutBranchAndStageVersion(newVersion: semver.SemVer, stagingBranch: string):
|
protected async checkoutBranchAndStageVersion(newVersion: semver.SemVer, stagingBranch: string):
|
||||||
Promise<PullRequest> {
|
Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> {
|
||||||
await this.verifyPassingGithubStatus(stagingBranch);
|
await this.verifyPassingGithubStatus(stagingBranch);
|
||||||
await this.checkoutUpstreamBranch(stagingBranch);
|
await this.checkoutUpstreamBranch(stagingBranch);
|
||||||
return await this.stageVersionForBranchAndCreatePullRequest(newVersion, 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.
|
* @returns a boolean indicating successful creation of the cherry-pick pull request.
|
||||||
*/
|
*/
|
||||||
protected async cherryPickChangelogIntoNextBranch(
|
protected async cherryPickChangelogIntoNextBranch(
|
||||||
newVersion: semver.SemVer, stagingBranch: string): Promise<boolean> {
|
releaseNotes: ReleaseNotes, stagingBranch: string): Promise<boolean> {
|
||||||
const nextBranch = this.active.next.branchName;
|
const nextBranch = this.active.next.branchName;
|
||||||
const commitMessage = getReleaseNoteCherryPickCommitMessage(newVersion);
|
const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version);
|
||||||
|
|
||||||
// Checkout the next branch.
|
// Checkout the next branch.
|
||||||
await this.checkoutUpstreamBranch(nextBranch);
|
await this.checkoutUpstreamBranch(nextBranch);
|
||||||
|
|
||||||
// Cherry-pick the release notes into the current branch. If it fails,
|
await this.prependReleaseNotesToChangelog(releaseNotes);
|
||||||
// ask the user to manually copy the release notes into the next branch.
|
|
||||||
if (!await this.createCherryPickReleaseNotesCommitFrom(newVersion, stagingBranch)) {
|
// Create a changelog cherry-pick commit.
|
||||||
error(yellow(` ✘ Could not cherry-pick release notes for v${newVersion}.`));
|
await this.createCommit(commitMessage, [changelogPath]);
|
||||||
error(
|
info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`));
|
||||||
yellow(` Please copy the release notes manually into the "${nextBranch}" branch.`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a cherry-pick pull request that should be merged by the caretaker.
|
// Create a cherry-pick pull request that should be merged by the caretaker.
|
||||||
const {url, id} = await this.pushChangesToForkAndCreatePullRequest(
|
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 ` +
|
`Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` +
|
||||||
`branch (${nextBranch}).`);
|
`branch (${nextBranch}).`);
|
||||||
|
|
||||||
|
@ -41,11 +41,12 @@ export class CutLongTermSupportPatchAction extends ReleaseAction {
|
|||||||
async perform() {
|
async perform() {
|
||||||
const ltsBranch = await this._promptForTargetLtsBranch();
|
const ltsBranch = await this._promptForTargetLtsBranch();
|
||||||
const newVersion = semverInc(ltsBranch.version, 'patch');
|
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.waitForPullRequestToBeMerged(id);
|
||||||
await this.buildAndPublish(newVersion, ltsBranch.name, ltsBranch.npmDistTag);
|
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. */
|
/** 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 {branchName} = this.active.latest;
|
||||||
const newVersion = this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
await this.buildAndPublish(newVersion, branchName, 'latest');
|
await this.buildAndPublish(newVersion, branchName, 'latest');
|
||||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async isActive(active: ActiveReleaseTrains) {
|
static async isActive(active: ActiveReleaseTrains) {
|
||||||
|
@ -32,7 +32,8 @@ export class CutNextPrereleaseAction extends ReleaseAction {
|
|||||||
const {branchName} = releaseTrain;
|
const {branchName} = releaseTrain;
|
||||||
const newVersion = await this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
await this.buildAndPublish(newVersion, branchName, 'next');
|
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
|
// to the next release-train, cherry-pick the changelog into the primary
|
||||||
// development branch. i.e. the `next` branch that is usually `master`.
|
// development branch. i.e. the `next` branch that is usually `master`.
|
||||||
if (releaseTrain !== this.active.next) {
|
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 {branchName} = this.active.releaseCandidate!;
|
||||||
const newVersion = this._newVersion;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
await this.buildAndPublish(newVersion, branchName, 'next');
|
await this.buildAndPublish(newVersion, branchName, 'next');
|
||||||
await this.cherryPickChangelogIntoNextBranch(newVersion, branchName);
|
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async isActive(active: ActiveReleaseTrains) {
|
static async isActive(active: ActiveReleaseTrains) {
|
||||||
|
@ -31,7 +31,8 @@ export class CutStableAction extends ReleaseAction {
|
|||||||
const isNewMajor = this.active.releaseCandidate?.isMajor;
|
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.waitForPullRequestToBeMerged(id);
|
||||||
await this.buildAndPublish(newVersion, branchName, 'latest');
|
await this.buildAndPublish(newVersion, branchName, 'latest');
|
||||||
@ -53,7 +54,7 @@ export class CutStableAction extends ReleaseAction {
|
|||||||
await invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version);
|
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. */
|
/** Gets the new stable version of the release candidate release-train. */
|
||||||
|
@ -8,12 +8,13 @@
|
|||||||
|
|
||||||
import * as semver from 'semver';
|
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 {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
||||||
import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version';
|
import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version';
|
||||||
import {ReleaseAction} from '../actions';
|
import {ReleaseAction} from '../actions';
|
||||||
import {getCommitMessageForExceptionalNextVersionBump} from '../commit-message';
|
import {getCommitMessageForExceptionalNextVersionBump, getReleaseNoteCherryPickCommitMessage} from '../commit-message';
|
||||||
import {packageJsonPath} from '../constants';
|
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
|
* 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
|
// 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
|
// 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.
|
// created branch instead of re-fetching from the upstream.
|
||||||
const stagingPullRequest =
|
const {pullRequest: {id}, releaseNotes} =
|
||||||
await this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch);
|
await this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch);
|
||||||
|
|
||||||
// Wait for the staging PR to be merged. Then build and publish the feature-freeze next
|
// 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
|
// pre-release. Finally, cherry-pick the release notes into the next branch in combination
|
||||||
// with bumping the version to the next minor too.
|
// 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.buildAndPublish(newVersion, newBranch, 'next');
|
||||||
await this._createNextBranchUpdatePullRequest(newVersion, newBranch);
|
await this._createNextBranchUpdatePullRequest(releaseNotes, newVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a new version branch from the next branch. */
|
/** 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
|
* 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 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;
|
const {branchName: nextBranch, version} = this.active.next;
|
||||||
// We increase the version for the next branch to the next minor. The team can decide
|
// 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.
|
// 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.
|
// a separate commit that makes it clear where the changelog is cherry-picked from.
|
||||||
await this.createCommit(bumpCommitMessage, [packageJsonPath]);
|
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 ` +
|
let nextPullRequestMessage = `The previous "next" release-train has moved into the ` +
|
||||||
`release-candidate phase. This PR updates the next branch to the subsequent ` +
|
`release-candidate phase. This PR updates the next branch to the subsequent ` +
|
||||||
`release-train.`;
|
`release-train.\n\nAlso this PR cherry-picks the changelog for ` +
|
||||||
const hasChangelogCherryPicked =
|
`v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`;
|
||||||
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}".`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextUpdatePullRequest = await this.pushChangesToForkAndCreatePullRequest(
|
const nextUpdatePullRequest = await this.pushChangesToForkAndCreatePullRequest(
|
||||||
nextBranch, `next-release-train-${newNextVersion}`,
|
nextBranch, `next-release-train-${newNextVersion}`,
|
||||||
|
@ -10,25 +10,12 @@ import {join} from 'path';
|
|||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
import {getCommitsInRange} from '../../../commit-message/utils';
|
import {getCommitsInRange} from '../../../commit-message/utils';
|
||||||
import {getConfig} from '../../../utils/config';
|
|
||||||
import {promptInput} from '../../../utils/console';
|
import {promptInput} from '../../../utils/console';
|
||||||
import {GitClient} from '../../../utils/git/index';
|
import {GitClient} from '../../../utils/git/index';
|
||||||
import {getReleaseConfig} from '../../config/index';
|
import {ReleaseConfig} from '../../config/index';
|
||||||
import {changelogPath} from '../constants';
|
import {changelogPath} from '../constants';
|
||||||
import {RenderContext} from './context';
|
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. */
|
/** Gets the path for the changelog file in a given project. */
|
||||||
export function getLocalChangelogFilePath(projectDir: string): string {
|
export function getLocalChangelogFilePath(projectDir: string): string {
|
||||||
return join(projectDir, changelogPath);
|
return join(projectDir, changelogPath);
|
||||||
@ -37,33 +24,35 @@ export function getLocalChangelogFilePath(projectDir: string): string {
|
|||||||
|
|
||||||
/** Release note generation. */
|
/** Release note generation. */
|
||||||
export class ReleaseNotes {
|
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. */
|
/** An instance of GitClient. */
|
||||||
private git = GitClient.getInstance();
|
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. */
|
/** The RenderContext to be used during rendering. */
|
||||||
private renderContext: RenderContext|undefined;
|
private renderContext: RenderContext|undefined;
|
||||||
/** The title to use for the release. */
|
/** The title to use for the release. */
|
||||||
private title: string|false|undefined;
|
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. */
|
/** Retrieve the release note generated for a Github Release. */
|
||||||
async getGithubReleaseEntry(): Promise<string> {
|
async getGithubReleaseEntry(): Promise<string> {
|
||||||
return await renderFile(
|
return renderFile(
|
||||||
join(__dirname, 'templates/github-release.ejs'), await this.generateRenderContext());
|
join(__dirname, 'templates/github-release.ejs'), await this.generateRenderContext(),
|
||||||
|
{rmWhitespace: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieve the release note generated for a CHANGELOG entry. */
|
/** Retrieve the release note generated for a CHANGELOG entry. */
|
||||||
async getChangelogEntry() {
|
async getChangelogEntry() {
|
||||||
return await renderFile(
|
return renderFile(
|
||||||
join(__dirname, 'templates/changelog.ejs'), await this.generateRenderContext());
|
join(__dirname, 'templates/changelog.ejs'), await this.generateRenderContext(),
|
||||||
|
{rmWhitespace: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +61,7 @@ export class ReleaseNotes {
|
|||||||
*/
|
*/
|
||||||
async promptForReleaseTitle() {
|
async promptForReleaseTitle() {
|
||||||
if (this.title === undefined) {
|
if (this.title === undefined) {
|
||||||
if (this.config.useReleaseTitle) {
|
if (this.config.releaseNotes.useReleaseTitle) {
|
||||||
this.title = await promptInput('Please provide a title for the release:');
|
this.title = await promptInput('Please provide a title for the release:');
|
||||||
} else {
|
} else {
|
||||||
this.title = false;
|
this.title = false;
|
||||||
@ -86,10 +75,10 @@ export class ReleaseNotes {
|
|||||||
if (!this.renderContext) {
|
if (!this.renderContext) {
|
||||||
this.renderContext = new RenderContext({
|
this.renderContext = new RenderContext({
|
||||||
commits: await this.commits,
|
commits: await this.commits,
|
||||||
github: getConfig().github,
|
github: this.git.remoteConfig,
|
||||||
version: this.version.format(),
|
version: this.version.format(),
|
||||||
groupOrder: this.config.groupOrder,
|
groupOrder: this.config.releaseNotes.groupOrder,
|
||||||
hiddenScopes: this.config.hiddenScopes,
|
hiddenScopes: this.config.releaseNotes.hiddenScopes,
|
||||||
title: await this.promptForReleaseTitle(),
|
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.
|
# enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional.
|
||||||
# https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa.
|
# https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa.
|
||||||
args = ["--nobazel_patch_module_resolver"],
|
args = ["--nobazel_patch_module_resolver"],
|
||||||
|
data = [
|
||||||
|
"//dev-infra/release/publish/release-notes/templates",
|
||||||
|
],
|
||||||
deps = [":test_lib"],
|
deps = [":test_lib"],
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,7 @@ import {ReleaseTrain} from '../../versioning/release-trains';
|
|||||||
import {ReleaseAction} from '../actions';
|
import {ReleaseAction} from '../actions';
|
||||||
import {actions} from '../actions/index';
|
import {actions} from '../actions/index';
|
||||||
import {changelogPath} from '../constants';
|
import {changelogPath} from '../constants';
|
||||||
|
import {ReleaseNotes} from '../release-notes/release-notes';
|
||||||
|
|
||||||
import {fakeNpmPackageQueryRequest, getChangelogForVersion, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';
|
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 fakeReleaseNotes = getChangelogForVersion(version.format());
|
||||||
const forkBranchName = `changelog-cherry-pick-${version}`;
|
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} =
|
const {repo, fork, instance, testTmpDir} =
|
||||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||||
|
|
||||||
@ -109,62 +110,6 @@ describe('common release action logic', () => {
|
|||||||
expect(changelogContent).toEqual(`${fakeReleaseNotes}Existing changelog`);
|
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 () => {
|
it('should push changes to a fork for creating a pull request', async () => {
|
||||||
const {repo, fork, instance, gitClient} =
|
const {repo, fork, instance, gitClient} =
|
||||||
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
setupReleaseActionForTesting(TestAction, baseReleaseTrains);
|
||||||
@ -214,6 +159,7 @@ class TestAction extends ReleaseAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) {
|
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.');
|
'Expected next release-train update branch be created in fork.');
|
||||||
|
|
||||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||||
expect(releaseConfig.generateReleaseNotesForHead).toHaveBeenCalledTimes(1);
|
|
||||||
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
||||||
expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'next', undefined);
|
expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'next', undefined);
|
||||||
expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, '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 {join} from 'path';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
import * as commitMessageUtils from '../../../commit-message/utils';
|
||||||
import {GithubConfig} from '../../../utils/config';
|
import {GithubConfig} from '../../../utils/config';
|
||||||
import * as console from '../../../utils/console';
|
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 {ReleaseConfig} from '../../config/index';
|
||||||
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
|
||||||
import * as npm from '../../versioning/npm-publish';
|
import * as npm from '../../versioning/npm-publish';
|
||||||
@ -21,6 +22,7 @@ import {_npmPackageInfoCache, NpmPackageInfo} from '../../versioning/npm-registr
|
|||||||
import {ReleaseAction, ReleaseActionConstructor} from '../actions';
|
import {ReleaseAction, ReleaseActionConstructor} from '../actions';
|
||||||
import * as constants from '../constants';
|
import * as constants from '../constants';
|
||||||
import * as externalCommands from '../external-commands';
|
import * as externalCommands from '../external-commands';
|
||||||
|
import {buildDateStamp} from '../release-notes/context';
|
||||||
|
|
||||||
import {GithubTestingRepo} from './github-api-testing';
|
import {GithubTestingRepo} from './github-api-testing';
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ export function getTestingMocksForReleaseAction() {
|
|||||||
'@angular/pkg1',
|
'@angular/pkg1',
|
||||||
'@angular/pkg2',
|
'@angular/pkg2',
|
||||||
],
|
],
|
||||||
generateReleaseNotesForHead: jasmine.createSpy('generateReleaseNotesForHead').and.resolveTo(),
|
releaseNotes: {},
|
||||||
buildPackages: () => {
|
buildPackages: () => {
|
||||||
throw Error('Not implemented');
|
throw Error('Not implemented');
|
||||||
},
|
},
|
||||||
@ -67,6 +69,9 @@ export function getTestingMocksForReleaseAction() {
|
|||||||
export function setupReleaseActionForTesting<T extends ReleaseAction>(
|
export function setupReleaseActionForTesting<T extends ReleaseAction>(
|
||||||
actionCtor: ReleaseActionConstructor<T>, active: ActiveReleaseTrains,
|
actionCtor: ReleaseActionConstructor<T>, active: ActiveReleaseTrains,
|
||||||
isNextPublishedToNpm = true): TestReleaseAction<T> {
|
isNextPublishedToNpm = true): TestReleaseAction<T> {
|
||||||
|
installVirtualGitClientSpies();
|
||||||
|
spyOn(commitMessageUtils, 'getCommitsInRange').and.returnValue(Promise.resolve([]));
|
||||||
|
|
||||||
// Reset existing HTTP interceptors.
|
// Reset existing HTTP interceptors.
|
||||||
nock.cleanAll();
|
nock.cleanAll();
|
||||||
|
|
||||||
@ -121,7 +126,7 @@ export function parse(version: string): semver.SemVer {
|
|||||||
|
|
||||||
/** Gets a changelog for the specified version. */
|
/** Gets a changelog for the specified version. */
|
||||||
export function getChangelogForVersion(version: string): string {
|
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(
|
export async function expectStagingAndPublishWithoutCherryPick(
|
||||||
@ -166,7 +171,6 @@ export async function expectStagingAndPublishWithoutCherryPick(
|
|||||||
'Expected release staging branch to be created in fork.');
|
'Expected release staging branch to be created in fork.');
|
||||||
|
|
||||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||||
expect(releaseConfig.generateReleaseNotesForHead).toHaveBeenCalledTimes(1);
|
|
||||||
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
||||||
expect(npm.runNpmPublish)
|
expect(npm.runNpmPublish)
|
||||||
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
||||||
@ -235,7 +239,6 @@ export async function expectStagingAndPublishWithCherryPick(
|
|||||||
'Expected cherry-pick branch to be created in fork.');
|
'Expected cherry-pick branch to be created in fork.');
|
||||||
|
|
||||||
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1);
|
||||||
expect(releaseConfig.generateReleaseNotesForHead).toHaveBeenCalledTimes(1);
|
|
||||||
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
|
||||||
expect(npm.runNpmPublish)
|
expect(npm.runNpmPublish)
|
||||||
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
.toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined);
|
||||||
|
@ -32,7 +32,7 @@ describe('ng-dev release set-dist-tag', () => {
|
|||||||
npmPackages,
|
npmPackages,
|
||||||
publishRegistry,
|
publishRegistry,
|
||||||
buildPackages: async () => [],
|
buildPackages: async () => [],
|
||||||
generateReleaseNotesForHead: async () => {}
|
releaseNotes: {},
|
||||||
});
|
});
|
||||||
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
|
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ ts_library(
|
|||||||
"@npm//@types/jasmine",
|
"@npm//@types/jasmine",
|
||||||
"@npm//@types/minimist",
|
"@npm//@types/minimist",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
|
"@npm//@types/semver",
|
||||||
"@npm//minimist",
|
"@npm//minimist",
|
||||||
|
"@npm//semver",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import {SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
import {SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||||
import * as parseArgs from 'minimist';
|
import * as parseArgs from 'minimist';
|
||||||
|
import {SemVer} from 'semver';
|
||||||
|
|
||||||
import {NgDevConfig} from '../config';
|
import {NgDevConfig} from '../config';
|
||||||
import {GitClient} from '../git/index';
|
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. */
|
/** List of pushed heads to a given remote ref. */
|
||||||
pushed: {remote: RemoteRef, head: GitHead}[] = [];
|
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. */
|
/** Override for the actual Git client command execution. */
|
||||||
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
|
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
|
||||||
const [command, ...rawArgs] = args;
|
const [command, ...rawArgs] = args;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user