豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 8747538

Browse files
josephperrottdevversion
authored andcommitted
feat(ng-dev/release): support prepending new release note entries to the changelog (#204)
Support prepending the release note entries to the changelog.md file. Additionally, we try to run the formatter on the changelog file to ensure that if formatting is required for the file it is completed. Additionally, updating the `ng-dev release notes` command to leverage the newly created `prependEntryToChangelog` method. PR Close #204
1 parent 75f95e8 commit 8747538

File tree

8 files changed

+2918
-80
lines changed

8 files changed

+2918
-80
lines changed

ng-dev/release/notes/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ts_library(
1111
],
1212
deps = [
1313
"//ng-dev/commit-message",
14+
"//ng-dev/format",
1415
"//ng-dev/release/config",
1516
"//ng-dev/release/versioning",
1617
"//ng-dev/utils",

ng-dev/release/notes/cli.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
9-
import {writeFileSync} from 'fs';
10-
import {join} from 'path';
118
import {SemVer} from 'semver';
129
import {Arguments, Argv, CommandModule} from 'yargs';
1310

@@ -16,16 +13,16 @@ import {info} from '../../utils/console';
1613
import {ReleaseNotes} from './release-notes';
1714

1815
/** Command line options for building a release. */
19-
export interface ReleaseNotesOptions {
16+
export interface Options {
2017
from: string;
2118
to: string;
22-
outFile?: string;
19+
updateChangelog: boolean;
2320
releaseVersion: SemVer;
2421
type: 'github-release' | 'changelog';
2522
}
2623

2724
/** Yargs command builder for configuring the `ng-dev release build` command. */
28-
function builder(argv: Argv): Argv<ReleaseNotesOptions> {
25+
function builder(argv: Argv): Argv<Options> {
2926
return argv
3027
.option('releaseVersion', {
3128
type: 'string',
@@ -48,33 +45,35 @@ function builder(argv: Argv): Argv<ReleaseNotesOptions> {
4845
choices: ['github-release', 'changelog'] as const,
4946
default: 'changelog' as const,
5047
})
51-
.option('outFile', {
52-
type: 'string',
53-
description: 'File location to write the generated release notes to',
54-
coerce: (filePath?: string) => (filePath ? join(process.cwd(), filePath) : undefined),
48+
.option('updateChangelog', {
49+
type: 'boolean',
50+
default: false,
51+
description: 'Whether to update the changelog with the newly created entry',
5552
});
5653
}
5754

5855
/** Yargs command handler for generating release notes. */
59-
async function handler({releaseVersion, from, to, outFile, type}: Arguments<ReleaseNotesOptions>) {
56+
async function handler({releaseVersion, from, to, updateChangelog, type}: Arguments<Options>) {
6057
/** The ReleaseNotes instance to generate release notes. */
6158
const releaseNotes = await ReleaseNotes.forRange(releaseVersion, from, to);
6259

60+
if (updateChangelog) {
61+
await releaseNotes.prependEntryToChangelog();
62+
info(`Added release notes for "${releaseVersion}" to the changelog`);
63+
return;
64+
}
65+
6366
/** The requested release notes entry. */
64-
const releaseNotesEntry = await (type === 'changelog'
65-
? releaseNotes.getChangelogEntry()
66-
: releaseNotes.getGithubReleaseEntry());
67+
const releaseNotesEntry =
68+
type === 'changelog'
69+
? await releaseNotes.getChangelogEntry()
70+
: await releaseNotes.getGithubReleaseEntry();
6771

68-
if (outFile) {
69-
writeFileSync(outFile, releaseNotesEntry);
70-
info(`Generated release notes for "${releaseVersion}" written to ${outFile}`);
71-
} else {
72-
process.stdout.write(releaseNotesEntry);
73-
}
72+
process.stdout.write(releaseNotesEntry);
7473
}
7574

7675
/** CLI command module for generating release notes. */
77-
export const ReleaseNotesCommandModule: CommandModule<{}, ReleaseNotesOptions> = {
76+
export const ReleaseNotesCommandModule: CommandModule<{}, Options> = {
7877
builder,
7978
handler,
8079
command: 'notes',

ng-dev/release/notes/release-notes.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,43 @@ import * as semver from 'semver';
1010
import {CommitFromGitLog} from '../../commit-message/parse';
1111

1212
import {promptInput} from '../../utils/console';
13+
import {formatFiles} from '../../format/format';
1314
import {GitClient} from '../../utils/git/git-client';
14-
import {assertValidReleaseConfig, ReleaseNotesConfig} from '../config/index';
15+
import {assertValidReleaseConfig, ReleaseConfig, ReleaseNotesConfig} from '../config/index';
1516
import {RenderContext} from './context';
1617

1718
import changelogTemplate from './templates/changelog';
1819
import githubReleaseTemplate from './templates/github-release';
1920
import {getCommitsForRangeWithDeduping} from './commits/get-commits-in-range';
2021
import {getConfig} from '../../utils/config';
22+
import {existsSync, readFileSync, writeFileSync} from 'fs';
23+
import {join} from 'path';
24+
import {assertValidFormatConfig} from '../../format/config';
2125

2226
/** Release note generation. */
2327
export class ReleaseNotes {
2428
static async forRange(version: semver.SemVer, baseRef: string, headRef: string) {
25-
const client = GitClient.get();
26-
const commits = getCommitsForRangeWithDeduping(client, baseRef, headRef);
27-
return new ReleaseNotes(version, commits);
29+
const git = GitClient.get();
30+
const commits = getCommitsForRangeWithDeduping(git, baseRef, headRef);
31+
return new ReleaseNotes(version, commits, git);
2832
}
2933

30-
/** An instance of GitClient. */
31-
private git = GitClient.get();
3234
/** The RenderContext to be used during rendering. */
3335
private renderContext: RenderContext | undefined;
3436
/** The title to use for the release. */
3537
private title: string | false | undefined;
36-
/** The configuration for release notes. */
37-
private config: ReleaseNotesConfig = this.getReleaseConfig().releaseNotes ?? {};
38+
/** The configuration ng-dev. */
39+
private config: {release: ReleaseConfig} = getConfig([assertValidReleaseConfig]);
40+
/** The configuration for the release notes. */
41+
private get notesConfig() {
42+
return this.config.release.releaseNotes || {};
43+
}
3844

39-
protected constructor(public version: semver.SemVer, private commits: CommitFromGitLog[]) {}
45+
protected constructor(
46+
public version: semver.SemVer,
47+
private commits: CommitFromGitLog[],
48+
private git: GitClient,
49+
) {}
4050

4151
/** Retrieve the release note generated for a Github Release. */
4252
async getGithubReleaseEntry(): Promise<string> {
@@ -50,6 +60,28 @@ export class ReleaseNotes {
5060
return render(changelogTemplate, await this.generateRenderContext(), {rmWhitespace: true});
5161
}
5262

63+
/** Prepends the generated release note to the CHANGELOG file. */
64+
async prependEntryToChangelog() {
65+
/** The fully path to the changelog file. */
66+
const filePath = join(this.git.baseDir, 'CHANGELOG.md');
67+
/** The changelog contents in the current changelog. */
68+
let changelog = '';
69+
if (existsSync(filePath)) {
70+
changelog = readFileSync(filePath, {encoding: 'utf8'});
71+
}
72+
/** The new changelog entry to add to the changelog. */
73+
const entry = await this.getChangelogEntry();
74+
75+
writeFileSync(filePath, `${entry}\n\n${changelog}`);
76+
77+
try {
78+
assertValidFormatConfig(this.config);
79+
await formatFiles([filePath]);
80+
} catch {
81+
// If the formatting is either unavailable or fails, continue on with the unformatted result.
82+
}
83+
}
84+
5385
/** Retrieve the number of commits included in the release notes after filtering and deduping. */
5486
async getCommitCountInReleaseNotes() {
5587
const context = await this.generateRenderContext();
@@ -70,7 +102,7 @@ export class ReleaseNotes {
70102
*/
71103
async promptForReleaseTitle() {
72104
if (this.title === undefined) {
73-
if (this.config.useReleaseTitle) {
105+
if (this.notesConfig.useReleaseTitle) {
74106
this.title = await promptInput('Please provide a title for the release:');
75107
} else {
76108
this.title = false;
@@ -86,20 +118,12 @@ export class ReleaseNotes {
86118
commits: this.commits,
87119
github: this.git.remoteConfig,
88120
version: this.version.format(),
89-
groupOrder: this.config.groupOrder,
90-
hiddenScopes: this.config.hiddenScopes,
91-
categorizeCommit: this.config.categorizeCommit,
121+
groupOrder: this.notesConfig.groupOrder,
122+
hiddenScopes: this.notesConfig.hiddenScopes,
123+
categorizeCommit: this.notesConfig.categorizeCommit,
92124
title: await this.promptForReleaseTitle(),
93125
});
94126
}
95127
return this.renderContext;
96128
}
97-
98-
// This method is used for access to the utility functions while allowing them
99-
// to be overwritten in subclasses during testing.
100-
protected getReleaseConfig() {
101-
const config = getConfig();
102-
assertValidReleaseConfig(config);
103-
return config.release;
104-
}
105129
}

ng-dev/release/publish/test/common.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import {getMockGitClient} from './test-utils/git-client-mock';
3535
import {CommitFromGitLog, parseCommitFromGitLog} from '../../../commit-message/parse';
3636
import {SandboxGitRepo} from './test-utils/sandbox-testing';
37+
import { GitClient } from '../../../utils/git/git-client';
3738

3839
describe('common release action logic', () => {
3940
const baseReleaseTrains: ActiveReleaseTrains = {
@@ -142,7 +143,7 @@ describe('common release action logic', () => {
142143
});
143144

144145
it('should link to the changelog in the release entry if notes are too large', async () => {
145-
const {repo, instance} = setupReleaseActionForTesting(TestAction, baseReleaseTrains);
146+
const {repo, instance, gitClient} = setupReleaseActionForTesting(TestAction, baseReleaseTrains);
146147
const {version, branchName} = baseReleaseTrains.latest;
147148
const tagName = version.format();
148149
const testCommit = parseCommitFromGitLog(Buffer.from('fix(test): test'));
@@ -157,7 +158,7 @@ describe('common release action logic', () => {
157158
testCommit.subject = exceedingText;
158159

159160
spyOn(ReleaseNotes, 'forRange').and.callFake(
160-
async () => new MockReleaseNotes(version, [testCommit]),
161+
async () => new MockReleaseNotes(version, [testCommit], gitClient)
161162
);
162163

163164
repo
@@ -246,8 +247,8 @@ describe('common release action logic', () => {
246247

247248
/** Mock class for `ReleaseNotes` which accepts a list of in-memory commit objects. */
248249
class MockReleaseNotes extends ReleaseNotes {
249-
constructor(version: SemVer, commits: CommitFromGitLog[]) {
250-
super(version, commits);
250+
constructor(version: SemVer, commits: CommitFromGitLog[], git: GitClient) {
251+
super(version, commits, git);
251252
}
252253
}
253254

ng-dev/release/publish/test/release-notes/generation.spec.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
*/
88

99
import {installSandboxGitClient, SandboxGitClient} from '../test-utils/sandbox-git-client';
10-
import {mkdirSync, rmdirSync} from 'fs';
10+
import {readFileSync, writeFileSync} from 'fs';
1111
import {prepareTempDirectory, testTmpDir} from '../test-utils/action-mocks';
1212
import {getMockGitClient} from '../test-utils/git-client-mock';
13-
import {GithubConfig} from '../../../../utils/config';
13+
import {GithubConfig, setConfig} from '../../../../utils/config';
1414
import {SandboxGitRepo} from '../test-utils/sandbox-testing';
1515
import {ReleaseNotes} from '../../../notes/release-notes';
1616
import {ReleaseConfig} from '../../../config';
1717
import {changelogPattern, parse} from '../test-utils/test-utils';
18+
import { buildDateStamp } from '../../../notes/context';
19+
import { dedent } from '../../../../utils/testing/dedent';
1820

1921
describe('release notes generation', () => {
2022
let releaseConfig: ReleaseConfig;
@@ -28,12 +30,10 @@ describe('release notes generation', () => {
2830

2931
releaseConfig = {npmPackages: [], buildPackages: async () => []};
3032
githubConfig = {owner: 'angular', name: 'dev-infra-test', mainBranchName: 'main'};
33+
setConfig({github: githubConfig, release: releaseConfig});
3134
client = getMockGitClient(githubConfig, /* useSandboxGitClient */ true);
3235

3336
installSandboxGitClient(client);
34-
35-
// Ensure the `ReleaseNotes` class picks up the fake release config for testing.
36-
spyOn(ReleaseNotes.prototype as any, 'getReleaseConfig').and.callFake(() => releaseConfig);
3737
});
3838

3939
describe('changelog', () => {
@@ -436,4 +436,36 @@ describe('release notes generation', () => {
436436

437437
expect(await releaseNotes.getCommitCountInReleaseNotes()).toBe(4);
438438
});
439+
440+
describe('updates the changelog file', () => {
441+
it('prepending the entry', async () => {
442+
writeFileSync(`${testTmpDir}/CHANGELOG.md`, '<Previous Changelog Entries>');
443+
444+
const sandboxRepo = SandboxGitRepo.withInitialCommit(githubConfig)
445+
.createTagForHead('startTag')
446+
.commit('fix(ng-dev): commit *1', 1);
447+
448+
const fullSha = sandboxRepo.getShaForCommitId(1, 'long');
449+
const shortSha = sandboxRepo.getShaForCommitId(1, 'short');
450+
451+
const releaseNotes = await ReleaseNotes.forRange(parse('13.0.0'), 'startTag', 'HEAD');
452+
await releaseNotes.prependEntryToChangelog();
453+
454+
const changelog = readFileSync(`${testTmpDir}/CHANGELOG.md`, 'utf8');
455+
456+
expect(changelog).toBe(dedent`
457+
<a name="13.0.0"></a>
458+
# 13.0.0 (${buildDateStamp()})
459+
### ng-dev
460+
| Commit | Type | Description |
461+
| -- | -- | -- |
462+
| [${shortSha}](https://github.com/angular/dev-infra-test/commit/${fullSha}) | fix | commit *1 |
463+
## Special Thanks
464+
Angular Robot
465+
466+
467+
<Previous Changelog Entries>`.trim()
468+
);
469+
});
470+
});
439471
});

ng-dev/release/publish/test/test-utils/action-mocks.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ import * as externalCommands from '../../external-commands';
1515
import * as console from '../../../../utils/console';
1616

1717
import {ReleaseAction} from '../../actions';
18-
import {GithubConfig} from '../../../../utils/config';
18+
import {GithubConfig, setConfig} from '../../../../utils/config';
1919
import {ReleaseConfig} from '../../../config';
2020
import {installVirtualGitClientSpies, VirtualGitClient} from '../../../../utils/testing';
2121
import {installSandboxGitClient} from './sandbox-git-client';
22-
import {ReleaseNotes} from '../../../notes/release-notes';
2322
import {getMockGitClient} from './git-client-mock';
2423

2524
/**
@@ -69,6 +68,9 @@ export function setupMocksForReleaseAction<T extends boolean>(
6968
// to persist between tests if the sandbox git client is used.
7069
prepareTempDirectory();
7170

71+
// Set the configuration to be used throughout the spec.
72+
setConfig({github: githubConfig, release: releaseConfig});
73+
7274
// Fake confirm any prompts. We do not want to make any changelog edits and
7375
// just proceed with the release action.
7476
spyOn(console, 'promptConfirm').and.resolveTo(true);
@@ -81,8 +83,6 @@ export function setupMocksForReleaseAction<T extends boolean>(
8183
testReleasePackages.map((name) => ({name, outputPath: `${testTmpDir}/dist/${name}`})),
8284
);
8385

84-
spyOn(ReleaseNotes.prototype as any, 'getReleaseConfig').and.returnValue(releaseConfig);
85-
8686
// Fake checking the package versions since we don't actually create NPM
8787
// package output that can be tested.
8888
spyOn(ReleaseAction.prototype, '_verifyPackageVersions' as any).and.resolveTo();

ng-dev/release/publish/test/test-utils/sandbox-testing.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,23 @@ export class SandboxGitRepo {
7878
}
7979

8080
/** Cherry-picks a commit into the current branch. */
81-
cherryPick(commitId: number) {
81+
cherryPick(commitId: number): this {
82+
runGitInTmpDir(['cherry-pick', '--allow-empty', this.getShaForCommitId(commitId)]);
83+
return this;
84+
}
85+
86+
/** Retrieve the sha for the commit. */
87+
getShaForCommitId(commitId: number, type: 'long'|'short' = 'long'): string {
8288
const commitSha = this._commitShaById.get(commitId);
8389

8490
if (commitSha === undefined) {
85-
throw Error('Unable to cherry-pick. Unknown commit id.');
91+
throw Error('Unable to get determine SHA due to an unknown commit id.');
8692
}
8793

88-
runGitInTmpDir(['cherry-pick', '--allow-empty', commitSha]);
89-
return this;
94+
if (type === 'short') {
95+
return runGitInTmpDir(['rev-parse', '--short', commitSha]);
96+
}
97+
98+
return commitSha;
9099
}
91100
}

0 commit comments

Comments
 (0)