feat(release)!: rewrite ChangelogRenderer to a class API and remove deprecated config (#28229)
BREAKING CHANGE
In Nx v19, implementing a custom changelog renderer would involve a lot
of work on the user side. They would need to create an additional
function making every property available in its declaration and then
call the underlying default one and customize the final string (or
reimplement the whole thing).
E.g.
```js
const changelogRenderer = async ({
projectGraph,
commits,
releaseVersion,
project,
entryWhenNoChanges,
changelogRenderOptions,
repoSlug,
conventionalCommitsConfig,
changes,
}) => {
const defaultChangelog = await defaultChangelogRenderer({
projectGraph,
commits,
releaseVersion,
project,
entryWhenNoChanges,
changelogRenderOptions,
repoSlug,
conventionalCommitsConfig,
changes,
});
// ...Do custom stuff and return final string...
};
module.exports = changelogRenderer;
```
In Nx v20, changelog renderer are classes. The DefaultChangelogRenderer
can therefore easily and granularly be extended and customized, and the
config does not need to be redeclared on the user side at all. We will
improve things even further in this area, but this breaking change is an
important stepping stone.
E.g. for manipulating the final string equivalent to the previous
example:
```js
module.exports = class CustomChangelogRenderer extends (
DefaultChangelogRenderer
) {
async render() {
const defaultChangelogEntry = await super.render();
// ...Do custom stuff and return final string...
}
};
```
E.g. for customizing just how titles get rendered:
```js
class CustomChangelogRenderer extends DefaultChangelogRenderer {
renderVersionTitle(): string {
return 'Custom Version Title';
}
}
```
This commit is contained in:
parent
fe01c61635
commit
2c0994ac87
@ -544,7 +544,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ # 2.0.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 2.0.0
|
||||
@ -556,7 +555,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ # 2.0.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 2.0.0
|
||||
@ -867,7 +865,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ # 2.0.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 2.0.0
|
||||
@ -879,7 +876,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ # 2.0.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 2.0.0
|
||||
@ -1054,7 +1050,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ # 2.0.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 1.0.1
|
||||
@ -1066,7 +1061,6 @@ describe('nx release circular dependencies', () => {
|
||||
|
||||
+ ## 1.0.1 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🧱 Updated Dependencies
|
||||
+
|
||||
+ - Updated {project-name} to 2.0.0
|
||||
|
||||
@ -364,12 +364,10 @@ describe('nx release conventional commits config', () => {
|
||||
expect(pkg1Changelog).toMatchInlineSnapshot(`
|
||||
# 1.0.0 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### Custom Type
|
||||
|
||||
- ⚠️ **{project-name}:** this is a breaking change
|
||||
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ **{project-name}:** this is a breaking change
|
||||
@ -379,7 +377,6 @@ describe('nx release conventional commits config', () => {
|
||||
expect(pkg2Changelog).toMatchInlineSnapshot(`
|
||||
# 1.0.0 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### Custom Type
|
||||
|
||||
- **{project-name}:** this is a custom type
|
||||
@ -389,7 +386,6 @@ describe('nx release conventional commits config', () => {
|
||||
expect(pkg3Changelog).toMatchInlineSnapshot(`
|
||||
# 1.0.0 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### Custom Docs Header
|
||||
|
||||
- this is a doc
|
||||
@ -408,7 +404,6 @@ describe('nx release conventional commits config', () => {
|
||||
expect(pkg5Changelog).toMatchInlineSnapshot(`
|
||||
# 1.0.0 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- this is a performance improvement
|
||||
@ -418,12 +413,10 @@ describe('nx release conventional commits config', () => {
|
||||
expect(pkg6Changelog).toMatchInlineSnapshot(`
|
||||
# 1.0.0 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- this is refactor
|
||||
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- this is a build
|
||||
|
||||
@ -129,7 +129,6 @@ describe('nx release', () => {
|
||||
|
||||
+ ## 999.9.9 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - an awesome new feature ([{COMMIT_SHA}](https://github.com/nrwl/fake-repo/commit/{COMMIT_SHA}))
|
||||
@ -150,7 +149,6 @@ describe('nx release', () => {
|
||||
expect(readFile('CHANGELOG.md')).toMatchInlineSnapshot(`
|
||||
## 999.9.9 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- an awesome new feature ([{COMMIT_SHA}](https://github.com/nrwl/fake-repo/commit/{COMMIT_SHA}))
|
||||
@ -666,7 +664,7 @@ describe('nx release', () => {
|
||||
+
|
||||
## 999.9.9 (YYYY-MM-DD)
|
||||
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
NX Previewing an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0
|
||||
|
||||
@ -174,12 +174,10 @@ Here is another line in the message.
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -190,12 +188,10 @@ Here is another line in the message.
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -206,14 +202,12 @@ Here is another line in the message.
|
||||
|
||||
+ ## 0.0.1 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+ Here is another line in the message.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -225,14 +219,12 @@ Here is another line in the message.
|
||||
|
||||
+ ## 0.1.0-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+ Here is another line in the message.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -244,14 +236,12 @@ Here is another line in the message.
|
||||
|
||||
+ ## 0.0.1-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+ Here is another line in the message.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -323,17 +313,14 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update packages in both groups with a mix #1
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -345,12 +332,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test
|
||||
@ -363,12 +348,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.0.2 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #1
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -381,12 +364,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -399,12 +380,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.0.1 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -511,12 +490,16 @@ const yargs = require('yargs');
|
||||
verbose: options.verbose,
|
||||
});
|
||||
|
||||
// The returned number value from releasePublish will be zero if all projects are published successfully, non-zero if not
|
||||
const publishStatus = await releasePublish({
|
||||
const publishProjectsResult = await releasePublish({
|
||||
dryRun: options.dryRun,
|
||||
verbose: options.verbose,
|
||||
});
|
||||
process.exit(publishStatus);
|
||||
// Derive an overall exit code from the publish projects result
|
||||
process.exit(
|
||||
Object.values(publishProjectsResult).every((result) => result.code === 0)
|
||||
? 0
|
||||
: 1
|
||||
);
|
||||
})();
|
||||
`
|
||||
);
|
||||
@ -562,12 +545,10 @@ const yargs = require('yargs');
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -578,12 +559,10 @@ const yargs = require('yargs');
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -594,12 +573,10 @@ const yargs = require('yargs');
|
||||
|
||||
+ ## 0.0.1 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -611,12 +588,10 @@ const yargs = require('yargs');
|
||||
|
||||
+ ## 0.1.0-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -628,12 +603,10 @@ const yargs = require('yargs');
|
||||
|
||||
+ ## 0.0.1-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update the independent packages with a patch, preminor, and prerelease.
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -708,17 +681,14 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update packages in both groups with a mix #1
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -730,12 +700,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test
|
||||
@ -748,12 +716,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.0.2 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #1
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -766,12 +732,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.2.0-0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -784,12 +748,10 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.0.1 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🩹 Fixes
|
||||
+
|
||||
+ - Update packages in both groups with a mix #2
|
||||
+
|
||||
+
|
||||
+ ### ❤️ Thank You
|
||||
+
|
||||
+ - Test`
|
||||
@ -850,7 +812,6 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.`
|
||||
@ -861,7 +822,6 @@ Update packages in both groups with a mix #2
|
||||
|
||||
+ ## 0.1.0 (YYYY-MM-DD)
|
||||
+
|
||||
+
|
||||
+ ### 🚀 Features
|
||||
+
|
||||
+ - Update the fixed packages with a minor release.`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,13 +2,10 @@ import { major } from 'semver';
|
||||
import { ChangelogChange } from '../../src/command-line/release/changelog';
|
||||
import { NxReleaseConfig } from '../../src/command-line/release/config/config';
|
||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
||||
import { GitCommit } from '../../src/command-line/release/utils/git';
|
||||
import {
|
||||
GithubRepoData,
|
||||
RepoSlug,
|
||||
formatReferences,
|
||||
} from '../../src/command-line/release/utils/github';
|
||||
import type { ProjectGraph } from '../../src/config/project-graph';
|
||||
|
||||
// axios types and values don't seem to match
|
||||
import _axios = require('axios');
|
||||
@ -21,7 +18,7 @@ const axios = _axios as any as (typeof _axios)['default'];
|
||||
export type ChangelogRenderOptions = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* When versioning projects independently and enabling `"updateDependents": "always"`, there could
|
||||
* When versioning projects independently and enabling `"updateDependents": "auto"`, there could
|
||||
* be additional dependency bump information that is not captured in the commit data, but that nevertheless
|
||||
* should be included in the rendered changelog.
|
||||
*/
|
||||
@ -30,39 +27,6 @@ export type DependencyBump = {
|
||||
newVersion: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A ChangelogRenderer function takes in the extracted commits and other relevant metadata
|
||||
* and returns a string, or a Promise of a string of changelog contents (usually markdown).
|
||||
*
|
||||
* @param {Object} config The configuration object for the ChangelogRenderer
|
||||
* @param {ProjectGraph} config.projectGraph The project graph for the workspace
|
||||
* @param {GitCommit[]} config.commits DEPRECATED [Use 'config.changes' instead] - The collection of extracted commits to generate a changelog for
|
||||
* @param {ChangelogChange[]} config.changes The collection of changes to show in the changelog
|
||||
* @param {string} config.releaseVersion The version that is being released
|
||||
* @param {string | null} config.project The name of specific project to generate a changelog for, or `null` if the overall workspace changelog
|
||||
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
|
||||
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
||||
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data
|
||||
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
|
||||
*/
|
||||
export type ChangelogRenderer = (config: {
|
||||
projectGraph: ProjectGraph;
|
||||
// TODO(v20): remove 'commits' and make 'changes' required
|
||||
commits?: GitCommit[];
|
||||
changes?: ChangelogChange[];
|
||||
releaseVersion: string;
|
||||
project: string | null;
|
||||
entryWhenNoChanges: string | false;
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||
dependencyBumps?: DependencyBump[];
|
||||
// TODO(v20): remove repoSlug in favour of repoData
|
||||
repoSlug?: RepoSlug;
|
||||
repoData?: GithubRepoData;
|
||||
// TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed
|
||||
// null if version plans are being used to generate the changelog
|
||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
||||
}) => Promise<string> | string;
|
||||
|
||||
/**
|
||||
* The specific options available to the default implementation of the ChangelogRenderer that nx exports
|
||||
* for the common case.
|
||||
@ -91,112 +55,209 @@ export interface DefaultChangelogRenderOptions extends ChangelogRenderOptions {
|
||||
versionTitleDate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default ChangelogRenderer implementation that nx exports for the common case of generating markdown
|
||||
* from the given commits and other metadata.
|
||||
*/
|
||||
const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
projectGraph,
|
||||
changes,
|
||||
releaseVersion,
|
||||
project,
|
||||
entryWhenNoChanges,
|
||||
changelogRenderOptions,
|
||||
dependencyBumps,
|
||||
repoSlug,
|
||||
conventionalCommitsConfig,
|
||||
repoData,
|
||||
}): Promise<string> => {
|
||||
const markdownLines: string[] = [];
|
||||
export default class DefaultChangelogRenderer {
|
||||
protected changes: ChangelogChange[];
|
||||
protected changelogEntryVersion: string;
|
||||
protected project: string | null;
|
||||
protected entryWhenNoChanges: string | false;
|
||||
protected changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||
protected isVersionPlans: boolean;
|
||||
protected dependencyBumps?: DependencyBump[];
|
||||
protected repoData?: GithubRepoData;
|
||||
protected conventionalCommitsConfig:
|
||||
| NxReleaseConfig['conventionalCommits']
|
||||
| null;
|
||||
protected relevantChanges: ChangelogChange[];
|
||||
protected breakingChanges: string[];
|
||||
protected additionalChangesForAuthorsSection: ChangelogChange[];
|
||||
|
||||
// If the current range of changes contains both a commit and its revert, we strip them both from the final list. Changes from version plans are unaffected, as they have no hashes.
|
||||
for (const change of changes) {
|
||||
if (change.type === 'revert' && change.revertedHashes) {
|
||||
for (const revertedHash of change.revertedHashes) {
|
||||
const revertedCommit = changes.find(
|
||||
(c) => c.shortHash && revertedHash.startsWith(c.shortHash)
|
||||
);
|
||||
if (revertedCommit) {
|
||||
changes.splice(changes.indexOf(revertedCommit), 1);
|
||||
changes.splice(changes.indexOf(change), 1);
|
||||
/**
|
||||
* A ChangelogRenderer class takes in the determined changes and other relevant metadata
|
||||
* and returns a string, or a Promise of a string of changelog contents (usually markdown).
|
||||
*
|
||||
* @param {Object} config The configuration object for the ChangelogRenderer
|
||||
* @param {ChangelogChange[]} config.changes The collection of changes to show in the changelog
|
||||
* @param {string} config.changelogEntryVersion The version for which we are rendering the current changelog entry
|
||||
* @param {string | null} config.project The name of specific project to generate a changelog entry for, or `null` if the overall workspace changelog
|
||||
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
|
||||
* @param {boolean} config.isVersionPlans Whether or not Nx release version plans are the source of truth for the changelog entry
|
||||
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
||||
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the change data
|
||||
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
|
||||
* @param {NxReleaseConfig['conventionalCommits'] | null} config.conventionalCommitsConfig The configuration for conventional commits, or null if version plans are being used
|
||||
*/
|
||||
constructor(config: {
|
||||
changes: ChangelogChange[];
|
||||
changelogEntryVersion: string;
|
||||
project: string | null;
|
||||
entryWhenNoChanges: string | false;
|
||||
isVersionPlans: boolean;
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||
dependencyBumps?: DependencyBump[];
|
||||
repoData?: GithubRepoData;
|
||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
||||
}) {
|
||||
this.changes = this.filterChanges(config.changes, config.project);
|
||||
this.changelogEntryVersion = config.changelogEntryVersion;
|
||||
this.project = config.project;
|
||||
this.entryWhenNoChanges = config.entryWhenNoChanges;
|
||||
this.isVersionPlans = config.isVersionPlans;
|
||||
this.changelogRenderOptions = config.changelogRenderOptions;
|
||||
this.dependencyBumps = config.dependencyBumps;
|
||||
this.repoData = config.repoData;
|
||||
this.conventionalCommitsConfig = config.conventionalCommitsConfig;
|
||||
|
||||
this.relevantChanges = [];
|
||||
this.breakingChanges = [];
|
||||
this.additionalChangesForAuthorsSection = [];
|
||||
}
|
||||
|
||||
protected filterChanges(
|
||||
changes: ChangelogChange[],
|
||||
project: string | null
|
||||
): ChangelogChange[] {
|
||||
if (project === null) {
|
||||
return changes;
|
||||
}
|
||||
return changes.filter(
|
||||
(c) =>
|
||||
c.affectedProjects &&
|
||||
(c.affectedProjects === '*' || c.affectedProjects.includes(project))
|
||||
);
|
||||
}
|
||||
|
||||
async render(): Promise<string> {
|
||||
const sections: string[][] = [];
|
||||
|
||||
this.preprocessChanges();
|
||||
|
||||
if (this.shouldRenderEmptyEntry()) {
|
||||
return this.renderEmptyEntry();
|
||||
}
|
||||
|
||||
sections.push([this.renderVersionTitle()]);
|
||||
|
||||
const changesByType = this.renderChangesByType();
|
||||
if (changesByType.length > 0) {
|
||||
sections.push(changesByType);
|
||||
}
|
||||
|
||||
if (this.hasBreakingChanges()) {
|
||||
sections.push(this.renderBreakingChanges());
|
||||
}
|
||||
|
||||
if (this.hasDependencyBumps()) {
|
||||
sections.push(this.renderDependencyBumps());
|
||||
}
|
||||
|
||||
if (this.shouldRenderAuthors()) {
|
||||
sections.push(await this.renderAuthors());
|
||||
}
|
||||
|
||||
// Join sections with double newlines, and trim any extra whitespace
|
||||
return sections
|
||||
.filter((section) => section.length > 0)
|
||||
.map((section) => section.join('\n').trim())
|
||||
.join('\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
protected preprocessChanges(): void {
|
||||
this.relevantChanges = [...this.changes];
|
||||
this.breakingChanges = [];
|
||||
this.additionalChangesForAuthorsSection = [];
|
||||
|
||||
// Filter out reverted changes
|
||||
for (let i = this.relevantChanges.length - 1; i >= 0; i--) {
|
||||
const change = this.relevantChanges[i];
|
||||
if (change.type === 'revert' && change.revertedHashes) {
|
||||
for (const revertedHash of change.revertedHashes) {
|
||||
const revertedCommitIndex = this.relevantChanges.findIndex(
|
||||
(c) => c.shortHash && revertedHash.startsWith(c.shortHash)
|
||||
);
|
||||
if (revertedCommitIndex !== -1) {
|
||||
this.relevantChanges.splice(revertedCommitIndex, 1);
|
||||
this.relevantChanges.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isVersionPlans) {
|
||||
this.conventionalCommitsConfig = {
|
||||
types: {
|
||||
feat: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.feat,
|
||||
fix: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.fix,
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = this.relevantChanges.length - 1; i >= 0; i--) {
|
||||
if (this.relevantChanges[i].isBreaking) {
|
||||
const change = this.relevantChanges[i];
|
||||
this.additionalChangesForAuthorsSection.push(change);
|
||||
const line = this.formatChange(change);
|
||||
this.breakingChanges.push(line);
|
||||
this.relevantChanges.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const change of this.relevantChanges) {
|
||||
if (change.isBreaking) {
|
||||
const breakingChangeExplanation =
|
||||
this.extractBreakingChangeExplanation(change.body);
|
||||
this.breakingChanges.push(
|
||||
breakingChangeExplanation
|
||||
? `- ${
|
||||
change.scope ? `**${change.scope.trim()}:** ` : ''
|
||||
}${breakingChangeExplanation}`
|
||||
: this.formatChange(change)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let relevantChanges = changes;
|
||||
const breakingChanges = [];
|
||||
|
||||
// For now to keep the interface of the changelog renderer non-breaking for v19 releases we have a somewhat indirect check for whether or not we are generating a changelog for version plans
|
||||
const isVersionPlans = !conventionalCommitsConfig;
|
||||
|
||||
// Only applicable for version plans
|
||||
const additionalChangesForAuthorsSection = [];
|
||||
|
||||
// Provide a default configuration for version plans to allow most of the subsequent logic to work in the same way it would for conventional commits
|
||||
// NOTE: The one exception is breaking/major changes, where we do not follow the same structure and instead only show the changes once
|
||||
if (isVersionPlans) {
|
||||
conventionalCommitsConfig = {
|
||||
types: {
|
||||
feat: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.feat,
|
||||
fix: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.fix,
|
||||
},
|
||||
};
|
||||
// Trim down "relevant changes" to only include non-breaking ones so that we can render them differently under version plans,
|
||||
// but keep track of the changes for the purposes of the authors section
|
||||
// TODO(v20): Clean this abstraction up as part of the larger overall refactor of changelog rendering
|
||||
for (let i = 0; i < relevantChanges.length; i++) {
|
||||
if (relevantChanges[i].isBreaking) {
|
||||
const change = relevantChanges[i];
|
||||
additionalChangesForAuthorsSection.push(change);
|
||||
const line = formatChange(
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoData
|
||||
);
|
||||
breakingChanges.push(line);
|
||||
relevantChanges.splice(i, 1);
|
||||
}
|
||||
}
|
||||
protected shouldRenderEmptyEntry(): boolean {
|
||||
return (
|
||||
this.relevantChanges.length === 0 &&
|
||||
this.breakingChanges.length === 0 &&
|
||||
!this.hasDependencyBumps()
|
||||
);
|
||||
}
|
||||
|
||||
const changeTypes = conventionalCommitsConfig.types;
|
||||
|
||||
// workspace root level changelog
|
||||
if (project === null) {
|
||||
// No changes for the workspace
|
||||
if (relevantChanges.length === 0 && breakingChanges.length === 0) {
|
||||
if (dependencyBumps?.length) {
|
||||
applyAdditionalDependencyBumps({
|
||||
markdownLines,
|
||||
dependencyBumps,
|
||||
releaseVersion,
|
||||
changelogRenderOptions,
|
||||
});
|
||||
} else if (entryWhenNoChanges) {
|
||||
markdownLines.push(
|
||||
'',
|
||||
`${createVersionTitle(
|
||||
releaseVersion,
|
||||
changelogRenderOptions
|
||||
)}\n\n${entryWhenNoChanges}`,
|
||||
''
|
||||
);
|
||||
}
|
||||
return markdownLines.join('\n').trim();
|
||||
protected renderEmptyEntry(): string {
|
||||
if (this.hasDependencyBumps()) {
|
||||
return [
|
||||
this.renderVersionTitle(),
|
||||
'',
|
||||
...this.renderDependencyBumps(),
|
||||
].join('\n');
|
||||
} else if (this.entryWhenNoChanges) {
|
||||
return `${this.renderVersionTitle()}\n\n${this.entryWhenNoChanges}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const typeGroups: Record<string, ChangelogChange[]> = groupBy(
|
||||
relevantChanges,
|
||||
'type'
|
||||
);
|
||||
protected renderVersionTitle(): string {
|
||||
const isMajorVersion =
|
||||
`${major(this.changelogEntryVersion)}.0.0` ===
|
||||
this.changelogEntryVersion.replace(/^v/, '');
|
||||
let maybeDateStr = '';
|
||||
if (this.changelogRenderOptions.versionTitleDate) {
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
maybeDateStr = ` (${dateStr})`;
|
||||
}
|
||||
return isMajorVersion
|
||||
? `# ${this.changelogEntryVersion}${maybeDateStr}`
|
||||
: `## ${this.changelogEntryVersion}${maybeDateStr}`;
|
||||
}
|
||||
|
||||
markdownLines.push(
|
||||
'',
|
||||
createVersionTitle(releaseVersion, changelogRenderOptions),
|
||||
''
|
||||
);
|
||||
protected renderChangesByType(): string[] {
|
||||
const markdownLines: string[] = [];
|
||||
const typeGroups = this.groupChangesByType();
|
||||
const changeTypes = this.conventionalCommitsConfig.types;
|
||||
|
||||
for (const type of Object.keys(changeTypes)) {
|
||||
const group = typeGroups[type];
|
||||
@ -204,37 +265,41 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
markdownLines.push('', '### ' + changeTypes[type].changelog.title, '');
|
||||
markdownLines.push('', `### ${changeTypes[type].changelog.title}`, '');
|
||||
|
||||
/**
|
||||
* In order to make the final changelog most readable, we organize changes as follows:
|
||||
* - By scope, where scopes are in alphabetical order (changes with no scope are listed first)
|
||||
* - Within a particular scope grouping, we list changes in chronological order
|
||||
*/
|
||||
const changesInChronologicalOrder = group.reverse();
|
||||
const changesGroupedByScope: Record<string, ChangelogChange[]> = groupBy(
|
||||
changesInChronologicalOrder,
|
||||
'scope'
|
||||
);
|
||||
const scopesSortedAlphabetically = Object.keys(
|
||||
changesGroupedByScope
|
||||
).sort();
|
||||
if (this.project === null) {
|
||||
const changesGroupedByScope = this.groupChangesByScope(group);
|
||||
const scopesSortedAlphabetically = Object.keys(
|
||||
changesGroupedByScope
|
||||
).sort();
|
||||
|
||||
for (const scope of scopesSortedAlphabetically) {
|
||||
const changes = changesGroupedByScope[scope];
|
||||
for (const change of changes) {
|
||||
const line = formatChange(
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoData
|
||||
);
|
||||
for (const scope of scopesSortedAlphabetically) {
|
||||
const changes = changesGroupedByScope[scope];
|
||||
for (const change of changes.reverse()) {
|
||||
const line = this.formatChange(change);
|
||||
markdownLines.push(line);
|
||||
if (change.isBreaking && !this.isVersionPlans) {
|
||||
const breakingChangeExplanation =
|
||||
this.extractBreakingChangeExplanation(change.body);
|
||||
this.breakingChanges.push(
|
||||
breakingChangeExplanation
|
||||
? `- ${
|
||||
change.scope ? `**${change.scope.trim()}:** ` : ''
|
||||
}${breakingChangeExplanation}`
|
||||
: line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For project-specific changelogs, maintain the original order
|
||||
for (const change of group) {
|
||||
const line = this.formatChange(change);
|
||||
markdownLines.push(line);
|
||||
if (change.isBreaking) {
|
||||
const breakingChangeExplanation = extractBreakingChangeExplanation(
|
||||
change.body
|
||||
);
|
||||
breakingChanges.push(
|
||||
if (change.isBreaking && !this.isVersionPlans) {
|
||||
const breakingChangeExplanation =
|
||||
this.extractBreakingChangeExplanation(change.body);
|
||||
this.breakingChanges.push(
|
||||
breakingChangeExplanation
|
||||
? `- ${
|
||||
change.scope ? `**${change.scope.trim()}:** ` : ''
|
||||
@ -245,123 +310,68 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// project level changelog
|
||||
relevantChanges = relevantChanges.filter(
|
||||
(c) =>
|
||||
c.affectedProjects &&
|
||||
(c.affectedProjects === '*' || c.affectedProjects.includes(project))
|
||||
);
|
||||
|
||||
// Generating for a named project, but that project has no relevant changes in the current set of commits, exit early
|
||||
if (relevantChanges.length === 0 && breakingChanges.length === 0) {
|
||||
if (dependencyBumps?.length) {
|
||||
applyAdditionalDependencyBumps({
|
||||
markdownLines,
|
||||
dependencyBumps,
|
||||
releaseVersion,
|
||||
changelogRenderOptions,
|
||||
});
|
||||
} else if (entryWhenNoChanges) {
|
||||
markdownLines.push(
|
||||
'',
|
||||
`${createVersionTitle(
|
||||
releaseVersion,
|
||||
changelogRenderOptions
|
||||
)}\n\n${entryWhenNoChanges}`,
|
||||
''
|
||||
);
|
||||
}
|
||||
return markdownLines.join('\n').trim();
|
||||
}
|
||||
|
||||
markdownLines.push(
|
||||
'',
|
||||
createVersionTitle(releaseVersion, changelogRenderOptions),
|
||||
''
|
||||
);
|
||||
|
||||
const typeGroups: Record<string, ChangelogChange[]> = groupBy(
|
||||
// Sort the relevant changes to have the unscoped changes first, before grouping by type
|
||||
relevantChanges.sort((a, b) => (b.scope ? 1 : 0) - (a.scope ? 1 : 0)),
|
||||
'type'
|
||||
);
|
||||
for (const type of Object.keys(changeTypes)) {
|
||||
const group = typeGroups[type];
|
||||
if (!group || group.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
markdownLines.push('', `### ${changeTypes[type].changelog.title}`, '');
|
||||
|
||||
const changesInChronologicalOrder = group.reverse();
|
||||
for (const change of changesInChronologicalOrder) {
|
||||
const line = formatChange(
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoData
|
||||
);
|
||||
markdownLines.push(line + '\n');
|
||||
if (change.isBreaking) {
|
||||
const breakingChangeExplanation = extractBreakingChangeExplanation(
|
||||
change.body
|
||||
);
|
||||
breakingChanges.push(
|
||||
breakingChangeExplanation
|
||||
? `- ${
|
||||
change.scope ? `**${change.scope.trim()}:** ` : ''
|
||||
}${breakingChangeExplanation}`
|
||||
: line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return markdownLines;
|
||||
}
|
||||
|
||||
if (breakingChanges.length > 0) {
|
||||
markdownLines.push('', '### ⚠️ Breaking Changes', '', ...breakingChanges);
|
||||
protected hasBreakingChanges(): boolean {
|
||||
return this.breakingChanges.length > 0;
|
||||
}
|
||||
|
||||
if (dependencyBumps?.length) {
|
||||
applyAdditionalDependencyBumps({
|
||||
markdownLines,
|
||||
dependencyBumps,
|
||||
releaseVersion,
|
||||
changelogRenderOptions,
|
||||
protected renderBreakingChanges(): string[] {
|
||||
const uniqueBreakingChanges = Array.from(new Set(this.breakingChanges));
|
||||
return ['### ⚠️ Breaking Changes', '', ...uniqueBreakingChanges];
|
||||
}
|
||||
|
||||
protected hasDependencyBumps(): boolean {
|
||||
return this.dependencyBumps && this.dependencyBumps.length > 0;
|
||||
}
|
||||
|
||||
protected renderDependencyBumps(): string[] {
|
||||
const markdownLines = ['', '### 🧱 Updated Dependencies', ''];
|
||||
this.dependencyBumps.forEach(({ dependencyName, newVersion }) => {
|
||||
markdownLines.push(`- Updated ${dependencyName} to ${newVersion}`);
|
||||
});
|
||||
return markdownLines;
|
||||
}
|
||||
|
||||
if (changelogRenderOptions.authors) {
|
||||
protected shouldRenderAuthors(): boolean {
|
||||
return this.changelogRenderOptions.authors;
|
||||
}
|
||||
|
||||
protected async renderAuthors(): Promise<string[]> {
|
||||
const markdownLines: string[] = [];
|
||||
const _authors = new Map<string, { email: Set<string>; github?: string }>();
|
||||
|
||||
for (const change of [
|
||||
...relevantChanges,
|
||||
...additionalChangesForAuthorsSection,
|
||||
...this.relevantChanges,
|
||||
...this.additionalChangesForAuthorsSection,
|
||||
]) {
|
||||
if (!change.author) {
|
||||
if (!change.authors) {
|
||||
continue;
|
||||
}
|
||||
const name = formatName(change.author.name);
|
||||
if (!name || name.includes('[bot]')) {
|
||||
continue;
|
||||
}
|
||||
if (_authors.has(name)) {
|
||||
const entry = _authors.get(name);
|
||||
entry.email.add(change.author.email);
|
||||
} else {
|
||||
_authors.set(name, { email: new Set([change.author.email]) });
|
||||
for (const author of change.authors) {
|
||||
const name = this.formatName(author.name);
|
||||
if (!name || name.includes('[bot]')) {
|
||||
continue;
|
||||
}
|
||||
if (_authors.has(name)) {
|
||||
const entry = _authors.get(name);
|
||||
entry.email.add(author.email);
|
||||
} else {
|
||||
_authors.set(name, { email: new Set([author.email]) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to map authors to github usernames
|
||||
if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
|
||||
if (
|
||||
this.repoData &&
|
||||
this.changelogRenderOptions.mapAuthorsToGitHubUsernames
|
||||
) {
|
||||
await Promise.all(
|
||||
[..._authors.keys()].map(async (authorName) => {
|
||||
const meta = _authors.get(authorName);
|
||||
for (const email of meta.email) {
|
||||
// For these pseudo-anonymized emails we can just extract the Github username from before the @
|
||||
// It could either be in the format: username@ or github_id+username@
|
||||
if (email.endsWith('@users.noreply.github.com')) {
|
||||
const match = email.match(
|
||||
/^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/
|
||||
@ -371,7 +381,6 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Look up any other emails against the ungh.cc API
|
||||
const { data } = await axios
|
||||
.get<any, { data?: { user?: { username: string } } }>(
|
||||
`https://ungh.cc/users/find/${email}`
|
||||
@ -397,147 +406,93 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
'### ' + '❤️ Thank You',
|
||||
'',
|
||||
...authors
|
||||
// Sort the contributors by name
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((i) => {
|
||||
// Tag the author's Github username if we were able to resolve it so that Github adds them as a contributor
|
||||
const github = i.github ? ` @${i.github}` : '';
|
||||
return `- ${i.name}${github}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return markdownLines;
|
||||
}
|
||||
|
||||
return markdownLines.join('\n').trim();
|
||||
};
|
||||
protected formatChange(change: ChangelogChange): string {
|
||||
let description = change.description;
|
||||
let extraLines = [];
|
||||
let extraLinesStr = '';
|
||||
if (description.includes('\n')) {
|
||||
[description, ...extraLines] = description.split('\n');
|
||||
const indentation = ' ';
|
||||
extraLinesStr = extraLines
|
||||
.filter((l) => l.trim().length > 0)
|
||||
.map((l) => `${indentation}${l}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export default defaultChangelogRenderer;
|
||||
|
||||
function applyAdditionalDependencyBumps({
|
||||
markdownLines,
|
||||
dependencyBumps,
|
||||
releaseVersion,
|
||||
changelogRenderOptions,
|
||||
}: {
|
||||
markdownLines: string[];
|
||||
dependencyBumps: DependencyBump[];
|
||||
releaseVersion: string;
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||
}) {
|
||||
if (markdownLines.length === 0) {
|
||||
markdownLines.push(
|
||||
'',
|
||||
`${createVersionTitle(releaseVersion, changelogRenderOptions)}\n`,
|
||||
''
|
||||
);
|
||||
} else {
|
||||
markdownLines.push('');
|
||||
let changeLine =
|
||||
'- ' +
|
||||
(!this.isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
|
||||
(!this.isVersionPlans && change.scope
|
||||
? `**${change.scope.trim()}:** `
|
||||
: '') +
|
||||
description;
|
||||
if (this.repoData && this.changelogRenderOptions.commitReferences) {
|
||||
changeLine += formatReferences(change.githubReferences, this.repoData);
|
||||
}
|
||||
if (extraLinesStr) {
|
||||
changeLine += '\n\n' + extraLinesStr;
|
||||
}
|
||||
return changeLine;
|
||||
}
|
||||
|
||||
protected groupChangesByType(): Record<string, ChangelogChange[]> {
|
||||
const typeGroups: Record<string, ChangelogChange[]> = {};
|
||||
for (const change of this.relevantChanges) {
|
||||
typeGroups[change.type] = typeGroups[change.type] || [];
|
||||
typeGroups[change.type].push(change);
|
||||
}
|
||||
return typeGroups;
|
||||
}
|
||||
|
||||
protected groupChangesByScope(
|
||||
changes: ChangelogChange[]
|
||||
): Record<string, ChangelogChange[]> {
|
||||
const scopeGroups: Record<string, ChangelogChange[]> = {};
|
||||
for (const change of changes) {
|
||||
const scope = change.scope || '';
|
||||
scopeGroups[scope] = scopeGroups[scope] || [];
|
||||
scopeGroups[scope].push(change);
|
||||
}
|
||||
return scopeGroups;
|
||||
}
|
||||
|
||||
protected extractBreakingChangeExplanation(message: string): string | null {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const breakingChangeIdentifier = 'BREAKING CHANGE:';
|
||||
const startIndex = message.indexOf(breakingChangeIdentifier);
|
||||
|
||||
if (startIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startOfBreakingChange = startIndex + breakingChangeIdentifier.length;
|
||||
const endOfBreakingChange = message.indexOf('\n', startOfBreakingChange);
|
||||
|
||||
if (endOfBreakingChange === -1) {
|
||||
return message.substring(startOfBreakingChange).trim();
|
||||
}
|
||||
|
||||
return message.substring(startOfBreakingChange, endOfBreakingChange).trim();
|
||||
}
|
||||
|
||||
protected formatName(name = ''): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((p) => p.trim())
|
||||
.join(' ');
|
||||
}
|
||||
markdownLines.push('### 🧱 Updated Dependencies\n');
|
||||
dependencyBumps.forEach(({ dependencyName, newVersion }) => {
|
||||
markdownLines.push(`- Updated ${dependencyName} to ${newVersion}`);
|
||||
});
|
||||
markdownLines.push('');
|
||||
}
|
||||
|
||||
function formatName(name = '') {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((p) => p.trim())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function groupBy(items: any[], key: string) {
|
||||
const groups = {};
|
||||
for (const item of items) {
|
||||
groups[item[key]] = groups[item[key]] || [];
|
||||
groups[item[key]].push(item);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function formatChange(
|
||||
change: ChangelogChange,
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions,
|
||||
isVersionPlans: boolean,
|
||||
repoData?: GithubRepoData
|
||||
): string {
|
||||
let description = change.description;
|
||||
let extraLines = [];
|
||||
let extraLinesStr = '';
|
||||
if (description.includes('\n')) {
|
||||
[description, ...extraLines] = description.split('\n');
|
||||
// Align the extra lines with the start of the description for better readability
|
||||
const indentation = ' ';
|
||||
extraLinesStr = extraLines
|
||||
.filter((l) => l.trim().length > 0)
|
||||
.map((l) => `${indentation}${l}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* In version plans changelogs:
|
||||
* - don't repeat the breaking change icon
|
||||
* - don't render the scope
|
||||
*/
|
||||
let changeLine =
|
||||
'- ' +
|
||||
(!isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
|
||||
(!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') +
|
||||
description;
|
||||
if (repoData && changelogRenderOptions.commitReferences) {
|
||||
changeLine += formatReferences(change.githubReferences, repoData);
|
||||
}
|
||||
if (extraLinesStr) {
|
||||
changeLine += '\n\n' + extraLinesStr;
|
||||
}
|
||||
return changeLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* It is common to add further information about a breaking change in the commit body,
|
||||
* and it is naturally that information that should be included in the BREAKING CHANGES
|
||||
* section of changelog, rather than repeating the commit title/description.
|
||||
*/
|
||||
function extractBreakingChangeExplanation(message: string): string | null {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const breakingChangeIdentifier = 'BREAKING CHANGE:';
|
||||
const startIndex = message.indexOf(breakingChangeIdentifier);
|
||||
|
||||
if (startIndex === -1) {
|
||||
// "BREAKING CHANGE:" not found in the message
|
||||
return null;
|
||||
}
|
||||
|
||||
const startOfBreakingChange = startIndex + breakingChangeIdentifier.length;
|
||||
const endOfBreakingChange = message.indexOf('\n', startOfBreakingChange);
|
||||
|
||||
if (endOfBreakingChange === -1) {
|
||||
// No newline character found, extract till the end of the message
|
||||
return message.substring(startOfBreakingChange).trim();
|
||||
}
|
||||
|
||||
// Extract and return the breaking change message
|
||||
return message.substring(startOfBreakingChange, endOfBreakingChange).trim();
|
||||
}
|
||||
|
||||
function createVersionTitle(
|
||||
version: string,
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions
|
||||
) {
|
||||
// Normalize by removing any leading `v` during comparison
|
||||
const isMajorVersion = `${major(version)}.0.0` === version.replace(/^v/, '');
|
||||
let maybeDateStr = '';
|
||||
if (changelogRenderOptions.versionTitleDate) {
|
||||
// YYYY-MM-DD
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
maybeDateStr = ` (${dateStr})`;
|
||||
}
|
||||
if (isMajorVersion) {
|
||||
return `# ${version}${maybeDateStr}`;
|
||||
}
|
||||
return `## ${version}${maybeDateStr}`;
|
||||
}
|
||||
|
||||
@ -96,8 +96,7 @@ export interface ChangelogChange {
|
||||
body?: string;
|
||||
isBreaking?: boolean;
|
||||
githubReferences?: Reference[];
|
||||
// TODO(v20): This should be an array of one or more authors (Co-authored-by is supported at the commit level and should have been supported here)
|
||||
author?: { name: string; email: string };
|
||||
authors?: { name: string; email: string }[];
|
||||
shortHash?: string;
|
||||
revertedHashes?: string[];
|
||||
}
|
||||
@ -303,7 +302,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: '',
|
||||
isBreaking: releaseType.isBreaking,
|
||||
githubReferences,
|
||||
author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [author],
|
||||
affectedProjects: '*',
|
||||
}
|
||||
: vp.triggeredByProjects.map((project) => {
|
||||
@ -314,7 +314,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: '',
|
||||
isBreaking: releaseType.isBreaking,
|
||||
githubReferences,
|
||||
author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [author],
|
||||
affectedProjects: [project],
|
||||
};
|
||||
});
|
||||
@ -362,7 +363,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: c.body,
|
||||
isBreaking: c.isBreaking,
|
||||
githubReferences: c.references,
|
||||
author: c.author,
|
||||
authors: [c.author],
|
||||
shortHash: c.shortHash,
|
||||
revertedHashes: c.revertedHashes,
|
||||
affectedProjects: '*',
|
||||
@ -515,13 +516,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
const releaseType =
|
||||
versionPlanSemverReleaseTypeToChangelogType(bumpForProject);
|
||||
let githubReferences = [];
|
||||
let author = undefined;
|
||||
let authors = [];
|
||||
const parsedCommit = vp.commit
|
||||
? parseGitCommit(vp.commit, true)
|
||||
: null;
|
||||
if (parsedCommit) {
|
||||
githubReferences = parsedCommit.references;
|
||||
author = parsedCommit.author;
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors = [parsedCommit.author];
|
||||
}
|
||||
return {
|
||||
type: releaseType.type,
|
||||
@ -531,8 +533,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
isBreaking: releaseType.isBreaking,
|
||||
affectedProjects: Object.keys(vp.projectVersionBumps),
|
||||
githubReferences,
|
||||
author,
|
||||
};
|
||||
authors,
|
||||
} as ChangelogChange;
|
||||
})
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
@ -589,7 +591,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: c.body,
|
||||
isBreaking: c.isBreaking,
|
||||
githubReferences: c.references,
|
||||
author: c.author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [c.author],
|
||||
shortHash: c.shortHash,
|
||||
revertedHashes: c.revertedHashes,
|
||||
affectedProjects: commitChangesNonProjectFiles(
|
||||
@ -606,18 +609,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
const projectChangelogs = await generateChangelogForProjects({
|
||||
tree,
|
||||
args,
|
||||
projectGraph,
|
||||
changes,
|
||||
projectsVersionData,
|
||||
releaseGroup,
|
||||
projects: [project],
|
||||
nxReleaseConfig,
|
||||
projectToAdditionalDependencyBumps,
|
||||
// TODO: remove this after the changelog renderer is refactored to remove coupling with git commits
|
||||
commits: filterHiddenCommits(
|
||||
commits,
|
||||
nxReleaseConfig.conventionalCommits
|
||||
),
|
||||
});
|
||||
|
||||
let hasPushed = false;
|
||||
@ -688,7 +685,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: '',
|
||||
isBreaking: releaseType.isBreaking,
|
||||
githubReferences,
|
||||
author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [author],
|
||||
affectedProjects: '*',
|
||||
}
|
||||
: vp.triggeredByProjects.map((project) => {
|
||||
@ -699,7 +697,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: '',
|
||||
isBreaking: releaseType.isBreaking,
|
||||
githubReferences,
|
||||
author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [author],
|
||||
affectedProjects: [project],
|
||||
};
|
||||
});
|
||||
@ -745,7 +744,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
body: c.body,
|
||||
isBreaking: c.isBreaking,
|
||||
githubReferences: c.references,
|
||||
author: c.author,
|
||||
// TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors
|
||||
authors: [c.author],
|
||||
shortHash: c.shortHash,
|
||||
revertedHashes: c.revertedHashes,
|
||||
affectedProjects: commitChangesNonProjectFiles(
|
||||
@ -762,18 +762,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
const projectChangelogs = await generateChangelogForProjects({
|
||||
tree,
|
||||
args,
|
||||
projectGraph,
|
||||
changes,
|
||||
projectsVersionData,
|
||||
releaseGroup,
|
||||
projects: projectNodes,
|
||||
nxReleaseConfig,
|
||||
projectToAdditionalDependencyBumps,
|
||||
// TODO: remove this after the changelog renderer is refactored to remove coupling with git commits
|
||||
commits: filterHiddenCommits(
|
||||
commits,
|
||||
nxReleaseConfig.conventionalCommits
|
||||
),
|
||||
});
|
||||
|
||||
let hasPushed = false;
|
||||
@ -1094,7 +1088,7 @@ async function generateChangelogForWorkspace({
|
||||
const dryRun = !!args.dryRun;
|
||||
const gitRemote = args.gitRemote;
|
||||
|
||||
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
||||
const ChangelogRendererClass = resolveChangelogRenderer(config.renderer);
|
||||
|
||||
let interpolatedTreePath = config.file || '';
|
||||
if (interpolatedTreePath) {
|
||||
@ -1121,18 +1115,17 @@ async function generateChangelogForWorkspace({
|
||||
|
||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||
|
||||
let contents = await changelogRenderer({
|
||||
projectGraph,
|
||||
const changelogRenderer = new ChangelogRendererClass({
|
||||
changes,
|
||||
commits,
|
||||
releaseVersion: releaseVersion.rawVersion,
|
||||
changelogEntryVersion: releaseVersion.rawVersion,
|
||||
project: null,
|
||||
repoSlug: githubRepoData?.slug,
|
||||
isVersionPlans: false,
|
||||
repoData: githubRepoData,
|
||||
entryWhenNoChanges: config.entryWhenNoChanges,
|
||||
changelogRenderOptions: config.renderOptions,
|
||||
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
||||
});
|
||||
let contents = await changelogRenderer.render();
|
||||
|
||||
/**
|
||||
* If interactive mode, make the changelog contents available for the user to modify in their editor of choice,
|
||||
@ -1191,9 +1184,7 @@ async function generateChangelogForWorkspace({
|
||||
async function generateChangelogForProjects({
|
||||
tree,
|
||||
args,
|
||||
projectGraph,
|
||||
changes,
|
||||
commits,
|
||||
projectsVersionData,
|
||||
releaseGroup,
|
||||
projects,
|
||||
@ -1202,9 +1193,7 @@ async function generateChangelogForProjects({
|
||||
}: {
|
||||
tree: Tree;
|
||||
args: ChangelogOptions;
|
||||
projectGraph: ProjectGraph;
|
||||
changes: ChangelogChange[];
|
||||
commits: GitCommit[];
|
||||
projectsVersionData: VersionData;
|
||||
releaseGroup: ReleaseGroupWithName;
|
||||
projects: ProjectGraphProjectNode[];
|
||||
@ -1223,7 +1212,7 @@ async function generateChangelogForProjects({
|
||||
const dryRun = !!args.dryRun;
|
||||
const gitRemote = args.gitRemote;
|
||||
|
||||
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
||||
const ChangelogRendererClass = resolveChangelogRenderer(config.renderer);
|
||||
|
||||
const projectChangelogs: NxReleaseChangelogResult['projectChangelogs'] = {};
|
||||
|
||||
@ -1262,13 +1251,10 @@ async function generateChangelogForProjects({
|
||||
|
||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||
|
||||
let contents = await changelogRenderer({
|
||||
projectGraph,
|
||||
const changelogRenderer = new ChangelogRendererClass({
|
||||
changes,
|
||||
commits,
|
||||
releaseVersion: releaseVersion.rawVersion,
|
||||
changelogEntryVersion: releaseVersion.rawVersion,
|
||||
project: project.name,
|
||||
repoSlug: githubRepoData?.slug,
|
||||
repoData: githubRepoData,
|
||||
entryWhenNoChanges:
|
||||
typeof config.entryWhenNoChanges === 'string'
|
||||
@ -1279,11 +1265,13 @@ async function generateChangelogForProjects({
|
||||
})
|
||||
: false,
|
||||
changelogRenderOptions: config.renderOptions,
|
||||
isVersionPlans: !!releaseGroup.versionPlans,
|
||||
conventionalCommitsConfig: releaseGroup.versionPlans
|
||||
? null
|
||||
: nxReleaseConfig.conventionalCommits,
|
||||
dependencyBumps: projectToAdditionalDependencyBumps.get(project.name),
|
||||
});
|
||||
let contents = await changelogRenderer.render();
|
||||
|
||||
/**
|
||||
* If interactive mode, make the changelog contents available for the user to modify in their editor of choice,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ChangelogRenderer } from '../../../../release/changelog-renderer';
|
||||
import type ChangelogRenderer from '../../../../release/changelog-renderer';
|
||||
import { registerTsProject } from '../../../plugins/js/utils/register';
|
||||
import { getRootTsConfigPath } from '../../../plugins/js/utils/typescript';
|
||||
import { interpolate } from '../../../tasks-runner/utils';
|
||||
@ -6,13 +6,13 @@ import { workspaceRoot } from '../../../utils/workspace-root';
|
||||
|
||||
export function resolveChangelogRenderer(
|
||||
changelogRendererPath: string
|
||||
): ChangelogRenderer {
|
||||
): typeof ChangelogRenderer {
|
||||
const interpolatedChangelogRendererPath = interpolate(changelogRendererPath, {
|
||||
workspaceRoot,
|
||||
});
|
||||
|
||||
// Try and load the provided (or default) changelog renderer
|
||||
let changelogRenderer: ChangelogRenderer;
|
||||
let ChangelogRendererClass: typeof ChangelogRenderer;
|
||||
let cleanupTranspiler = () => {};
|
||||
try {
|
||||
const rootTsconfigPath = getRootTsConfigPath();
|
||||
@ -20,11 +20,11 @@ export function resolveChangelogRenderer(
|
||||
cleanupTranspiler = registerTsProject(rootTsconfigPath);
|
||||
}
|
||||
const r = require(interpolatedChangelogRendererPath);
|
||||
changelogRenderer = r.default || r;
|
||||
ChangelogRendererClass = r.default || r;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
cleanupTranspiler();
|
||||
}
|
||||
return changelogRenderer;
|
||||
return ChangelogRendererClass;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user