Skip to content

Commit c97b930

Browse files
devversionjelbourn
authored andcommitted
build: generate changelog as part of staging script (#14250)
* Converts the changelog script into plain TypeScript. This makes it easier to integrate it into the _interactive_ staging script * Fixes that the "Is this is a pre-release" prompt is not returning a boolean. * No longer runs the staging script with Bazel because `ts-node` is more convenient and also we can be more context-aware (e.g. paths)
1 parent c4bc6dd commit c97b930

File tree

9 files changed

+205
-277
lines changed

9 files changed

+205
-277
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"api": "gulp api-docs",
2424
"breaking-changes": "gulp breaking-changes",
2525
"gulp": "gulp",
26-
"stage-release": "bash ./tools/release/stage-release-bin.sh",
26+
"stage-release": "ts-node --project tools/release/ tools/release/stage-release.ts",
2727
"preinstall": "node ./tools/npm/check-npm.js"
2828
},
2929
"version": "7.1.0",
@@ -78,20 +78,19 @@
7878
"axe-webdriverjs": "^1.1.1",
7979
"chalk": "^1.1.3",
8080
"codelyzer": "^4.5.0",
81+
"conventional-changelog": "^3.0.5",
8182
"dgeni": "^0.4.10",
8283
"dgeni-packages": "^0.26.12",
8384
"firebase": "^5.5.2",
8485
"firebase-admin": "^5.0.0",
8586
"firebase-tools": "^4.1.0",
8687
"fs-extra": "^3.0.1",
87-
"git-semver-tags": "^2.0.0",
8888
"glob": "^7.1.2",
8989
"gulp": "^3.9.1",
9090
"gulp-clean": "^0.3.2",
9191
"gulp-clean-css": "^3.3.1",
9292
"gulp-cli": "^2.0.1",
9393
"gulp-connect": "^5.0.0",
94-
"gulp-conventional-changelog": "^1.1.3",
9594
"gulp-dom": "^0.9.17",
9695
"gulp-flatten": "^0.3.1",
9796
"gulp-highlight-files": "^0.0.5",
@@ -117,6 +116,7 @@
117116
"karma-sourcemap-loader": "^0.3.7",
118117
"magic-string": "^0.22.4",
119118
"marked": "^0.5.1",
119+
"merge2": "^1.2.3",
120120
"minimatch": "^3.0.4",
121121
"minimist": "^1.2.0",
122122
"mock-fs": "^4.7.0",

tools/gulp/gulpfile.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ createPackageBuildTasks(momentAdapterPackage);
1717

1818
import './tasks/aot';
1919
import './tasks/breaking-changes';
20-
import './tasks/changelog';
2120
import './tasks/ci';
2221
import './tasks/clean';
2322
import './tasks/default';

tools/gulp/tasks/changelog.ts

Lines changed: 0 additions & 99 deletions
This file was deleted.

tools/release/BUILD.bazel

Lines changed: 0 additions & 25 deletions
This file was deleted.

tools/release/changelog.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {green, grey} from 'chalk';
2+
import {createReadStream, createWriteStream, readFileSync} from 'fs';
3+
import {prompt} from 'inquirer';
4+
import {join} from 'path';
5+
import {Readable} from 'stream';
6+
7+
// These imports lack type definitions.
8+
const conventionalChangelog = require('conventional-changelog');
9+
const merge2 = require('merge2');
10+
11+
/** Prompts for a changelog release name and prepends the new changelog. */
12+
export async function promptAndGenerateChangelog(changelogPath: string) {
13+
const releaseName = await promptChangelogReleaseName();
14+
await prependChangelogFromLatestTag(changelogPath, releaseName);
15+
}
16+
17+
/**
18+
* Writes the changelog from the latest Semver tag to the current HEAD.
19+
* @param changelogPath Path to the changelog file.
20+
* @param releaseName Name of the release that should show up in the changelog.
21+
*/
22+
export async function prependChangelogFromLatestTag(changelogPath: string, releaseName: string) {
23+
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 */ createDedupeWriterOptions(changelogPath));
29+
30+
// Stream for reading the existing changelog. This is necessary because we want to
31+
// actually prepend the new changelog to the existing one.
32+
const previousChangelogStream = createReadStream(changelogPath);
33+
34+
return new Promise((resolve, reject) => {
35+
// Sequentially merge the changelog output and the previous changelog stream, so that
36+
// the new changelog section comes before the existing versions. Afterwards, pipe into the
37+
// changelog file, so that the changes are reflected on file system.
38+
const mergedCompleteChangelog = merge2(outputStream, previousChangelogStream);
39+
40+
// Wait for the previous changelog to be completely read because otherwise we would
41+
// read and write from the same source which causes the content to be thrown off.
42+
previousChangelogStream.on('end', () => {
43+
mergedCompleteChangelog.pipe(createWriteStream(changelogPath))
44+
.once('error', (error: any) => reject(error))
45+
.once('finish', () => resolve());
46+
});
47+
48+
});
49+
}
50+
51+
/** Prompts the terminal for a changelog release name. */
52+
export async function promptChangelogReleaseName(): Promise<string> {
53+
return (await prompt<{releaseName: string}>({
54+
type: 'text',
55+
name: 'releaseName',
56+
message: 'What should be the name of the release?'
57+
})).releaseName;
58+
}
59+
60+
/**
61+
* Creates changelog writer options which ensure that commits are not showing up multiple times.
62+
* Commits can show up multiple times if a changelog has been generated on a publish branch
63+
* and has been cherry-picked into "master". In that case, the changelog will already contain
64+
* commits from master which might be added to the changelog again. This is because usually
65+
* patch and minor releases are tagged from the publish branches and therefore
66+
* conventional-changelog tries to build the changelog from last major version to master's HEAD.
67+
*/
68+
function createDedupeWriterOptions(changelogPath: string) {
69+
const existingChangelogContent = readFileSync(changelogPath, 'utf8');
70+
71+
return {
72+
// Specify a writer option that can be used to modify the content of a new changelog section.
73+
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
74+
finalizeContext: (context: any) => {
75+
context.commitGroups.forEach((group: any) => {
76+
group.commits = group.commits.filter((commit: any) => {
77+
// NOTE: We cannot compare the SHA's because the commits will have a different SHA
78+
// if they are being cherry-picked into a different branch.
79+
if (existingChangelogContent.includes(commit.header)) {
80+
console.log(grey(`Excluding: "${commit.header}" (${commit.hash})`));
81+
return false;
82+
}
83+
return true;
84+
});
85+
});
86+
return context;
87+
}
88+
};
89+
}
90+
91+
/** Entry-point for generating the changelog when called through the CLI. */
92+
if (require.main === module) {
93+
promptAndGenerateChangelog(join(__dirname, '../../CHANGELOG.md')).then(() => {
94+
console.log(green(' ✓ Successfully updated the changelog.'));
95+
});
96+
}
97+
98+

tools/release/prompt/new-version-prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export async function promptForNewVersion(currentVersion: Version): Promise<Vers
4545
message: `What's the type of the new release?`,
4646
choices: versionChoices,
4747
}, {
48-
type: 'prompt',
48+
type: 'confirm',
4949
name: 'isPrerelease',
5050
message: 'Should this be a pre-release?',
5151
// Prompt whether this should a pre-release if the current release is not a pre-release

tools/release/stage-release-bin.sh

Lines changed: 0 additions & 10 deletions
This file was deleted.

tools/release/stage-release.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import {bold, cyan, green, italic, red, yellow} from 'chalk';
22
import {existsSync, readFileSync, writeFileSync} from 'fs';
33
import {prompt} from 'inquirer';
44
import {join} from 'path';
5+
import {promptAndGenerateChangelog} from './changelog';
56
import {GitClient} from './git/git-client';
67
import {promptForNewVersion} from './prompt/new-version-prompt';
78
import {parseVersionName, Version} from './version-name/parse-version';
89
import {getExpectedPublishBranch} from './version-name/publish-branch';
910

11+
/** Default filename for the changelog. */
12+
const CHANGELOG_FILE_NAME = 'CHANGELOG.md';
13+
1014
/**
1115
* Class that can be instantiated in order to stage a new release. The tasks requires user
1216
* interaction/input through command line prompts.
@@ -92,13 +96,15 @@ class StageReleaseTask {
9296

9397
console.log(green(` ✓ Updated the version to "${bold(newVersionName)}" inside of the ` +
9498
`${italic('package.json')}`));
99+
console.log();
95100

96-
// TODO(devversion): run changelog script w/prompts in the future.
97-
// For now, we just let users make modifications and stage the changes.
101+
await promptAndGenerateChangelog(join(this.projectDir, CHANGELOG_FILE_NAME));
98102

99-
console.log(yellow(` ⚠ Please generate the ${bold('CHANGELOG')} for the new version. ` +
100-
`You can also make other unrelated modifications. After the changes have been made, ` +
101-
`just continue here.`));
103+
console.log();
104+
console.log(green(` ✓ Updated the changelog in ` +
105+
`"${bold(CHANGELOG_FILE_NAME)}"`));
106+
console.log(yellow(` ⚠ You can also make other unrelated modifications. After the ` +
107+
`changes have been made, just continue here.`));
102108
console.log();
103109

104110
const {shouldContinue} = await prompt<{shouldContinue: boolean}>({
@@ -166,19 +172,7 @@ class StageReleaseTask {
166172
}
167173

168174
/** Entry-point for the release staging script. */
169-
async function main() {
170-
const projectDir = process.argv.slice(2)[0];
171-
172-
if (!projectDir) {
173-
console.error(red(`You specified no project directory. Cannot run stage release script.`));
174-
console.error(red(`Usage: bazel run //tools/release:stage-release <project-directory>`));
175-
process.exit(1);
176-
}
177-
178-
return new StageReleaseTask(projectDir).run();
179-
}
180-
181175
if (require.main === module) {
182-
main();
176+
new StageReleaseTask(join(__dirname, '../../')).run();
183177
}
184178

0 commit comments

Comments
 (0)