From 393ce94718cccf3d69fff44cb509ab487d61f3de Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 19 Apr 2021 11:58:32 -0700 Subject: [PATCH] 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 --- dev-infra/build-worker.js | 4 +- dev-infra/ng-dev.js | 321 ++++++++++++------ .../pr/merge/defaults/integration.spec.ts | 2 +- dev-infra/release/build/build.spec.ts | 2 +- dev-infra/release/config/index.ts | 16 +- dev-infra/release/publish/actions.ts | 92 ++--- .../release/publish/actions/cut-lts-patch.ts | 5 +- .../release/publish/actions/cut-new-patch.ts | 5 +- .../publish/actions/cut-next-prerelease.ts | 5 +- .../publish/actions/cut-release-candidate.ts | 5 +- .../release/publish/actions/cut-stable.ts | 5 +- .../actions/move-next-into-feature-freeze.ts | 35 +- .../publish/release-notes/release-notes.ts | 51 ++- dev-infra/release/publish/test/BUILD.bazel | 3 + dev-infra/release/publish/test/common.spec.ts | 62 +--- .../move-next-into-feature-freeze.spec.ts | 1 - dev-infra/release/publish/test/test-utils.ts | 13 +- .../release/set-dist-tag/set-dist-tag.spec.ts | 2 +- dev-infra/utils/testing/BUILD.bazel | 2 + dev-infra/utils/testing/virtual-git-client.ts | 10 + 20 files changed, 325 insertions(+), 316 deletions(-) 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;