Skip to content

Commit

Permalink
fix(ng-dev/release): Use new changelog writer each time an entry is p…
Browse files Browse the repository at this point in the history
…repending to the changelog file (#224)

* fix(ng-dev/release): Use new changelog writer each time an entry is prepending to the changelog file

Creating a new changelog writer instance reach time the ReleaseNotes prepends to the changelog ensures
that even in cases where we "cherry-pick" the entry, old entries are not removed from the changelog file.

* fixup! fix(ng-dev/release): Use new changelog writer each time an entry is prepending to the changelog file
  • Loading branch information
josephperrott authored Sep 16, 2021
1 parent 57a4705 commit ad534e2
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 45 deletions.
52 changes: 33 additions & 19 deletions ng-dev/release/notes/changelog.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import {existsSync, readFileSync} from 'fs';
import {GitClient} from '../../utils/git/git-client';
import {SemVer} from 'semver';
import {dedent} from '../../utils/testing/dedent';
import {getMockGitClient} from '../publish/test/test-utils/git-client-mock';
import {Changelog, splitMarker} from './changelog';

describe('Changelog', () => {
let changelog: Changelog;
let gitClient: GitClient;

beforeEach(() => {
const gitClient = getMockGitClient(
gitClient = getMockGitClient(
{owner: 'angular', name: 'dev-infra-test', mainBranchName: 'main'},
/* useSandboxGitClient */ false,
);
changelog = new Changelog(gitClient);
spyOn(GitClient, 'get').and.returnValue(gitClient);
changelog = Changelog.getChangelogFilePaths();
});

it('throws an error if it cannot find the anchor containing the version for an entry', () => {
expect(() => changelog.prependEntryToChangelog('does not have version <a> tag')).toThrow();
expect(() => Changelog.prependEntryToChangelogFile('does not have version <a> tag')).toThrow();
});

it('throws an error if it cannot determine the version for an entry', () => {
expect(() => changelog.prependEntryToChangelog(createChangelogEntry('NotSemVer'))).toThrow();
expect(() =>
Changelog.prependEntryToChangelogFile(createChangelogEntry('NotSemVer')),
).toThrow();
});

it('concatenates the changelog entries into the changelog file with the split marker between', () => {
changelog.prependEntryToChangelog(createChangelogEntry('1.0.0'));
changelog.prependEntryToChangelog(createChangelogEntry('2.0.0'));
changelog.prependEntryToChangelog(createChangelogEntry('3.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('1.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('2.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('3.0.0'));

expect(readFileAsString(changelog.filePath)).toBe(
dedent`
Expand All @@ -42,7 +47,7 @@ describe('Changelog', () => {
`.trim(),
);

changelog.moveEntriesPriorToVersionToArchive(new SemVer('3.0.0'));
Changelog.moveEntriesPriorToVersionToArchive(new SemVer('3.0.0'));

expect(readFileAsString(changelog.archiveFilePath)).toBe(
dedent`
Expand All @@ -61,19 +66,19 @@ describe('Changelog', () => {
it('creates a new changelog file if one does not exist.', () => {
expect(existsSync(changelog.filePath)).toBe(false);

changelog.prependEntryToChangelog(createChangelogEntry('0.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('0.0.0'));
expect(existsSync(changelog.filePath)).toBe(true);
});

it('should not include a split marker when only one changelog entry is in the changelog.', () => {
changelog.prependEntryToChangelog(createChangelogEntry('0.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('0.0.0'));

expect(readFileAsString(changelog.filePath)).not.toContain(splitMarker);
});

it('separates multiple changelog entries using a standard split marker', () => {
for (let i = 0; i < 2; i++) {
changelog.prependEntryToChangelog(createChangelogEntry(`0.0.${i}`));
Changelog.prependEntryToChangelogFile(createChangelogEntry(`0.0.${i}`));
}

expect(readFileAsString(changelog.filePath)).toContain(splitMarker);
Expand All @@ -82,22 +87,31 @@ describe('Changelog', () => {

describe('adds entries to the changelog archive', () => {
it('only updates or creates the changelog archive if necessary', () => {
changelog.prependEntryToChangelog(createChangelogEntry('1.0.0'));
Changelog.prependEntryToChangelogFile(createChangelogEntry('1.0.0'));
expect(existsSync(changelog.archiveFilePath)).toBe(false);

changelog.moveEntriesPriorToVersionToArchive(new SemVer('1.0.0'));
Changelog.moveEntriesPriorToVersionToArchive(new SemVer('1.0.0'));
expect(existsSync(changelog.archiveFilePath)).toBe(false);

changelog.moveEntriesPriorToVersionToArchive(new SemVer('2.0.0'));
Changelog.moveEntriesPriorToVersionToArchive(new SemVer('2.0.0'));
expect(existsSync(changelog.archiveFilePath)).toBe(true);
});

it('from the primary changelog older than a provided version', () => {
changelog.prependEntryToChangelog(createChangelogEntry('1.0.0', 'This is version 1'));
changelog.prependEntryToChangelog(createChangelogEntry('2.0.0', 'This is version 2'));
changelog.prependEntryToChangelog(createChangelogEntry('3.0.0', 'This is version 3'));

changelog.moveEntriesPriorToVersionToArchive(new SemVer('3.0.0'));
Changelog.prependEntryToChangelogFile(
createChangelogEntry('1.0.0', 'This is version 1'),
gitClient,
);
Changelog.prependEntryToChangelogFile(
createChangelogEntry('2.0.0', 'This is version 2'),
gitClient,
);
Changelog.prependEntryToChangelogFile(
createChangelogEntry('3.0.0', 'This is version 3'),
gitClient,
);

Changelog.moveEntriesPriorToVersionToArchive(new SemVer('3.0.0'));
expect(readFileAsString(changelog.archiveFilePath)).toContain('version 1');
expect(readFileAsString(changelog.archiveFilePath)).toContain('version 2');
expect(readFileAsString(changelog.archiveFilePath)).not.toContain('version 3');
Expand Down
49 changes: 42 additions & 7 deletions ng-dev/release/notes/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import * as semver from 'semver';
import {GitClient} from '../../utils/git/git-client';

/** Project-relative path for the changelog file. */
export const changelogPath = 'CHANGELOG.md';
const changelogPath = 'CHANGELOG.md';

/** Project-relative path for the changelog archive file. */
export const changelogArchivePath = 'CHANGELOG_ARCHIVE.md';
const changelogArchivePath = 'CHANGELOG_ARCHIVE.md';

/** A marker used to split a CHANGELOG.md file into individual entries. */
export const splitMarker = '<!-- CHANGELOG SPLIT MARKER -->';
Expand Down Expand Up @@ -37,12 +37,47 @@ interface ChangelogEntry {
}

export class Changelog {
/** Prepend a changelog entry to the current changelog file. */
static prependEntryToChangelogFile(entry: string, git = GitClient.get()) {
const changelog = new this(git);
changelog.prependEntryToChangelogFile(entry);
}

/**
* Move all changelog entries from the CHANGELOG.md file for versions prior to the provided
* version to the changelog archive.
*
* Versions should be used to determine which entries are moved to archive as versions are the
* most accurate piece of context found within a changelog entry to determine its relationship to
* other changelog entries. This allows for example, moving all changelog entries out of the
* main changelog when a version moves out of support.
*/
static moveEntriesPriorToVersionToArchive(version: semver.SemVer, git = GitClient.get()) {
const changelog = new this(git);
changelog.moveEntriesPriorToVersionToArchive(version);
}

// TODO(josephperrott): Remove this after it is unused.
/** Retrieve the file paths for the changelog files. */
static getChangelogFilePaths(git = GitClient.get()) {
return new this(git);
}

/** The absolute path to the changelog file. */
readonly filePath = join(this.git.baseDir, changelogPath);
/** The absolute path to the changelog archive file. */
readonly archiveFilePath = join(this.git.baseDir, changelogArchivePath);
/** The changelog entries in the CHANGELOG.md file. */
private entries = this.getEntriesFor(this.filePath);
/**
* The changelog entries in the CHANGELOG.md file.
* Delays reading the CHANGELOG.md file until it is actually used.
*/
private get entries() {
if (this._entries === undefined) {
return (this._entries = this.getEntriesFor(this.filePath));
}
return this._entries;
}
private _entries: undefined | ChangelogEntry[] = undefined;
/**
* The changelog entries in the CHANGELOG_ARCHIVE.md file.
* Delays reading the CHANGELOG_ARCHIVE.md file until it is actually used.
Expand All @@ -55,10 +90,10 @@ export class Changelog {
}
private _archiveEntries: undefined | ChangelogEntry[] = undefined;

constructor(private git: GitClient) {}
private constructor(private git: GitClient) {}

/** Prepend a changelog entry to the changelog. */
prependEntryToChangelog(entry: string) {
private prependEntryToChangelogFile(entry: string) {
this.entries.unshift(parseChangelogEntry(entry));
this.writeToChangelogFile();
}
Expand All @@ -72,7 +107,7 @@ export class Changelog {
* other changelog entries. This allows for example, moving all changelog entries out of the
* main changelog when a version moves out of support.
*/
moveEntriesPriorToVersionToArchive(version: semver.SemVer) {
private moveEntriesPriorToVersionToArchive(version: semver.SemVer) {
[...this.entries].reverse().forEach((entry: ChangelogEntry) => {
if (semver.lt(entry.version, version)) {
this.archiveEntries.unshift(entry);
Expand Down
2 changes: 1 addition & 1 deletion ng-dev/release/notes/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function handler({releaseVersion, from, to, prependToChangelog, type}: Arg
const releaseNotes = await ReleaseNotes.forRange(releaseVersion, from, to);

if (prependToChangelog) {
await releaseNotes.prependEntryToChangelog();
await releaseNotes.prependEntryToChangelogFile();
info(`Added release notes for "${releaseVersion}" to the changelog`);
return;
}
Expand Down
8 changes: 3 additions & 5 deletions ng-dev/release/notes/release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export class ReleaseNotes {
return new ReleaseNotes(version, commits, git);
}

/** The changelog writer. */
private changelog = new Changelog(this.git);
/** The RenderContext to be used during rendering. */
private renderContext: RenderContext | undefined;
/** The title to use for the release. */
Expand Down Expand Up @@ -68,16 +66,16 @@ export class ReleaseNotes {
* Prepend generated release note to the CHANGELOG.md file in the base directory of the repository
* provided by the GitClient.
*/
async prependEntryToChangelog() {
this.changelog.prependEntryToChangelog(await this.getChangelogEntry());
async prependEntryToChangelogFile() {
Changelog.prependEntryToChangelogFile(await this.getChangelogEntry(), this.git);

// TODO(josephperrott): Remove file formatting calls.
// Upon reaching a standardized formatting for markdown files, rather than calling a formatter
// for all creation of changelogs, we instead will confirm in our testing that the new changes
// created for changelogs meet on standardized markdown formats via unit testing.
try {
assertValidFormatConfig(this.config);
await formatFiles([this.changelog.filePath]);
await formatFiles([Changelog.getChangelogFilePaths(this.git).filePath]);
} catch {
// If the formatting is either unavailable or fails, continue on with the unformatted result.
}
Expand Down
2 changes: 1 addition & 1 deletion ng-dev/release/publish/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export abstract class ReleaseAction {
* @returns A boolean indicating whether the release notes have been prepended.
*/
protected async prependReleaseNotesToChangelog(releaseNotes: ReleaseNotes): Promise<void> {
await releaseNotes.prependEntryToChangelog();
await releaseNotes.prependEntryToChangelogFile();
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
}

Expand Down
41 changes: 29 additions & 12 deletions tools/local-actions/changelog/main.js

Large diffs are not rendered by default.

0 comments on commit ad534e2

Please sign in to comment.