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

Run formatter for CHANGELOG.md when generated during release #204

Closed
16 changes: 13 additions & 3 deletions github-actions/slash-commands/main.js

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions ng-dev/caretaker/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {info} from 'console';
import {Arguments, Argv} from 'yargs';
import {getConfig} from '../utils/config';
import {assertValidGithubConfig, getConfig} from '../utils/config';
import {CheckModule} from './check/cli';
import {assertValidCaretakerConfig} from './config';
import {HandoffModule} from './handoff/cli';
Expand All @@ -22,9 +22,8 @@ export function buildCaretakerParser(yargs: Argv) {
}

function caretakerCommandCanRun(argv: Arguments) {
const config = getConfig();
try {
assertValidCaretakerConfig(config);
getConfig([assertValidCaretakerConfig, assertValidGithubConfig]);
} catch {
info('The `caretaker` command is not enabled in this repository.');
info(` To enable it, provide a caretaker config in the repository's .ng-dev/ directory`);
Expand Down
16 changes: 8 additions & 8 deletions ng-dev/format/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,41 @@ export function buildFormatParser(localYargs: yargs.Argv) {
'all',
'Run the formatter on all files in the repository',
(args) => args,
({check}) => {
async ({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
const allFiles = GitClient.get().allFiles();
executionCmd(allFiles);
process.exitCode = await executionCmd(allFiles);
},
)
.command(
'changed [shaOrRef]',
'Run the formatter on files changed since the provided sha/ref',
(args) => args.positional('shaOrRef', {type: 'string'}),
({shaOrRef, check}) => {
async ({shaOrRef, check}) => {
const git = GitClient.get();
const sha = shaOrRef || git.mainBranchName;
const executionCmd = check ? checkFiles : formatFiles;
const allChangedFilesSince = git.allChangesFilesSince(sha);
executionCmd(allChangedFilesSince);
process.exitCode = await executionCmd(allChangedFilesSince);
},
)
.command(
'staged',
'Run the formatter on all staged files',
(args) => args,
({check}) => {
async ({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
const allStagedFiles = GitClient.get().allStagedFiles();
executionCmd(allStagedFiles);
process.exitCode = await executionCmd(allStagedFiles);
},
)
.command(
'files <files..>',
'Run the formatter on provided files',
(args) => args.positional('files', {array: true, type: 'string'}),
({check, files}) => {
async ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files!);
process.exitCode = await executionCmd(files!);
},
);
}
21 changes: 12 additions & 9 deletions ng-dev/format/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {runFormatterInParallel} from './run-commands-parallel';

/**
* Format provided files in place.
*
* @returns a status code indicating whether the formatting run was successful.
*/
export async function formatFiles(files: string[]) {
export async function formatFiles(files: string[]): Promise<1 | 0> {
// Whether any files failed to format.
let failures = await runFormatterInParallel(files, 'format');

josephperrott marked this conversation as resolved.
Show resolved Hide resolved
if (failures === false) {
info('No files matched for formatting.');
process.exit(0);
return 0;
}

// The process should exit as a failure if any of the files failed to format.
Expand All @@ -29,22 +31,24 @@ export async function formatFiles(files: string[]) {
info(` • ${filePath}: ${message}`);
});
error(red(`Formatting failed, see errors above for more information.`));
process.exit(1);
return 1;
}
info(`√ Formatting complete.`);
process.exit(0);
return 0;
}

/**
* Check provided files for formatting correctness.
*
* @returns a status code indicating whether the format check run was successful.
*/
export async function checkFiles(files: string[]) {
// Files which are currently not formatted correctly.
const failures = await runFormatterInParallel(files, 'check');

if (failures === false) {
info('No files matched for formatting check.');
process.exit(0);
return 0;
}

if (failures.length) {
Expand All @@ -64,17 +68,16 @@ export async function checkFiles(files: string[]) {

if (runFormatter) {
// Format the failing files as requested.
await formatFiles(failures.map((f) => f.filePath));
process.exit(0);
return (await formatFiles(failures.map((f) => f.filePath))) || 0;
} else {
// Inform user how to format files in the future.
info();
info(`To format the failing file run the following command:`);
info(` yarn ng-dev format files ${failures.map((f) => f.filePath).join(' ')}`);
process.exit(1);
return 1;
}
} else {
info('√ All files correctly formatted.');
process.exit(0);
return 0;
}
}
1 change: 1 addition & 0 deletions ng-dev/release/notes/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ts_library(
],
deps = [
"//ng-dev/commit-message",
"//ng-dev/format",
"//ng-dev/release/config",
"//ng-dev/release/versioning",
"//ng-dev/utils",
Expand Down
41 changes: 20 additions & 21 deletions ng-dev/release/notes/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {writeFileSync} from 'fs';
import {join} from 'path';
import {SemVer} from 'semver';
import {Arguments, Argv, CommandModule} from 'yargs';

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

/** Command line options for building a release. */
export interface ReleaseNotesOptions {
export interface Options {
from: string;
to: string;
outFile?: string;
prependToChangelog: boolean;
releaseVersion: SemVer;
type: 'github-release' | 'changelog';
}

/** Yargs command builder for configuring the `ng-dev release build` command. */
function builder(argv: Argv): Argv<ReleaseNotesOptions> {
function builder(argv: Argv): Argv<Options> {
return argv
.option('releaseVersion', {
type: 'string',
Expand All @@ -48,33 +45,35 @@ function builder(argv: Argv): Argv<ReleaseNotesOptions> {
choices: ['github-release', 'changelog'] as const,
default: 'changelog' as const,
})
.option('outFile', {
type: 'string',
description: 'File location to write the generated release notes to',
coerce: (filePath?: string) => (filePath ? join(process.cwd(), filePath) : undefined),
.option('prependToChangelog', {
type: 'boolean',
default: false,
description: 'Whether to update the changelog with the newly created entry',
});
}

/** Yargs command handler for generating release notes. */
async function handler({releaseVersion, from, to, outFile, type}: Arguments<ReleaseNotesOptions>) {
async function handler({releaseVersion, from, to, prependToChangelog, type}: Arguments<Options>) {
/** The ReleaseNotes instance to generate release notes. */
const releaseNotes = await ReleaseNotes.forRange(releaseVersion, from, to);

if (prependToChangelog) {
await releaseNotes.prependEntryToChangelog();
info(`Added release notes for "${releaseVersion}" to the changelog`);
return;
}

/** The requested release notes entry. */
const releaseNotesEntry = await (type === 'changelog'
? releaseNotes.getChangelogEntry()
: releaseNotes.getGithubReleaseEntry());
const releaseNotesEntry =
type === 'changelog'
? await releaseNotes.getChangelogEntry()
: await releaseNotes.getGithubReleaseEntry();

if (outFile) {
writeFileSync(outFile, releaseNotesEntry);
info(`Generated release notes for "${releaseVersion}" written to ${outFile}`);
} else {
process.stdout.write(releaseNotesEntry);
}
process.stdout.write(releaseNotesEntry);
}

/** CLI command module for generating release notes. */
export const ReleaseNotesCommandModule: CommandModule<{}, ReleaseNotesOptions> = {
export const ReleaseNotesCommandModule: CommandModule<{}, Options> = {
builder,
handler,
command: 'notes',
Expand Down
72 changes: 51 additions & 21 deletions ng-dev/release/notes/release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,48 @@ import * as semver from 'semver';
import {CommitFromGitLog} from '../../commit-message/parse';

import {promptInput} from '../../utils/console';
import {formatFiles} from '../../format/format';
import {GitClient} from '../../utils/git/git-client';
import {assertValidReleaseConfig, ReleaseNotesConfig} from '../config/index';
import {assertValidReleaseConfig, ReleaseConfig} from '../config/index';
import {RenderContext} from './context';

import changelogTemplate from './templates/changelog';
import githubReleaseTemplate from './templates/github-release';
import {getCommitsForRangeWithDeduping} from './commits/get-commits-in-range';
import {getConfig} from '../../utils/config';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {join} from 'path';
import {assertValidFormatConfig} from '../../format/config';

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

/** Release note generation. */
export class ReleaseNotes {
static async forRange(version: semver.SemVer, baseRef: string, headRef: string) {
const client = GitClient.get();
const commits = getCommitsForRangeWithDeduping(client, baseRef, headRef);
return new ReleaseNotes(version, commits);
const git = GitClient.get();
const commits = getCommitsForRangeWithDeduping(git, baseRef, headRef);
return new ReleaseNotes(version, commits, git);
}

/** An instance of GitClient. */
private git = GitClient.get();
/** The absolute path to the changelog file. */
private changelogPath = join(this.git.baseDir, changelogPath);
/** The RenderContext to be used during rendering. */
private renderContext: RenderContext | undefined;
/** The title to use for the release. */
private title: string | false | undefined;
/** The configuration for release notes. */
private config: ReleaseNotesConfig = this.getReleaseConfig().releaseNotes ?? {};
/** The configuration ng-dev. */
private config: {release: ReleaseConfig} = getConfig([assertValidReleaseConfig]);
/** The configuration for the release notes. */
private get notesConfig() {
return this.config.release.releaseNotes ?? {};
}

protected constructor(public version: semver.SemVer, private commits: CommitFromGitLog[]) {}
protected constructor(
public version: semver.SemVer,
private commits: CommitFromGitLog[],
private git: GitClient,
) {}

/** Retrieve the release note generated for a Github Release. */
async getGithubReleaseEntry(): Promise<string> {
Expand All @@ -50,6 +65,29 @@ export class ReleaseNotes {
return render(changelogTemplate, await this.generateRenderContext(), {rmWhitespace: true});
}

/**
* Prepend generated release note to the CHANGELOG.md file in the base directory of the repository
* provided by the GitClient.
*/
async prependEntryToChangelog() {
/** The changelog contents in the current changelog. */
let changelog = '';
if (existsSync(this.changelogPath)) {
changelog = readFileSync(this.changelogPath, {encoding: 'utf8'});
}
/** The new changelog entry to add to the changelog. */
const entry = await this.getChangelogEntry();

writeFileSync(this.changelogPath, `${entry}\n\n${changelog}`);

try {
assertValidFormatConfig(this.config);
await formatFiles([this.changelogPath]);
} catch {
// If the formatting is either unavailable or fails, continue on with the unformatted result.
}
}

/** Retrieve the number of commits included in the release notes after filtering and deduping. */
async getCommitCountInReleaseNotes() {
const context = await this.generateRenderContext();
Expand All @@ -70,7 +108,7 @@ export class ReleaseNotes {
*/
async promptForReleaseTitle() {
if (this.title === undefined) {
if (this.config.useReleaseTitle) {
if (this.notesConfig.useReleaseTitle) {
this.title = await promptInput('Please provide a title for the release:');
} else {
this.title = false;
Expand All @@ -86,20 +124,12 @@ export class ReleaseNotes {
commits: this.commits,
github: this.git.remoteConfig,
version: this.version.format(),
groupOrder: this.config.groupOrder,
hiddenScopes: this.config.hiddenScopes,
categorizeCommit: this.config.categorizeCommit,
groupOrder: this.notesConfig.groupOrder,
hiddenScopes: this.notesConfig.hiddenScopes,
categorizeCommit: this.notesConfig.categorizeCommit,
title: await this.promptForReleaseTitle(),
});
}
return this.renderContext;
}

// This method is used for access to the utility functions while allowing them
// to be overwritten in subclasses during testing.
protected getReleaseConfig() {
const config = getConfig();
assertValidReleaseConfig(config);
return config.release;
}
}
14 changes: 3 additions & 11 deletions ng-dev/release/publish/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,14 @@ import {
} from '../../utils/git/github-urls';
import {createExperimentalSemver} from '../../utils/semver';
import {BuiltPackage, ReleaseConfig} from '../config/index';
import {ReleaseNotes} from '../notes/release-notes';
import {changelogPath, ReleaseNotes} from '../notes/release-notes';
import {NpmDistTag} from '../versioning';
import {ActiveReleaseTrains} from '../versioning/active-release-trains';
import {runNpmPublish} from '../versioning/npm-publish';

import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error';
import {getCommitMessageForRelease, getReleaseNoteCherryPickCommitMessage} from './commit-message';
import {
changelogPath,
githubReleaseBodyLimit,
packageJsonPath,
waitForPullRequestInterval,
} from './constants';
import {githubReleaseBodyLimit, packageJsonPath, waitForPullRequestInterval} from './constants';
import {invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands';
import {findOwnedForksOfRepoQuery} from './graphql-queries';
import {getPullRequestState} from './pull-request-state';
Expand Down Expand Up @@ -372,10 +367,7 @@ export abstract class ReleaseAction {
* @returns A boolean indicating whether the release notes have been prepended.
*/
protected async prependReleaseNotesToChangelog(releaseNotes: ReleaseNotes): Promise<void> {
const localChangelogPath = join(this.projectDir, changelogPath);
const localChangelog = await fs.readFile(localChangelogPath, 'utf8');
const releaseNotesEntry = await releaseNotes.getChangelogEntry();
await fs.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`);
await releaseNotes.prependEntryToChangelog();
info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`));
}

Expand Down
4 changes: 2 additions & 2 deletions ng-dev/release/publish/actions/branch-off-next-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as semver from 'semver';

import {green, info, yellow} from '../../../utils/console';
import {semverInc} from '../../../utils/semver';
import {ReleaseNotes} from '../../notes/release-notes';
import {changelogPath, ReleaseNotes} from '../../notes/release-notes';
import {
computeNewPrereleaseVersionForNext,
getReleaseNotesCompareVersionForNext,
Expand All @@ -20,7 +20,7 @@ import {
getCommitMessageForExceptionalNextVersionBump,
getReleaseNoteCherryPickCommitMessage,
} from '../commit-message';
import {changelogPath, packageJsonPath} from '../constants';
import {packageJsonPath} from '../constants';

/**
* Base action that can be used to move the next release-train into the feature-freeze or
Expand Down
Loading