Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.