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:
Joey Perrott 2021-04-19 11:58:32 -07:00 committed by Alex Rickabaugh
parent 90d4b2277b
commit 393ce94718
20 changed files with 325 additions and 316 deletions

View File

@ -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;

View File

@ -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}.`));

View File

@ -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

View File

@ -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: '', _: []});
}

View File

@ -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);

View File

@ -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}).`);

View File

@ -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. */

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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. */

View File

@ -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}`,

View File

@ -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(),
});
}

View File

@ -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"],
)

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -32,7 +32,7 @@ describe('ng-dev release set-dist-tag', () => {
npmPackages,
publishRegistry,
buildPackages: async () => [],
generateReleaseNotesForHead: async () => {}
releaseNotes: {},
});
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
}

View File

@ -10,6 +10,8 @@ ts_library(
"@npm//@types/jasmine",
"@npm//@types/minimist",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//minimist",
"@npm//semver",
],
)

View File

@ -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;