test(dev-infra): add testing for integration of ReleaseNotes in publishing tooling (#41967)

Add testing of the ReleaseNotes integration with the release publishing tooling.

PR Close #41967
This commit is contained in:
Joey Perrott 2021-05-06 12:30:35 -07:00 committed by Zach Arend
parent e89e3cb375
commit 6d87895ffd
9 changed files with 100 additions and 51 deletions

View File

@ -5855,18 +5855,20 @@ function getLocalChangelogFilePath(projectDir) {
}
/** Release note generation. */
class ReleaseNotes {
constructor(version, config) {
constructor(version, startingRef, endingRef) {
this.version = version;
this.config = config;
this.startingRef = startingRef;
this.endingRef = endingRef;
/** 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');
this.commits = this.getCommitsInRange(this.startingRef, this.endingRef);
/** The configuration for release notes. */
this.config = this.getReleaseConfig().releaseNotes;
}
/** Construct a release note generation instance. */
static fromLatestTagToHead(version, config) {
static fromRange(version, startingRef, endingRef) {
return tslib.__awaiter(this, void 0, void 0, function* () {
return new ReleaseNotes(version, config);
return new ReleaseNotes(version, startingRef, endingRef);
});
}
/** Retrieve the release note generated for a Github Release. */
@ -5888,7 +5890,7 @@ class ReleaseNotes {
promptForReleaseTitle() {
return tslib.__awaiter(this, void 0, void 0, function* () {
if (this.title === undefined) {
if (this.config.releaseNotes.useReleaseTitle) {
if (this.config.useReleaseTitle) {
this.title = yield promptInput('Please provide a title for the release:');
}
else {
@ -5906,14 +5908,24 @@ class ReleaseNotes {
commits: yield this.commits,
github: this.git.remoteConfig,
version: this.version.format(),
groupOrder: this.config.releaseNotes.groupOrder,
hiddenScopes: this.config.releaseNotes.hiddenScopes,
groupOrder: this.config.groupOrder,
hiddenScopes: this.config.hiddenScopes,
title: yield this.promptForReleaseTitle(),
});
}
return this.renderContext;
});
}
// These methods are used for access to the utility functions while allowing them to be
// overwritten in subclasses during testing.
getCommitsInRange(from, to) {
return tslib.__awaiter(this, void 0, void 0, function* () {
return getCommitsInRange(from, to);
});
}
getReleaseConfig(config) {
return getReleaseConfig(config);
}
}
/**
@ -6194,7 +6206,7 @@ class ReleaseAction {
*/
stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) {
return tslib.__awaiter(this, void 0, void 0, function* () {
const releaseNotes = yield ReleaseNotes.fromLatestTagToHead(newVersion, this.config);
const releaseNotes = yield ReleaseNotes.fromRange(newVersion, this.git.getLatestSemverTag().format(), 'HEAD');
yield this.updateProjectVersion(newVersion);
yield this.prependReleaseNotesToChangelog(releaseNotes);
yield this.waitForEditsAndCreateReleaseCommit(newVersion);

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as semver from 'semver';
import {assertNoErrors, getConfig, NgDevConfig} from '../../utils/config';
/** Interface describing a built package. */

View File

@ -350,7 +350,8 @@ export abstract class ReleaseAction {
protected async stageVersionForBranchAndCreatePullRequest(
newVersion: semver.SemVer, pullRequestBaseBranch: string):
Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> {
const releaseNotes = await ReleaseNotes.fromLatestTagToHead(newVersion, this.config);
const releaseNotes =
await ReleaseNotes.fromRange(newVersion, this.git.getLatestSemverTag().format(), 'HEAD');
await this.updateProjectVersion(newVersion);
await this.prependReleaseNotesToChangelog(releaseNotes);
await this.waitForEditsAndCreateReleaseCommit(newVersion);

View File

@ -8,11 +8,12 @@
import {renderFile} from 'ejs';
import {join} from 'path';
import * as semver from 'semver';
import {CommitFromGitLog} from '../../../commit-message/parse';
import {getCommitsInRange} from '../../../commit-message/utils';
import {promptInput} from '../../../utils/console';
import {GitClient} from '../../../utils/git/index';
import {ReleaseConfig} from '../../config/index';
import {DevInfraReleaseConfig, getReleaseConfig, ReleaseNotesConfig} from '../../config/index';
import {changelogPath} from '../constants';
import {RenderContext} from './context';
@ -21,13 +22,10 @@ export function getLocalChangelogFilePath(projectDir: string): string {
return join(projectDir, changelogPath);
}
/** 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);
static async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) {
return new ReleaseNotes(version, startingRef, endingRef);
}
/** An instance of GitClient. */
@ -37,9 +35,13 @@ export class ReleaseNotes {
/** 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');
private commits: Promise<CommitFromGitLog[]> =
this.getCommitsInRange(this.startingRef, this.endingRef);
/** The configuration for release notes. */
private config: ReleaseNotesConfig = this.getReleaseConfig().releaseNotes;
private constructor(public readonly version: semver.SemVer, private config: ReleaseConfig) {}
protected constructor(
public version: semver.SemVer, private startingRef: string, private endingRef: string) {}
/** Retrieve the release note generated for a Github Release. */
async getGithubReleaseEntry(): Promise<string> {
@ -61,7 +63,7 @@ export class ReleaseNotes {
*/
async promptForReleaseTitle() {
if (this.title === undefined) {
if (this.config.releaseNotes.useReleaseTitle) {
if (this.config.useReleaseTitle) {
this.title = await promptInput('Please provide a title for the release:');
} else {
this.title = false;
@ -77,11 +79,22 @@ export class ReleaseNotes {
commits: await this.commits,
github: this.git.remoteConfig,
version: this.version.format(),
groupOrder: this.config.releaseNotes.groupOrder,
hiddenScopes: this.config.releaseNotes.hiddenScopes,
groupOrder: this.config.groupOrder,
hiddenScopes: this.config.hiddenScopes,
title: await this.promptForReleaseTitle(),
});
}
return this.renderContext;
}
// These methods are used for access to the utility functions while allowing them to be
// overwritten in subclasses during testing.
protected async getCommitsInRange(from: string, to?: string) {
return getCommitsInRange(from, to);
}
protected getReleaseConfig(config?: Partial<DevInfraReleaseConfig>) {
return getReleaseConfig(config);
}
}

View File

@ -20,7 +20,7 @@ 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';
import {fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';
describe('common release action logic', () => {
const baseReleaseTrains: ActiveReleaseTrains = {
@ -87,7 +87,6 @@ describe('common release action logic', () => {
describe('changelog cherry-picking', () => {
const {version, branchName} = baseReleaseTrains.latest;
const fakeReleaseNotes = getChangelogForVersion(version.format());
const forkBranchName = `changelog-cherry-pick-${version}`;
it('should prepend the changelog to the next branch', async () => {
@ -96,8 +95,7 @@ describe('common release action logic', () => {
// 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, fakeReleaseNotes)
.expectFindForkRequest(fork)
repo.expectFindForkRequest(fork)
.expectPullRequestToBeCreated('master', fork, forkBranchName, 200)
.expectPullRequestWait(200);
@ -107,7 +105,7 @@ describe('common release action logic', () => {
await instance.testCherryPickWithPullRequest(version, branchName);
const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
expect(changelogContent).toEqual(`${fakeReleaseNotes}Existing changelog`);
expect(changelogContent).toEqual(`Changelog Entry for 10.0.1\n\nExisting changelog`);
});
it('should push changes to a fork for creating a pull request', async () => {
@ -116,8 +114,7 @@ describe('common release action logic', () => {
// 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, fakeReleaseNotes)
.expectFindForkRequest(fork)
repo.expectFindForkRequest(fork)
.expectPullRequestToBeCreated('master', fork, forkBranchName, 200)
.expectPullRequestWait(200);
@ -155,12 +152,12 @@ class TestAction extends ReleaseAction {
}
async testBuildAndPublish(version: semver.SemVer, publishBranch: string, distTag: string) {
const releaseNotes = await ReleaseNotes.fromLatestTagToHead(version, this.config);
const releaseNotes = await ReleaseNotes.fromRange(version, '', '');
await this.buildAndPublish(releaseNotes, publishBranch, distTag);
}
async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) {
const releaseNotes = await ReleaseNotes.fromLatestTagToHead(version, this.config);
const releaseNotes = await ReleaseNotes.fromRange(version, '', '');
await this.cherryPickChangelogIntoNextBranch(releaseNotes, branch);
}
}

View File

@ -60,13 +60,6 @@ export class GithubTestingRepo {
return this;
}
expectChangelogFetch(branch: string, content: string): this {
nock(this.repoApiUrl).get(`/contents/%2FCHANGELOG.md`).query(p => p.ref === branch).reply(200, {
content: new Buffer(content).toString('base64')
});
return this;
}
expectCommitRequest(sha: string, message: string): this {
nock(this.repoApiUrl).get(`/commits/${sha}`).reply(200, {commit: {message}});
return this;

View File

@ -13,7 +13,7 @@ import {ReleaseTrain} from '../../versioning/release-trains';
import {MoveNextIntoFeatureFreezeAction} from '../actions/move-next-into-feature-freeze';
import * as externalCommands from '../external-commands';
import {getChangelogForVersion, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';
import {parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';
describe('move next into feature-freeze action', () => {
it('should not activate if a feature-freeze release-train is active', async () => {
@ -84,7 +84,6 @@ describe('move next into feature-freeze action', () => {
'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`)
.expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA')
.expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName)
.expectChangelogFetch(expectedNewBranch, getChangelogForVersion(expectedVersion))
.expectPullRequestToBeCreated('master', fork, expectedNextUpdateBranch, 100);
// In the fork, we make the following branches appear as non-existent,

View File

@ -0,0 +1,43 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as semver from 'semver';
import {DevInfraReleaseConfig, ReleaseConfig} from '../../../config';
import {ReleaseNotes} from '../../release-notes/release-notes';
/**
* Mock version of the ReleaseNotes for testing, preventing actual calls to git for commits and
* returning versioned entry strings.
*/
class MockReleaseNotes extends ReleaseNotes {
static async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) {
return new MockReleaseNotes(version, startingRef, endingRef);
}
async getChangelogEntry() {
return `Changelog Entry for ${this.version}`;
}
async getGithubReleaseEntry() {
return `Github Release Entry for ${this.version}`;
}
// Overrides of utility functions which call out to other tools and are unused in this mock.
protected async getCommitsInRange(from: string, to?: string) {
return [];
}
protected getReleaseConfig(config?: Partial<DevInfraReleaseConfig>) {
return {} as ReleaseConfig;
}
}
/** Replace the ReleaseNotes static builder function with the MockReleaseNotes builder function. */
export function installMockReleaseNotes() {
spyOn(ReleaseNotes, 'fromRange').and.callFake(MockReleaseNotes.fromRange);
}

View File

@ -11,7 +11,6 @@ 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, installVirtualGitClientSpies, VirtualGitClient} from '../../../utils/testing';
@ -22,9 +21,9 @@ 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';
import {installMockReleaseNotes} from './release-notes/release-notes-utils';
/**
* Temporary directory which will be used as project directory in tests. Note that
@ -70,7 +69,7 @@ export function setupReleaseActionForTesting<T extends ReleaseAction>(
actionCtor: ReleaseActionConstructor<T>, active: ActiveReleaseTrains,
isNextPublishedToNpm = true): TestReleaseAction<T> {
installVirtualGitClientSpies();
spyOn(commitMessageUtils, 'getCommitsInRange').and.returnValue(Promise.resolve([]));
installMockReleaseNotes();
// Reset existing HTTP interceptors.
nock.cleanAll();
@ -123,11 +122,6 @@ export function parse(version: string): semver.SemVer {
return semver.parse(version)!;
}
/** Gets a changelog for the specified version. */
export function getChangelogForVersion(version: string): string {
return `<a name="${version}"></a>\n# ${version} (${buildDateStamp()})\n\n\n`;
}
export async function expectStagingAndPublishWithoutCherryPick(
action: TestReleaseAction, expectedBranch: string, expectedVersion: string,
expectedNpmDistTag: string) {
@ -197,7 +191,6 @@ export async function expectStagingAndPublishWithCherryPick(
'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`)
.expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA')
.expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName)
.expectChangelogFetch(expectedBranch, getChangelogForVersion(expectedVersion))
.expectPullRequestToBeCreated('master', fork, expectedCherryPickForkBranch, 300)
.expectPullRequestWait(300);