diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js
index b0e97c27c3..dd7ed545d4 100644
--- a/dev-infra/build-worker.js
+++ b/dev-infra/build-worker.js
@@ -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;
diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js
index e24281c5ef..9f95925b63 100755
--- a/dev-infra/ng-dev.js
+++ b/dev-infra/ng-dev.js
@@ -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(`(.*?)(?: {
releaseConfig = {
npmPackages: ['@angular/dev-infra-test-pkg'],
buildPackages: async () => [],
- generateReleaseNotesForHead: async () => {},
+ releaseNotes: {}
};
// The label determination will print warn messages. These should not be
diff --git a/dev-infra/release/build/build.spec.ts b/dev-infra/release/build/build.spec.ts
index 16cb0d5b66..6302752233 100644
--- a/dev-infra/release/build/build.spec.ts
+++ b/dev-infra/release/build/build.spec.ts
@@ -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: '', _: []});
}
diff --git a/dev-infra/release/config/index.ts b/dev-infra/release/config/index.ts
index de0602f294..368e942dda 100644
--- a/dev-infra/release/config/index.ts
+++ b/dev-infra/release/config/index.ts
@@ -26,20 +26,10 @@ export interface ReleaseConfig {
npmPackages: string[];
/** Builds release packages and returns a list of paths pointing to the output. */
buildPackages: () => Promise;
- /** Generates the release notes from the most recent tag to `HEAD`. */
- generateReleaseNotesForHead: (outputPath: string) => Promise;
- /**
- * 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 = 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);
diff --git a/dev-infra/release/publish/actions.ts b/dev-infra/release/publish/actions.ts
index 556c605fb5..01e3211703 100644
--- a/dev-infra/release/publish/actions.ts
+++ b/dev-infra/release/publish/actions.ts
@@ -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 {
- 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 {
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 {
- 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 {
+ 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 {
+ 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 {
+ releaseNotes: ReleaseNotes, stagingBranch: string): Promise {
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}).`);
diff --git a/dev-infra/release/publish/actions/cut-lts-patch.ts b/dev-infra/release/publish/actions/cut-lts-patch.ts
index a3593f4929..91b839b192 100644
--- a/dev-infra/release/publish/actions/cut-lts-patch.ts
+++ b/dev-infra/release/publish/actions/cut-lts-patch.ts
@@ -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. */
diff --git a/dev-infra/release/publish/actions/cut-new-patch.ts b/dev-infra/release/publish/actions/cut-new-patch.ts
index fe8d79203f..4c494d2a37 100644
--- a/dev-infra/release/publish/actions/cut-new-patch.ts
+++ b/dev-infra/release/publish/actions/cut-new-patch.ts
@@ -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) {
diff --git a/dev-infra/release/publish/actions/cut-next-prerelease.ts b/dev-infra/release/publish/actions/cut-next-prerelease.ts
index 40ecb20fd1..b9998d5ccd 100644
--- a/dev-infra/release/publish/actions/cut-next-prerelease.ts
+++ b/dev-infra/release/publish/actions/cut-next-prerelease.ts
@@ -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);
}
}
diff --git a/dev-infra/release/publish/actions/cut-release-candidate.ts b/dev-infra/release/publish/actions/cut-release-candidate.ts
index 716446a1ee..93ba374fcb 100644
--- a/dev-infra/release/publish/actions/cut-release-candidate.ts
+++ b/dev-infra/release/publish/actions/cut-release-candidate.ts
@@ -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) {
diff --git a/dev-infra/release/publish/actions/cut-stable.ts b/dev-infra/release/publish/actions/cut-stable.ts
index 0db17c0bba..e34bd905b5 100644
--- a/dev-infra/release/publish/actions/cut-stable.ts
+++ b/dev-infra/release/publish/actions/cut-stable.ts
@@ -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. */
diff --git a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts b/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts
index 0ecf632227..591b837524 100644
--- a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts
+++ b/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts
@@ -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}`,
diff --git a/dev-infra/release/publish/release-notes/release-notes.ts b/dev-infra/release/publish/release-notes/release-notes.ts
index 15f6eb03df..edbad43238 100644
--- a/dev-infra/release/publish/release-notes/release-notes.ts
+++ b/dev-infra/release/publish/release-notes/release-notes.ts
@@ -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(`(.*?)(?: {
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);
}
}
diff --git a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts
index 11a64ca486..78309754f9 100644
--- a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts
+++ b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts
@@ -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);
diff --git a/dev-infra/release/publish/test/test-utils.ts b/dev-infra/release/publish/test/test-utils.ts
index 76b254dd57..0cffd5b8ff 100644
--- a/dev-infra/release/publish/test/test-utils.ts
+++ b/dev-infra/release/publish/test/test-utils.ts
@@ -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(
actionCtor: ReleaseActionConstructor, active: ActiveReleaseTrains,
isNextPublishedToNpm = true): TestReleaseAction {
+ 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 `Changelog\n\n`;
+ return `\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);
diff --git a/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts b/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts
index f7c02b9853..13cbe1064a 100644
--- a/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts
+++ b/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts
@@ -32,7 +32,7 @@ describe('ng-dev release set-dist-tag', () => {
npmPackages,
publishRegistry,
buildPackages: async () => [],
- generateReleaseNotesForHead: async () => {}
+ releaseNotes: {},
});
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
}
diff --git a/dev-infra/utils/testing/BUILD.bazel b/dev-infra/utils/testing/BUILD.bazel
index 734fbc2153..0962e94d69 100644
--- a/dev-infra/utils/testing/BUILD.bazel
+++ b/dev-infra/utils/testing/BUILD.bazel
@@ -10,6 +10,8 @@ ts_library(
"@npm//@types/jasmine",
"@npm//@types/minimist",
"@npm//@types/node",
+ "@npm//@types/semver",
"@npm//minimist",
+ "@npm//semver",
],
)
diff --git a/dev-infra/utils/testing/virtual-git-client.ts b/dev-infra/utils/testing/virtual-git-client.ts
index a1cbf1e2b5..37d59b446a 100644
--- a/dev-infra/utils/testing/virtual-git-client.ts
+++ b/dev-infra/utils/testing/virtual-git-client.ts
@@ -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 extends GitClient {
const [command, ...rawArgs] = args;