Skip to content

Commit e816910

Browse files
committed
build: group changelog entries by packages
Resolves COMP-225
1 parent c8ceae5 commit e816910

File tree

3 files changed

+156
-26
lines changed

3 files changed

+156
-26
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{{#if scope}} **{{scope}}:**
2+
{{~/if}} {{#if subject}}
3+
{{~subject}}
4+
{{~else}}
5+
{{~header}}
6+
{{~/if}}
7+
8+
{{~!-- commit link --}} {{#if @root.linkReferences~}}
9+
([{{hash}}](
10+
{{~#if @root.repository}}
11+
{{~#if @root.host}}
12+
{{~@root.host}}/
13+
{{~/if}}
14+
{{~#if @root.owner}}
15+
{{~@root.owner}}/
16+
{{~/if}}
17+
{{~@root.repository}}
18+
{{~else}}
19+
{{~@root.repoUrl}}
20+
{{~/if}}/
21+
{{~@root.commit}}/{{hash}}))
22+
{{~else}}
23+
{{~hash}}
24+
{{~/if}}
25+
26+
{{~!-- commit references --}}
27+
{{~#if references~}}
28+
, closes
29+
{{~#each references}} {{#if @root.linkReferences~}}
30+
[
31+
{{~#if this.owner}}
32+
{{~this.owner}}/
33+
{{~/if}}
34+
{{~this.repository}}#{{this.issue}}](
35+
{{~#if @root.repository}}
36+
{{~#if @root.host}}
37+
{{~@root.host}}/
38+
{{~/if}}
39+
{{~#if this.repository}}
40+
{{~#if this.owner}}
41+
{{~this.owner}}/
42+
{{~/if}}
43+
{{~this.repository}}
44+
{{~else}}
45+
{{~#if @root.owner}}
46+
{{~@root.owner}}/
47+
{{~/if}}
48+
{{~@root.repository}}
49+
{{~/if}}
50+
{{~else}}
51+
{{~@root.repoUrl}}
52+
{{~/if}}/
53+
{{~@root.issue}}/{{this.issue}})
54+
{{~else}}
55+
{{~#if this.owner}}
56+
{{~this.owner}}/
57+
{{~/if}}
58+
{{~this.repository}}#{{this.issue}}
59+
{{~/if}}{{/each}}
60+
{{~/if}}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{> header}}
2+
3+
{{#each packageGroups}}
4+
### {{title}}
5+
6+
| | |
7+
| ---------- | --------------------- |
8+
{{#each commits}}
9+
| <img src="{{typeImageUrl}}" alt="{{typeDescription}}"> | {{> commit root=@root}} |
10+
{{/each}}
11+
12+
{{/each}}
13+
14+
{{> footer}}
15+

tools/release/changelog.ts

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {createReadStream, createWriteStream, readFileSync} from 'fs';
33
import {prompt} from 'inquirer';
44
import {join} from 'path';
55
import {Readable} from 'stream';
6+
import {releasePackages} from './release-output/release-packages';
67

78
// These imports lack type definitions.
89
const conventionalChangelog = require('conventional-changelog');
10+
const changelogCompare = require('conventional-changelog-writer/lib/util');
911
const merge2 = require('merge2');
1012

1113
/** Prompts for a changelog release name and prepends the new changelog. */
@@ -21,11 +23,16 @@ export async function promptAndGenerateChangelog(changelogPath: string) {
2123
*/
2224
export async function prependChangelogFromLatestTag(changelogPath: string, releaseName: string) {
2325
const outputStream: Readable = conventionalChangelog(
24-
/* core options */ {preset: 'angular'},
25-
/* context options */ {title: releaseName},
26-
/* raw-commits options */ null,
27-
/* commit parser options */ null,
28-
/* writer options */ createChangelogWriterOptions(changelogPath));
26+
/* core options */ {preset: 'angular'},
27+
/* context options */ {title: releaseName},
28+
/* raw-commits options */ null,
29+
/* commit parser options */ {
30+
// Expansion of the convention-changelog-angular preset to extract the package
31+
// name from the commit message.
32+
headerPattern: /^(\w*)(?:\((?:([^/]+)\/)?(.*)\))?: (.*)$/,
33+
headerCorrespondence: ['type', 'package', 'scope', 'subject'],
34+
},
35+
/* writer options */ createChangelogWriterOptions(changelogPath));
2936

3037
// Stream for reading the existing changelog. This is necessary because we want to
3138
// actually prepend the new changelog to the existing one.
@@ -41,19 +48,20 @@ export async function prependChangelogFromLatestTag(changelogPath: string, relea
4148
// read and write from the same source which causes the content to be thrown off.
4249
previousChangelogStream.on('end', () => {
4350
mergedCompleteChangelog.pipe(createWriteStream(changelogPath))
44-
.once('error', (error: any) => reject(error))
45-
.once('finish', () => resolve());
51+
.once('error', (error: any) => reject(error))
52+
.once('finish', () => resolve());
4653
});
4754
});
4855
}
4956

5057
/** Prompts the terminal for a changelog release name. */
5158
export async function promptChangelogReleaseName(): Promise<string> {
5259
return (await prompt<{releaseName: string}>({
53-
type: 'text',
54-
name: 'releaseName',
55-
message: 'What should be the name of the release?'
56-
})).releaseName;
60+
type: 'text',
61+
name: 'releaseName',
62+
message: 'What should be the name of the release?'
63+
}))
64+
.releaseName;
5765
}
5866

5967
/**
@@ -68,45 +76,92 @@ export async function promptChangelogReleaseName(): Promise<string> {
6876
*/
6977
function createChangelogWriterOptions(changelogPath: string) {
7078
const existingChangelogContent = readFileSync(changelogPath, 'utf8');
79+
const commitSortFunction = changelogCompare.functionify(['type', 'scope', 'subject']);
7180

7281
return {
82+
// Overwrite the changelog templates so that we can render the commits grouped
83+
// by package names.
84+
mainTemplate: readFileSync(join(__dirname, 'changelog-root-template.hbs'), 'utf8'),
85+
commitPartial: readFileSync(join(__dirname, 'changelog-commit-template.hbs'), 'utf8'),
86+
7387
// Specify a writer option that can be used to modify the content of a new changelog section.
7488
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
7589
finalizeContext: (context: any) => {
76-
context.commitGroups = context.commitGroups.filter((group: any) => {
77-
group.commits = group.commits.filter((commit: any) => {
78-
79-
// Commits that change things for "cdk-experimental" or "material-experimental" will also
80-
// show up in the changelog by default. We don't want to show these in the changelog.
81-
if (commit.scope && commit.scope.includes('experimental')) {
82-
console.log(yellow(` ↺ Skipping experimental: "${bold(commit.header)}"`));
83-
return false;
84-
}
90+
const packageGroups: {[packageName: string]: any[]} = {};
8591

92+
context.commitGroups.forEach((group: any) => {
93+
group.commits.forEach((commit: any) => {
8694
// Filter out duplicate commits. Note that we cannot compare the SHA because the commits
8795
// will have a different SHA if they are being cherry-picked into a different branch.
8896
if (existingChangelogContent.includes(commit.subject)) {
8997
console.log(yellow(` ↺ Skipping duplicate: "${bold(commit.header)}"`));
9098
return false;
9199
}
92-
return true;
100+
101+
// Commits which just specify a scope that refers to a package but do not follow
102+
// the commit format that is parsed by the conventional-changelog-parser, can be
103+
// still resolved to their package from the scope. This handles the case where
104+
// a commit targets the whole package and does not specify a specific scope.
105+
// e.g. "refactor(material-experimental): support strictness flags".
106+
if (!commit.package && commit.scope) {
107+
const matchingPackage = releasePackages.find(pkgName => pkgName === commit.scope);
108+
if (matchingPackage) {
109+
commit.scope = null;
110+
commit.package = matchingPackage;
111+
}
112+
}
113+
114+
// TODO(devversion): once we formalize the commit message format and
115+
// require specifying the "material" package explicitly, we can remove
116+
// the fallback to the "material" package.
117+
const packageName = commit.package || 'material';
118+
const {color, title} = getTitleAndColorOfTypeLabel(commit.type);
119+
120+
if (!packageGroups[packageName]) {
121+
packageGroups[packageName] = [];
122+
}
123+
124+
packageGroups[packageName].push({
125+
typeDescription: title,
126+
typeImageUrl: `https://img.shields.io/badge/-${title}-${color}`,
127+
...commit
128+
});
93129
});
130+
});
94131

95-
// Filter out commit groups which don't have any commits. Commit groups will become
96-
// empty if we filter out all duplicated commits.
97-
return group.commits.length;
132+
context.packageGroups = Object.keys(packageGroups).sort().map(pkgName => {
133+
return {
134+
title: pkgName,
135+
commits: packageGroups[pkgName].sort(commitSortFunction),
136+
};
98137
});
99138

100139
return context;
101140
}
102141
};
103142
}
104143

144+
/** Gets the title and color from a commit type label. */
145+
function getTitleAndColorOfTypeLabel(typeLabel: string): {title: string, color: string} {
146+
if (typeLabel === `Features`) {
147+
return {title: 'feature', color: 'green'};
148+
} else if (typeLabel === `Bug Fixes`) {
149+
return {title: 'bug fix', color: 'orange'};
150+
} else if (typeLabel === `Performance Improvements`) {
151+
return {title: 'performance', color: 'blue'};
152+
} else if (typeLabel === `Reverts`) {
153+
return {title: 'revert', color: 'grey'};
154+
} else if (typeLabel === `Documentation`) {
155+
return {title: 'docs', color: 'darkgreen'};
156+
} else if (typeLabel === `refactor`) {
157+
return {title: 'refactor', color: 'lightgrey'};
158+
}
159+
return {title: typeLabel, color: 'yellow'};
160+
}
161+
105162
/** Entry-point for generating the changelog when called through the CLI. */
106163
if (require.main === module) {
107164
promptAndGenerateChangelog(join(__dirname, '../../CHANGELOG.md')).then(() => {
108165
console.log(green(' ✓ Successfully updated the changelog.'));
109166
});
110167
}
111-
112-

0 commit comments

Comments
 (0)