feat(dev-infra): create ReleaseNotes class for generating release notes during publishing (#41476)
Generate release notes to be used for entries in both CHANGELOG.md files as well as Github releases. PR Close #41476
This commit is contained in:
parent
7e6989ee4b
commit
5e0d5a9ec2
|
@ -44,6 +44,7 @@ pkg_npm(
|
|||
"index.bzl",
|
||||
"//dev-infra/bazel:files",
|
||||
"//dev-infra/benchmark:files",
|
||||
"//dev-infra/release/publish/release-notes/templates",
|
||||
],
|
||||
substitutions = {
|
||||
# angular/angular should not consume it's own packages, so we use
|
||||
|
|
|
@ -25,6 +25,7 @@ var cliProgress = require('cli-progress');
|
|||
var os = require('os');
|
||||
var minimatch = require('minimatch');
|
||||
var ora = require('ora');
|
||||
require('ejs');
|
||||
var glob = require('glob');
|
||||
var ts = require('typescript');
|
||||
|
||||
|
@ -5580,6 +5581,11 @@ function isCommitClosingPullRequest(api, sha, id) {
|
|||
* 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
|
||||
*/
|
||||
/** List of types to be included in the release notes. */
|
||||
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.
|
||||
|
|
|
@ -36,6 +36,25 @@ export interface ReleaseConfig {
|
|||
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;
|
||||
}
|
||||
|
||||
/** Configuration for creating release notes during publishing. */
|
||||
export interface ReleaseNotesConfig {
|
||||
/** Whether to prompt for and include a release title in the generated release notes. */
|
||||
useReleaseTitle?: boolean;
|
||||
/** List of commit scopes to disclude from generated release notes. */
|
||||
hiddenScopes?: string[];
|
||||
/**
|
||||
* List of commit groups, either {npmScope}/{scope} or {scope}, to use for ordering.
|
||||
*
|
||||
* Each group for the release notes, will appear in the order provided in groupOrder and any other
|
||||
* groups will appear after these groups, sorted by `Array.sort`'s default sorting order.
|
||||
*/
|
||||
groupOrder?: string[];
|
||||
}
|
||||
|
||||
/** Configuration for releases in the dev-infra configuration. */
|
||||
|
|
|
@ -8,15 +8,18 @@ ts_library(
|
|||
module_name = "@angular/dev-infra-private/release/publish",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/commit-message",
|
||||
"//dev-infra/pr/merge",
|
||||
"//dev-infra/release/config",
|
||||
"//dev-infra/release/versioning",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@octokit/rest",
|
||||
"@npm//@types/ejs",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/semver",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//ejs",
|
||||
"@npm//inquirer",
|
||||
"@npm//ora",
|
||||
"@npm//semver",
|
||||
|
|
|
@ -14,7 +14,7 @@ import * as semver from 'semver';
|
|||
import {debug, error, green, info, promptConfirm, red, warn, yellow} from '../../utils/console';
|
||||
import {getListCommitsInBranchUrl, getRepositoryGitUrl} from '../../utils/git/github-urls';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {BuiltPackage, ReleaseConfig} from '../config';
|
||||
import {BuiltPackage, ReleaseConfig} from '../config/index';
|
||||
import {ActiveReleaseTrains} from '../versioning/active-release-trains';
|
||||
import {runNpmPublish} from '../versioning/npm-publish';
|
||||
|
||||
|
@ -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';
|
||||
import {getDefaultExtractReleaseNotesPattern, getLocalChangelogFilePath} from './release-notes/release-notes';
|
||||
|
||||
/** Interface describing a Github repository. */
|
||||
export interface GithubRepo {
|
||||
|
|
|
@ -11,7 +11,7 @@ import {ListChoiceOptions, prompt} from 'inquirer';
|
|||
import {GithubConfig} from '../../utils/config';
|
||||
import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {ReleaseConfig} from '../config';
|
||||
import {ReleaseConfig} from '../config/index';
|
||||
import {ActiveReleaseTrains, fetchActiveReleaseTrains, nextBranchName} from '../versioning/active-release-trains';
|
||||
import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish';
|
||||
import {printActiveReleaseTrains} from '../versioning/print-active-trains';
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* @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 {join} from 'path';
|
||||
import * as semver from 'semver';
|
||||
import {changelogPath} from './constants';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* @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 {COMMIT_TYPES, ReleaseNotesLevel} from '../../../commit-message/config';
|
||||
import {CommitFromGitLog} from '../../../commit-message/parse';
|
||||
import {GithubConfig} from '../../../utils/config';
|
||||
import {ReleaseNotesConfig} from '../../config/index';
|
||||
|
||||
|
||||
/** List of types to be included in the release notes. */
|
||||
const typesToIncludeInReleaseNotes =
|
||||
Object.values(COMMIT_TYPES)
|
||||
.filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible)
|
||||
.map(type => type.name);
|
||||
|
||||
/** Data used for context during rendering. */
|
||||
export interface RenderContextData {
|
||||
title: string|false;
|
||||
groupOrder?: ReleaseNotesConfig['groupOrder'];
|
||||
hiddenScopes?: ReleaseNotesConfig['hiddenScopes'];
|
||||
date?: Date;
|
||||
commits: CommitFromGitLog[];
|
||||
version: string;
|
||||
github: GithubConfig;
|
||||
}
|
||||
|
||||
/** Context class used for rendering release notes. */
|
||||
export class RenderContext {
|
||||
/** An array of group names in sort order if defined. */
|
||||
private readonly groupOrder = this.data.groupOrder || [];
|
||||
/** An array of scopes to hide from the release entry output. */
|
||||
private readonly hiddenScopes = this.data.hiddenScopes || [];
|
||||
/** The title of the release, or `false` if no title should be used. */
|
||||
readonly title = this.data.title;
|
||||
/** An array of commits in the release period. */
|
||||
readonly commits = this.data.commits;
|
||||
/** The version of the release. */
|
||||
readonly version = this.data.version;
|
||||
/** The date stamp string for use in the release notes entry. */
|
||||
readonly dateStamp = buildDateStamp(this.data.date);
|
||||
|
||||
constructor(private readonly data: RenderContextData) {}
|
||||
|
||||
/**
|
||||
* 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: CommitFromGitLog[]) {
|
||||
/** The discovered groups to organize into. */
|
||||
const groups = new Map<string, CommitFromGitLog[]>();
|
||||
|
||||
// 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: CommitFromGitLog) => {
|
||||
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: keyof CommitFromGitLog) {
|
||||
return (commit: CommitFromGitLog) => {
|
||||
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: keyof CommitFromGitLog) {
|
||||
const set = new Set<CommitFromGitLog[typeof field]>();
|
||||
return (commit: CommitFromGitLog) => {
|
||||
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.
|
||||
*/
|
||||
export 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('-');
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @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 {renderFile} from 'ejs';
|
||||
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 {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);
|
||||
}
|
||||
|
||||
|
||||
/** Release note generation. */
|
||||
export class ReleaseNotes {
|
||||
/** An instance of GitClient. */
|
||||
private git = new GitClient();
|
||||
/** 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;
|
||||
|
||||
constructor(private version: semver.SemVer) {}
|
||||
|
||||
/** 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());
|
||||
}
|
||||
|
||||
/** Retrieve the release note generated for a CHANGELOG entry. */
|
||||
async getChangelogEntry() {
|
||||
return await renderFile(
|
||||
join(__dirname, 'templates/changelog.ejs'), await this.generateRenderContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for a title for the release, if the project's configuration is defined to use a
|
||||
* title.
|
||||
*/
|
||||
async promptForReleaseTitle() {
|
||||
if (this.title === undefined) {
|
||||
if (this.config.useReleaseTitle) {
|
||||
this.title = await 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. */
|
||||
private async generateRenderContext(): Promise<RenderContext> {
|
||||
if (!this.renderContext) {
|
||||
this.renderContext = new RenderContext({
|
||||
commits: await this.commits,
|
||||
github: getConfig().github,
|
||||
version: this.version.format(),
|
||||
groupOrder: this.config.groupOrder,
|
||||
hiddenScopes: this.config.hiddenScopes,
|
||||
title: await this.promptForReleaseTitle(),
|
||||
});
|
||||
}
|
||||
return this.renderContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package(default_visibility = ["//dev-infra:__subpackages__"])
|
||||
|
||||
filegroup(
|
||||
name = "templates",
|
||||
srcs = glob(["*.ejs"]),
|
||||
)
|
|
@ -0,0 +1,77 @@
|
|||
<a name="<%- version %>"></a>
|
||||
# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>)
|
||||
|
||||
<%_
|
||||
const commitsInChangelog = commits.filter(includeInReleaseNotes());
|
||||
for (const group of asCommitGroups(commitsInChangelog)) {
|
||||
_%>
|
||||
|
||||
### <%- group.title %>
|
||||
| Commit | Description |
|
||||
| -- | -- |
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
| <%- commit.shortHash %> | <%- commit.header %> |
|
||||
<%_
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const breakingChanges = commits.filter(contains('breakingChanges'));
|
||||
if (breakingChanges.length) {
|
||||
_%>
|
||||
## Breaking Changes
|
||||
|
||||
<%_
|
||||
for (const group of asCommitGroups(breakingChanges)) {
|
||||
_%>
|
||||
### <%- group.title %>
|
||||
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
<%- commit.breakingChanges[0].text %>
|
||||
|
||||
<%_
|
||||
}
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const deprecations = commits.filter(contains('deprecations'));
|
||||
if (deprecations.length) {
|
||||
_%>
|
||||
## Deprecations
|
||||
<%_
|
||||
for (const group of asCommitGroups(deprecations)) {
|
||||
_%>
|
||||
### <%- group.title %>
|
||||
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
<%- commit.deprecations[0].text %>
|
||||
<%_
|
||||
}
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const authors = commits.filter(unique('author')).map(c => c.author).sort();
|
||||
if (authors.length === 1) {
|
||||
_%>
|
||||
## Special Thanks:
|
||||
<%- authors[0]%>
|
||||
<%_
|
||||
}
|
||||
if (authors.length > 1) {
|
||||
_%>
|
||||
## Special Thanks:
|
||||
<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[0] %>
|
||||
<%_
|
||||
}
|
||||
_%>
|
|
@ -0,0 +1,77 @@
|
|||
<a name="<%- version %>"></a>
|
||||
# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>)
|
||||
|
||||
<%_
|
||||
const commitsInChangelog = commits.filter(includeInReleaseNotes());
|
||||
for (const group of asCommitGroups(commitsInChangelog)) {
|
||||
_%>
|
||||
|
||||
### <%- group.title %>
|
||||
| Commit | Description |
|
||||
| -- | -- |
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
| <%- commit.shortHash %> | <%- commit.header %> |
|
||||
<%_
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const breakingChanges = commits.filter(contains('breakingChanges'));
|
||||
if (breakingChanges.length) {
|
||||
_%>
|
||||
## Breaking Changes
|
||||
|
||||
<%_
|
||||
for (const group of asCommitGroups(breakingChanges)) {
|
||||
_%>
|
||||
### <%- group.title %>
|
||||
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
<%- commit.breakingChanges[0].text %>
|
||||
|
||||
<%_
|
||||
}
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const deprecations = commits.filter(contains('deprecations'));
|
||||
if (deprecations.length) {
|
||||
_%>
|
||||
## Deprecations
|
||||
<%_
|
||||
for (const group of asCommitGroups(deprecations)) {
|
||||
_%>
|
||||
### <%- group.title %>
|
||||
|
||||
<%_
|
||||
for (const commit of group.commits) {
|
||||
_%>
|
||||
<%- commit.deprecations[0].text %>
|
||||
<%_
|
||||
}
|
||||
}
|
||||
}
|
||||
_%>
|
||||
|
||||
<%_
|
||||
const authors = commits.filter(unique('author')).map(c => c.author).sort();
|
||||
if (authors.length === 1) {
|
||||
_%>
|
||||
## Special Thanks:
|
||||
<%- authors[0]%>
|
||||
<%_
|
||||
}
|
||||
if (authors.length > 1) {
|
||||
_%>
|
||||
## Special Thanks:
|
||||
<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[0] %>
|
||||
<%_
|
||||
}
|
||||
_%>
|
|
@ -8,6 +8,7 @@ ts_library(
|
|||
]),
|
||||
module_name = "@angular/dev-infra-private/release/test",
|
||||
deps = [
|
||||
"//dev-infra/commit-message",
|
||||
"//dev-infra/release/config",
|
||||
"//dev-infra/release/publish",
|
||||
"//dev-infra/release/versioning",
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* @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 {CommitFromGitLog, parseCommitFromGitLog} from '../../../../commit-message/parse';
|
||||
import {commitMessageBuilder} from '../../../../commit-message/test-util';
|
||||
import {RenderContext, RenderContextData,} from '../../release-notes/context';
|
||||
|
||||
const defaultContextData: RenderContextData = {
|
||||
commits: [],
|
||||
github: {
|
||||
name: 'repoName',
|
||||
owner: 'repoOwner',
|
||||
},
|
||||
title: false,
|
||||
version: '1.2.3',
|
||||
};
|
||||
|
||||
describe('RenderContext', () => {
|
||||
beforeAll(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
it('contains a date stamp using the current date by default', async () => {
|
||||
jasmine.clock().mockDate(new Date(1996, 11, 11));
|
||||
const renderContext = new RenderContext(defaultContextData);
|
||||
expect(renderContext.dateStamp).toBe('1996-12-11');
|
||||
});
|
||||
|
||||
it('contains a date stamp using a provided date', async () => {
|
||||
const data = {...defaultContextData, date: new Date(2000, 0, 20)};
|
||||
const renderContext = new RenderContext(data);
|
||||
expect(renderContext.dateStamp).toBe('2000-01-20');
|
||||
});
|
||||
|
||||
it('filters to include only commits which have specified field', () => {
|
||||
const renderContext = new RenderContext(defaultContextData);
|
||||
const matchingCommits = commitsFromList(2, 15);
|
||||
expect(commits.filter(renderContext.contains('breakingChanges'))).toEqual(matchingCommits);
|
||||
});
|
||||
|
||||
it('filters to include only the first commit discovered with a unique value for a specified field',
|
||||
() => {
|
||||
const renderContext = new RenderContext(defaultContextData);
|
||||
const matchingCommits = commitsFromList(0, 1, 2, 3, 4, 7, 12);
|
||||
expect(commits.filter(renderContext.unique('type'))).toEqual(matchingCommits);
|
||||
});
|
||||
|
||||
|
||||
describe('filters to include commits which are to be included in the release notes', () => {
|
||||
it('including all scopes by default', () => {
|
||||
const renderContext = new RenderContext(defaultContextData);
|
||||
const matchingCommits = commitsFromList(0, 2, 5, 6, 8, 10, 11, 12, 15, 16);
|
||||
expect(commits.filter(renderContext.includeInReleaseNotes())).toEqual(matchingCommits);
|
||||
});
|
||||
|
||||
it('excluding hidden scopes defined in the config', () => {
|
||||
const renderContext = new RenderContext({...defaultContextData, hiddenScopes: ['core']});
|
||||
const matchingCommits = commitsFromList(0, 2, 6, 8, 10, 11, 15, 16);
|
||||
expect(commits.filter(renderContext.includeInReleaseNotes())).toEqual(matchingCommits);
|
||||
});
|
||||
});
|
||||
|
||||
describe('organized lists of commits into groups', () => {
|
||||
let devInfraCommits: CommitFromGitLog[];
|
||||
let coreCommits: CommitFromGitLog[];
|
||||
let compilerCommits: CommitFromGitLog[];
|
||||
let unorganizedCommits: CommitFromGitLog[];
|
||||
function assertOrganizedGroupsMatch(
|
||||
generatedGroups: {title: string, commits: CommitFromGitLog[]}[],
|
||||
providedGroups: {title: string, commits: CommitFromGitLog[]}[]) {
|
||||
expect(generatedGroups.length).toBe(providedGroups.length);
|
||||
generatedGroups.forEach(({title, commits}, idx) => {
|
||||
expect(title).toBe(providedGroups[idx].title);
|
||||
expect(commits).toEqual(jasmine.arrayWithExactContents(providedGroups[idx].commits));
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
devInfraCommits = commits.filter(c => c.scope === 'dev-infra');
|
||||
coreCommits = commits.filter(c => c.scope === 'core');
|
||||
compilerCommits = commits.filter(c => c.scope === 'compiler');
|
||||
unorganizedCommits =
|
||||
[...devInfraCommits, ...coreCommits, ...compilerCommits].sort(() => Math.random() - 0.5);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('with default sorting', () => {
|
||||
const renderContext = new RenderContext(defaultContextData);
|
||||
const organizedCommits = renderContext.asCommitGroups(unorganizedCommits);
|
||||
|
||||
assertOrganizedGroupsMatch(organizedCommits, [
|
||||
{title: 'compiler', commits: compilerCommits},
|
||||
{title: 'core', commits: coreCommits},
|
||||
{title: 'dev-infra', commits: devInfraCommits},
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorted by the provided order in the config', () => {
|
||||
const renderContext =
|
||||
new RenderContext({...defaultContextData, groupOrder: ['core', 'dev-infra']});
|
||||
const organizedCommits = renderContext.asCommitGroups(unorganizedCommits);
|
||||
|
||||
assertOrganizedGroupsMatch(organizedCommits, [
|
||||
{title: 'core', commits: coreCommits},
|
||||
{title: 'dev-infra', commits: devInfraCommits},
|
||||
{title: 'compiler', commits: compilerCommits},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
const buildCommitMessage = commitMessageBuilder({
|
||||
prefix: '',
|
||||
type: '',
|
||||
npmScope: '',
|
||||
scope: '',
|
||||
summary: 'This is a short summary of the change',
|
||||
body: 'This is a longer description of the change',
|
||||
footer: '',
|
||||
});
|
||||
|
||||
function buildCommit(type: string, scope: string, withBreakingChange = false) {
|
||||
const footer = withBreakingChange ? 'BREAKING CHANGE: something is broken now' : '';
|
||||
const parts = {type, scope, footer};
|
||||
return parseCommitFromGitLog(Buffer.from(buildCommitMessage(parts)));
|
||||
}
|
||||
|
||||
|
||||
function commitsFromList(...indexes: number[]) {
|
||||
const output: CommitFromGitLog[] = [];
|
||||
for (const i of indexes) {
|
||||
output.push(commits[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
const commits: CommitFromGitLog[] = [
|
||||
buildCommit('fix', 'platform-browser'),
|
||||
buildCommit('test', 'dev-infra'),
|
||||
buildCommit('feat', 'dev-infra', true),
|
||||
buildCommit('build', 'docs-infra'),
|
||||
buildCommit('docs', 'router'),
|
||||
buildCommit('feat', 'core'),
|
||||
buildCommit('feat', 'common'),
|
||||
buildCommit('refactor', 'compiler'),
|
||||
buildCommit('fix', 'docs-infra'),
|
||||
buildCommit('test', 'core'),
|
||||
buildCommit('feat', 'compiler-cli'),
|
||||
buildCommit('fix', 'dev-infra'),
|
||||
buildCommit('perf', 'core'),
|
||||
buildCommit('docs', 'forms'),
|
||||
buildCommit('refactor', 'dev-infra'),
|
||||
buildCommit('feat', 'docs-infra', true),
|
||||
buildCommit('fix', 'compiler'),
|
||||
];
|
|
@ -17,6 +17,7 @@
|
|||
"chalk": "<from-root>",
|
||||
"cli-progress": "<from-root>",
|
||||
"conventional-commits-parser": "<from-root>",
|
||||
"ejs": "<from-root>",
|
||||
"git-raw-commits": "<from-root>",
|
||||
"glob": "<from-root>",
|
||||
"inquirer": "<from-root>",
|
||||
|
|
|
@ -175,6 +175,7 @@
|
|||
"@octokit/graphql": "^4.3.1",
|
||||
"@types/cli-progress": "^3.4.2",
|
||||
"@types/conventional-commits-parser": "^3.0.1",
|
||||
"@types/ejs": "^3.0.6",
|
||||
"@types/git-raw-commits": "^2.0.0",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
|
@ -187,6 +188,7 @@
|
|||
"cli-progress": "^3.7.0",
|
||||
"conventional-changelog": "^2.0.3",
|
||||
"conventional-commits-parser": "^3.2.1",
|
||||
"ejs": "^3.1.6",
|
||||
"entities": "1.1.1",
|
||||
"firebase-tools": "^7.11.0",
|
||||
"firefox-profile": "1.0.3",
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -2586,6 +2586,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ejs@^3.0.6":
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.0.6.tgz#aca442289df623bfa8e47c23961f0357847b83fe"
|
||||
integrity sha512-fj1hi+ZSW0xPLrJJD+YNwIh9GZbyaIepG26E/gXvp8nCa2pYokxUYO1sK9qjGxp2g8ryZYuon7wmjpwE2cyASQ==
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
|
@ -3642,6 +3647,11 @@ async-settle@^1.0.0:
|
|||
dependencies:
|
||||
async-done "^1.2.2"
|
||||
|
||||
async@0.9.x:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
|
||||
integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
|
||||
|
||||
async@1.2.x:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.2.1.tgz#a4816a17cd5ff516dfa2c7698a453369b9790de0"
|
||||
|
@ -6455,6 +6465,13 @@ ee-first@1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
ejs@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a"
|
||||
integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==
|
||||
dependencies:
|
||||
jake "^10.6.1"
|
||||
|
||||
electron-to-chromium@^1.3.390:
|
||||
version "1.3.394"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.394.tgz#50e927bb9f6a559ed21d284e7683ec5e2c784835"
|
||||
|
@ -7118,6 +7135,13 @@ file-uri-to-path@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
filelist@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
|
||||
integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==
|
||||
dependencies:
|
||||
minimatch "^3.0.4"
|
||||
|
||||
filesize@^3.1.3:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
|
||||
|
@ -9378,6 +9402,16 @@ istanbul-reports@^3.0.2:
|
|||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
jake@^10.6.1:
|
||||
version "10.8.2"
|
||||
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
|
||||
integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==
|
||||
dependencies:
|
||||
async "0.9.x"
|
||||
chalk "^2.4.2"
|
||||
filelist "^1.0.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
jasmine-ajax@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jasmine-ajax/-/jasmine-ajax-4.0.0.tgz#7d8ba7e47e3f7e780e155fe9aa563faafa7e1a26"
|
||||
|
|
Loading…
Reference in New Issue