fix(release): allow version plans to have multi-line, arbitrarily formatted messages (#27323)
This commit is contained in:
parent
555182353b
commit
7e2266177d
@ -114,7 +114,7 @@ describe('nx release version plans', () => {
|
|||||||
await ensureDir(versionPlansDir);
|
await ensureDir(versionPlansDir);
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
'release plan minor -g fixed-group -m "feat: Update the fixed packages with a minor release." --verbose',
|
'release plan minor -g fixed-group -m "Update the fixed packages with a minor release." --verbose',
|
||||||
{
|
{
|
||||||
silenceError: true,
|
silenceError: true,
|
||||||
}
|
}
|
||||||
@ -128,7 +128,9 @@ ${pkg4}: preminor
|
|||||||
${pkg5}: prerelease
|
${pkg5}: prerelease
|
||||||
---
|
---
|
||||||
|
|
||||||
feat: Update the independent packages with a patch, preminor, and prerelease.
|
Update the independent packages with a patch, preminor, and prerelease.
|
||||||
|
|
||||||
|
Here is another line in the message.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -193,9 +195,11 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
|
|||||||
+ ## 0.0.1 (YYYY-MM-DD)
|
+ ## 0.0.1 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg3}:** Update the independent packages with a patch, preminor, and prerelease.
|
||||||
|
+
|
||||||
|
+ Here is another line in the message.`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resultWithoutDate).toContain(
|
expect(resultWithoutDate).toContain(
|
||||||
@ -207,7 +211,9 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg4}:** Update the independent packages with a patch, preminor, and prerelease.
|
||||||
|
+
|
||||||
|
+ Here is another line in the message.`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resultWithoutDate).toContain(
|
expect(resultWithoutDate).toContain(
|
||||||
@ -217,9 +223,11 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
|
|||||||
+ ## 0.0.1-0 (YYYY-MM-DD)
|
+ ## 0.0.1-0 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg5}:** Update the independent packages with a patch, preminor, and prerelease.
|
||||||
|
+
|
||||||
|
+ Here is another line in the message.`
|
||||||
);
|
);
|
||||||
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
@ -229,7 +237,7 @@ ${pkg1}: minor
|
|||||||
${pkg3}: patch
|
${pkg3}: patch
|
||||||
---
|
---
|
||||||
|
|
||||||
fix: Update packages in both groups with a bug fix
|
Update packages in both groups with a mix #1
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
@ -240,7 +248,7 @@ ${pkg4}: preminor
|
|||||||
${pkg5}: patch
|
${pkg5}: patch
|
||||||
---
|
---
|
||||||
|
|
||||||
feat: Update packages in both groups with a feat
|
Update packages in both groups with a mix #2
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -291,12 +299,12 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat
|
+ - **${pkg1}:** Update packages in both groups with a mix #1
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix`
|
+ - Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
|
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
|
||||||
@ -306,14 +314,9 @@ feat: Update packages in both groups with a feat
|
|||||||
+ ## 0.2.0 (YYYY-MM-DD)
|
+ ## 0.2.0 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
|
||||||
+
|
|
||||||
+ - Update packages in both groups with a feat
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix
|
+ - Update packages in both groups with a mix #2
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -326,7 +329,7 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix`
|
+ - **${pkg3}:** Update packages in both groups with a mix #1`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -339,7 +342,7 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat`
|
+ - **${pkg4}:** Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -350,9 +353,9 @@ feat: Update packages in both groups with a feat
|
|||||||
+ ## 0.0.1 (YYYY-MM-DD)
|
+ ## 0.0.1 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat`
|
+ - **${pkg5}:** Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
|
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
|
||||||
@ -394,7 +397,7 @@ feat: Update packages in both groups with a feat
|
|||||||
fixed-group: minor
|
fixed-group: minor
|
||||||
---
|
---
|
||||||
|
|
||||||
feat: Update the fixed packages with a minor release.
|
Update the fixed packages with a minor release.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -406,7 +409,7 @@ ${pkg4}: preminor
|
|||||||
${pkg5}: prerelease
|
${pkg5}: prerelease
|
||||||
---
|
---
|
||||||
|
|
||||||
feat: Update the independent packages with a patch, preminor, and prerelease.
|
Update the independent packages with a patch, preminor, and prerelease.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -530,9 +533,9 @@ const yargs = require('yargs');
|
|||||||
+ ## 0.0.1 (YYYY-MM-DD)
|
+ ## 0.0.1 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg3}:** Update the independent packages with a patch, preminor, and prerelease.`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resultWithoutDate).toContain(
|
expect(resultWithoutDate).toContain(
|
||||||
@ -544,7 +547,7 @@ const yargs = require('yargs');
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg4}:** Update the independent packages with a patch, preminor, and prerelease.`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resultWithoutDate).toContain(
|
expect(resultWithoutDate).toContain(
|
||||||
@ -554,9 +557,9 @@ const yargs = require('yargs');
|
|||||||
+ ## 0.0.1-0 (YYYY-MM-DD)
|
+ ## 0.0.1-0 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update the independent packages with a patch, preminor, and prerelease.`
|
+ - **${pkg5}:** Update the independent packages with a patch, preminor, and prerelease.`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(exists(join(versionPlansDir, 'bump-fixed.md'))).toBeFalsy();
|
expect(exists(join(versionPlansDir, 'bump-fixed.md'))).toBeFalsy();
|
||||||
@ -569,8 +572,8 @@ ${pkg1}: minor
|
|||||||
${pkg3}: patch
|
${pkg3}: patch
|
||||||
---
|
---
|
||||||
|
|
||||||
fix: Update packages in both groups with a bug fix
|
Update packages in both groups with a mix #1
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(versionPlansDir, 'bump-mixed2.md'),
|
join(versionPlansDir, 'bump-mixed2.md'),
|
||||||
@ -580,8 +583,8 @@ ${pkg4}: preminor
|
|||||||
${pkg5}: patch
|
${pkg5}: patch
|
||||||
---
|
---
|
||||||
|
|
||||||
feat: Update packages in both groups with a feat
|
Update packages in both groups with a mix #2
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
await runCommandAsync(`git add ${join(versionPlansDir, 'bump-mixed1.md')}`);
|
await runCommandAsync(`git add ${join(versionPlansDir, 'bump-mixed1.md')}`);
|
||||||
@ -631,12 +634,12 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat
|
+ - **${pkg1}:** Update packages in both groups with a mix #1
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix`
|
+ - Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
|
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
|
||||||
@ -646,14 +649,9 @@ feat: Update packages in both groups with a feat
|
|||||||
+ ## 0.2.0 (YYYY-MM-DD)
|
+ ## 0.2.0 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
|
||||||
+
|
|
||||||
+ - Update packages in both groups with a feat
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix
|
+ - Update packages in both groups with a mix #2
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -666,7 +664,7 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🩹 Fixes
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a bug fix`
|
+ - **${pkg3}:** Update packages in both groups with a mix #1`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -679,7 +677,7 @@ feat: Update packages in both groups with a feat
|
|||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🚀 Features
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat`
|
+ - **${pkg4}:** Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result2WithoutDate).toContain(
|
expect(result2WithoutDate).toContain(
|
||||||
@ -690,9 +688,9 @@ feat: Update packages in both groups with a feat
|
|||||||
+ ## 0.0.1 (YYYY-MM-DD)
|
+ ## 0.0.1 (YYYY-MM-DD)
|
||||||
+
|
+
|
||||||
+
|
+
|
||||||
+ ### 🚀 Features
|
+ ### 🩹 Fixes
|
||||||
+
|
+
|
||||||
+ - Update packages in both groups with a feat`
|
+ - **${pkg5}:** Update packages in both groups with a mix #2`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
|
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
|
||||||
@ -715,7 +713,7 @@ feat: Update packages in both groups with a feat
|
|||||||
await ensureDir(versionPlansDir);
|
await ensureDir(versionPlansDir);
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
'release plan minor -m "feat: Update the fixed packages with a minor release." --verbose',
|
'release plan minor -m "Update the fixed packages with a minor release." --verbose',
|
||||||
{
|
{
|
||||||
silenceError: true,
|
silenceError: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -404,14 +404,30 @@ function formatChange(
|
|||||||
changelogRenderOptions: DefaultChangelogRenderOptions,
|
changelogRenderOptions: DefaultChangelogRenderOptions,
|
||||||
repoSlug?: RepoSlug
|
repoSlug?: RepoSlug
|
||||||
): string {
|
): 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');
|
||||||
|
}
|
||||||
|
|
||||||
let changeLine =
|
let changeLine =
|
||||||
'- ' +
|
'- ' +
|
||||||
(change.isBreaking ? '⚠️ ' : '') +
|
(change.isBreaking ? '⚠️ ' : '') +
|
||||||
(change.scope ? `**${change.scope.trim()}:** ` : '') +
|
(change.scope ? `**${change.scope.trim()}:** ` : '') +
|
||||||
change.description;
|
description;
|
||||||
if (repoSlug && changelogRenderOptions.commitReferences) {
|
if (repoSlug && changelogRenderOptions.commitReferences) {
|
||||||
changeLine += formatReferences(change.githubReferences, repoSlug);
|
changeLine += formatReferences(change.githubReferences, repoSlug);
|
||||||
}
|
}
|
||||||
|
if (extraLinesStr) {
|
||||||
|
changeLine += '\n\n' + extraLinesStr;
|
||||||
|
}
|
||||||
return changeLine;
|
return changeLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import * as chalk from 'chalk';
|
|||||||
import { prompt } from 'enquirer';
|
import { prompt } from 'enquirer';
|
||||||
import { removeSync } from 'fs-extra';
|
import { removeSync } from 'fs-extra';
|
||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { valid } from 'semver';
|
import { ReleaseType, valid } from 'semver';
|
||||||
import { dirSync } from 'tmp';
|
import { dirSync } from 'tmp';
|
||||||
import type { DependencyBump } from '../../../release/changelog-renderer';
|
import type { DependencyBump } from '../../../release/changelog-renderer';
|
||||||
import {
|
import {
|
||||||
@ -56,7 +56,6 @@ import {
|
|||||||
gitPush,
|
gitPush,
|
||||||
gitTag,
|
gitTag,
|
||||||
parseCommits,
|
parseCommits,
|
||||||
parseConventionalCommitsMessage,
|
|
||||||
} from './utils/git';
|
} from './utils/git';
|
||||||
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
|
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
|
||||||
import { launchEditor } from './utils/launch-editor';
|
import { launchEditor } from './utils/launch-editor';
|
||||||
@ -281,30 +280,37 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
const releaseGroup = releaseGroups[0];
|
const releaseGroup = releaseGroups[0];
|
||||||
if (releaseGroup.projectsRelationship === 'fixed') {
|
if (releaseGroup.projectsRelationship === 'fixed') {
|
||||||
const versionPlans = releaseGroup.versionPlans as GroupVersionPlan[];
|
const versionPlans = releaseGroup.versionPlans as GroupVersionPlan[];
|
||||||
workspaceChangelogChanges = filterHiddenChanges(
|
workspaceChangelogChanges = versionPlans
|
||||||
versionPlans
|
.flatMap((vp) => {
|
||||||
.map((vp) => {
|
const releaseType = versionPlanSemverReleaseTypeToChangelogType(
|
||||||
const parsedMessage = parseConventionalCommitsMessage(
|
vp.groupVersionBump
|
||||||
vp.message
|
);
|
||||||
);
|
const changes: ChangelogChange | ChangelogChange[] =
|
||||||
|
!vp.triggeredByProjects
|
||||||
// only properly formatted conventional commits messages will be included in the changelog
|
? {
|
||||||
if (!parsedMessage) {
|
type: releaseType.type,
|
||||||
return null;
|
scope: '',
|
||||||
}
|
description: vp.message,
|
||||||
|
body: '',
|
||||||
return <ChangelogChange>{
|
isBreaking: releaseType.isBreaking,
|
||||||
type: parsedMessage.type,
|
githubReferences: [],
|
||||||
scope: parsedMessage.scope,
|
affectedProjects: '*',
|
||||||
description: parsedMessage.description,
|
}
|
||||||
body: '',
|
: vp.triggeredByProjects.map((project) => {
|
||||||
isBreaking: parsedMessage.breaking,
|
return {
|
||||||
githubReferences: [],
|
type: releaseType.type,
|
||||||
};
|
scope: project,
|
||||||
})
|
description: vp.message,
|
||||||
.filter(Boolean),
|
body: '',
|
||||||
nxReleaseConfig.conventionalCommits
|
// TODO: what about github references?
|
||||||
);
|
isBreaking: releaseType.isBreaking,
|
||||||
|
githubReferences: [],
|
||||||
|
affectedProjects: [project],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changes;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -485,31 +491,26 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
let commits: GitCommit[];
|
let commits: GitCommit[];
|
||||||
|
|
||||||
if (releaseGroup.versionPlans) {
|
if (releaseGroup.versionPlans) {
|
||||||
changes = filterHiddenChanges(
|
changes = (releaseGroup.versionPlans as ProjectsVersionPlan[])
|
||||||
(releaseGroup.versionPlans as ProjectsVersionPlan[])
|
.map((vp) => {
|
||||||
.map((vp) => {
|
const bumpForProject = vp.projectVersionBumps[project.name];
|
||||||
const parsedMessage = parseConventionalCommitsMessage(
|
if (!bumpForProject) {
|
||||||
vp.message
|
return null;
|
||||||
);
|
}
|
||||||
|
const releaseType =
|
||||||
// only properly formatted conventional commits messages will be included in the changelog
|
versionPlanSemverReleaseTypeToChangelogType(bumpForProject);
|
||||||
if (!parsedMessage) {
|
return {
|
||||||
return null;
|
type: releaseType.type,
|
||||||
}
|
scope: project.name,
|
||||||
|
description: vp.message,
|
||||||
return {
|
body: '',
|
||||||
type: parsedMessage.type,
|
isBreaking: releaseType.isBreaking,
|
||||||
scope: parsedMessage.scope,
|
affectedProjects: Object.keys(vp.projectVersionBumps),
|
||||||
description: parsedMessage.description,
|
// TODO: can we include github references when using version plans?
|
||||||
body: '',
|
githubReferences: [],
|
||||||
isBreaking: parsedMessage.breaking,
|
};
|
||||||
affectedProjects: Object.keys(vp.projectVersionBumps),
|
})
|
||||||
githubReferences: [],
|
.filter(Boolean);
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean),
|
|
||||||
nxReleaseConfig.conventionalCommits
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let fromRef =
|
let fromRef =
|
||||||
args.from ||
|
args.from ||
|
||||||
@ -637,31 +638,37 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
// TODO: remove this after the changelog renderer is refactored to remove coupling with git commits
|
// TODO: remove this after the changelog renderer is refactored to remove coupling with git commits
|
||||||
let commits: GitCommit[] = [];
|
let commits: GitCommit[] = [];
|
||||||
if (releaseGroup.versionPlans) {
|
if (releaseGroup.versionPlans) {
|
||||||
changes = filterHiddenChanges(
|
changes = (releaseGroup.versionPlans as GroupVersionPlan[])
|
||||||
(releaseGroup.versionPlans as GroupVersionPlan[])
|
.flatMap((vp) => {
|
||||||
.map((vp) => {
|
const releaseType = versionPlanSemverReleaseTypeToChangelogType(
|
||||||
const parsedMessage = parseConventionalCommitsMessage(
|
vp.groupVersionBump
|
||||||
vp.message
|
);
|
||||||
);
|
const changes: ChangelogChange | ChangelogChange[] =
|
||||||
|
!vp.triggeredByProjects
|
||||||
// only properly formatted conventional commits messages will be included in the changelog
|
? {
|
||||||
if (!parsedMessage) {
|
type: releaseType.type,
|
||||||
return null;
|
scope: '',
|
||||||
}
|
description: vp.message,
|
||||||
|
body: '',
|
||||||
return <ChangelogChange>{
|
isBreaking: releaseType.isBreaking,
|
||||||
type: parsedMessage.type,
|
githubReferences: [],
|
||||||
scope: parsedMessage.scope,
|
affectedProjects: '*',
|
||||||
description: parsedMessage.description,
|
}
|
||||||
body: '',
|
: vp.triggeredByProjects.map((project) => {
|
||||||
isBreaking: parsedMessage.breaking,
|
return {
|
||||||
githubReferences: [],
|
type: releaseType.type,
|
||||||
affectedProjects: '*',
|
scope: project,
|
||||||
};
|
description: vp.message,
|
||||||
})
|
body: '',
|
||||||
.filter(Boolean),
|
// TODO: what about github references?
|
||||||
nxReleaseConfig.conventionalCommits
|
isBreaking: releaseType.isBreaking,
|
||||||
);
|
githubReferences: [],
|
||||||
|
affectedProjects: [project],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changes;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
} else {
|
} else {
|
||||||
let fromRef =
|
let fromRef =
|
||||||
args.from ||
|
args.from ||
|
||||||
@ -1408,3 +1415,23 @@ function createFileToProjectMap(
|
|||||||
}
|
}
|
||||||
return fileToProjectMap;
|
return fileToProjectMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function versionPlanSemverReleaseTypeToChangelogType(bump: ReleaseType): {
|
||||||
|
type: 'fix' | 'feat';
|
||||||
|
isBreaking: boolean;
|
||||||
|
} {
|
||||||
|
switch (bump) {
|
||||||
|
case 'premajor':
|
||||||
|
case 'major':
|
||||||
|
return { type: 'feat', isBreaking: true };
|
||||||
|
case 'preminor':
|
||||||
|
case 'minor':
|
||||||
|
return { type: 'feat', isBreaking: false };
|
||||||
|
case 'prerelease':
|
||||||
|
case 'prepatch':
|
||||||
|
case 'patch':
|
||||||
|
return { type: 'fix', isBreaking: false };
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid semver bump type: ${bump}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,3 +4,5 @@ pkg4: minor
|
|||||||
---
|
---
|
||||||
|
|
||||||
This is a change to packages 3 and 4
|
This is a change to packages 3 and 4
|
||||||
|
|
||||||
|
...and it includes multiple lines of text
|
||||||
|
|||||||
@ -75,7 +75,8 @@ describe('version-plans', () => {
|
|||||||
pkg1: patch,
|
pkg1: patch,
|
||||||
},
|
},
|
||||||
fileName: plan1.md,
|
fileName: plan1.md,
|
||||||
message: This is a change to just package 1,
|
message: This is a change to just package 1
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan1.md,
|
relativePath: .nx/version-plans/plan1.md,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -85,7 +86,8 @@ describe('version-plans', () => {
|
|||||||
pkg2: patch,
|
pkg2: patch,
|
||||||
},
|
},
|
||||||
fileName: plan2.md,
|
fileName: plan2.md,
|
||||||
message: This is a change to package 1 and package 2,
|
message: This is a change to package 1 and package 2
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan2.md,
|
relativePath: .nx/version-plans/plan2.md,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,7 +97,10 @@ describe('version-plans', () => {
|
|||||||
pkg4: minor,
|
pkg4: minor,
|
||||||
},
|
},
|
||||||
fileName: plan3.md,
|
fileName: plan3.md,
|
||||||
message: This is a change to packages 3 and 4,
|
message: This is a change to packages 3 and 4
|
||||||
|
|
||||||
|
...and it includes multiple lines of text
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan3.md,
|
relativePath: .nx/version-plans/plan3.md,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,7 +112,8 @@ describe('version-plans', () => {
|
|||||||
pkg6: preminor,
|
pkg6: preminor,
|
||||||
},
|
},
|
||||||
fileName: plan4.md,
|
fileName: plan4.md,
|
||||||
message: This is a change to packages 3, 4, 5, and 6,
|
message: This is a change to packages 3, 4, 5, and 6
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan4.md,
|
relativePath: .nx/version-plans/plan4.md,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -116,7 +122,8 @@ describe('version-plans', () => {
|
|||||||
fixed-group-1: minor,
|
fixed-group-1: minor,
|
||||||
},
|
},
|
||||||
fileName: plan5.md,
|
fileName: plan5.md,
|
||||||
message: This is a change to fixed-group-1,
|
message: This is a change to fixed-group-1
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan5.md,
|
relativePath: .nx/version-plans/plan5.md,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -127,7 +134,8 @@ describe('version-plans', () => {
|
|||||||
pkg3: major,
|
pkg3: major,
|
||||||
},
|
},
|
||||||
fileName: plan6.md,
|
fileName: plan6.md,
|
||||||
message: This is a major change to fixed-group-1 and pkg3 and a minor change to fixed-group-2,
|
message: This is a major change to fixed-group-1 and pkg3 and a minor change to fixed-group-2
|
||||||
|
,
|
||||||
relativePath: .nx/version-plans/plan6.md,
|
relativePath: .nx/version-plans/plan6.md,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -786,6 +794,11 @@ describe('version-plans', () => {
|
|||||||
groupVersionBump: patch,
|
groupVersionBump: patch,
|
||||||
message: plan1 message,
|
message: plan1 message,
|
||||||
relativePath: .nx/version-plans/plan1.md,
|
relativePath: .nx/version-plans/plan1.md,
|
||||||
|
triggeredByProjects: [
|
||||||
|
pkg1,
|
||||||
|
pkg2,
|
||||||
|
pkg3,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
absolutePath: <workspace-root>/version-plans/plan2.md,
|
absolutePath: <workspace-root>/version-plans/plan2.md,
|
||||||
@ -794,6 +807,11 @@ describe('version-plans', () => {
|
|||||||
groupVersionBump: minor,
|
groupVersionBump: minor,
|
||||||
message: plan2 message,
|
message: plan2 message,
|
||||||
relativePath: .nx/version-plans/plan2.md,
|
relativePath: .nx/version-plans/plan2.md,
|
||||||
|
triggeredByProjects: [
|
||||||
|
pkg1,
|
||||||
|
pkg2,
|
||||||
|
pkg3,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -1005,6 +1023,9 @@ describe('version-plans', () => {
|
|||||||
groupVersionBump: minor,
|
groupVersionBump: minor,
|
||||||
message: plan2 message,
|
message: plan2 message,
|
||||||
relativePath: .nx/version-plans/plan2.md,
|
relativePath: .nx/version-plans/plan2.md,
|
||||||
|
triggeredByProjects: [
|
||||||
|
pkg1,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -1026,6 +1047,9 @@ describe('version-plans', () => {
|
|||||||
groupVersionBump: minor,
|
groupVersionBump: minor,
|
||||||
message: plan2 message,
|
message: plan2 message,
|
||||||
relativePath: .nx/version-plans/plan2.md,
|
relativePath: .nx/version-plans/plan2.md,
|
||||||
|
triggeredByProjects: [
|
||||||
|
pkg2,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -1047,6 +1071,9 @@ describe('version-plans', () => {
|
|||||||
groupVersionBump: minor,
|
groupVersionBump: minor,
|
||||||
message: plan2 message,
|
message: plan2 message,
|
||||||
relativePath: .nx/version-plans/plan2.md,
|
relativePath: .nx/version-plans/plan2.md,
|
||||||
|
triggeredByProjects: [
|
||||||
|
pkg3,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,6 +25,11 @@ export interface VersionPlan extends VersionPlanFile {
|
|||||||
|
|
||||||
export interface GroupVersionPlan extends VersionPlan {
|
export interface GroupVersionPlan extends VersionPlan {
|
||||||
groupVersionBump: ReleaseType;
|
groupVersionBump: ReleaseType;
|
||||||
|
/**
|
||||||
|
* Will not be set if the group name was the trigger, otherwise will be a list of
|
||||||
|
* all the individual project names explicitly found in the version plan file.
|
||||||
|
*/
|
||||||
|
triggeredByProjects?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectsVersionPlan extends VersionPlan {
|
export interface ProjectsVersionPlan extends VersionPlan {
|
||||||
@ -54,7 +59,7 @@ export async function readRawVersionPlans(): Promise<RawVersionPlan[]> {
|
|||||||
relativePath: join(versionPlansDirectory, versionPlanFile),
|
relativePath: join(versionPlansDirectory, versionPlanFile),
|
||||||
fileName: versionPlanFile,
|
fileName: versionPlanFile,
|
||||||
content: parsedContent.attributes,
|
content: parsedContent.attributes,
|
||||||
message: getSingleLineMessage(parsedContent.body),
|
message: parsedContent.body,
|
||||||
createdOnMs: versionPlanStats.birthtimeMs,
|
createdOnMs: versionPlanStats.birthtimeMs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,6 +79,12 @@ export function setVersionPlansOnGroups(
|
|||||||
const isDefaultGroup = isDefault(releaseGroups);
|
const isDefaultGroup = isDefault(releaseGroups);
|
||||||
|
|
||||||
for (const rawVersionPlan of rawVersionPlans) {
|
for (const rawVersionPlan of rawVersionPlans) {
|
||||||
|
if (!rawVersionPlan.message) {
|
||||||
|
throw new Error(
|
||||||
|
`Please add a changelog message to version plan: '${rawVersionPlan.fileName}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(rawVersionPlan.content)) {
|
for (const [key, value] of Object.entries(rawVersionPlan.content)) {
|
||||||
if (groupsByName.has(key)) {
|
if (groupsByName.has(key)) {
|
||||||
const group = groupsByName.get(key);
|
const group = groupsByName.get(key);
|
||||||
@ -232,6 +243,8 @@ export function setVersionPlansOnGroups(
|
|||||||
`Found a version bump for project '${key}' in '${rawVersionPlan.fileName}' that conflicts with another project's version bump in the same release group '${groupForProject.name}'. When the group is in fixed versioning mode, all projects' version bumps within the same group must match.`
|
`Found a version bump for project '${key}' in '${rawVersionPlan.fileName}' that conflicts with another project's version bump in the same release group '${groupForProject.name}'. When the group is in fixed versioning mode, all projects' version bumps within the same group must match.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
existingPlan.triggeredByProjects.push(key);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
groupForProject.versionPlans.push(<GroupVersionPlan>{
|
groupForProject.versionPlans.push(<GroupVersionPlan>{
|
||||||
@ -241,7 +254,9 @@ export function setVersionPlansOnGroups(
|
|||||||
createdOnMs: rawVersionPlan.createdOnMs,
|
createdOnMs: rawVersionPlan.createdOnMs,
|
||||||
message: rawVersionPlan.message,
|
message: rawVersionPlan.message,
|
||||||
// This is a fixed group, so the version bump is for the group, even if a project within it was specified
|
// This is a fixed group, so the version bump is for the group, even if a project within it was specified
|
||||||
|
// but we track the projects that triggered the version bump so that we can accurately produce changelog entries.
|
||||||
groupVersionBump: value,
|
groupVersionBump: value,
|
||||||
|
triggeredByProjects: [key],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,8 +288,3 @@ export function getVersionPlansAbsolutePath() {
|
|||||||
function isReleaseType(value: string): value is ReleaseType {
|
function isReleaseType(value: string): value is ReleaseType {
|
||||||
return RELEASE_TYPES.includes(value as ReleaseType);
|
return RELEASE_TYPES.includes(value as ReleaseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// changelog messages may only be a single line long, so ignore anything else
|
|
||||||
function getSingleLineMessage(message: string) {
|
|
||||||
return message.trim().split('\n')[0];
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { prompt } from 'enquirer';
|
import { prompt } from 'enquirer';
|
||||||
import { ensureDir, writeFile } from 'fs-extra';
|
import { ensureDir, readFileSync, writeFile, writeFileSync } from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { join } from 'node:path';
|
||||||
import { RELEASE_TYPES } from 'semver';
|
import { RELEASE_TYPES } from 'semver';
|
||||||
|
import { dirSync } from 'tmp';
|
||||||
import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
|
import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
|
||||||
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
|
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
|
||||||
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
@ -13,13 +14,13 @@ import {
|
|||||||
createNxReleaseConfig,
|
createNxReleaseConfig,
|
||||||
handleNxReleaseConfigError,
|
handleNxReleaseConfigError,
|
||||||
} from './config/config';
|
} from './config/config';
|
||||||
|
import { deepMergeJson } from './config/deep-merge-json';
|
||||||
import { filterReleaseGroups } from './config/filter-release-groups';
|
import { filterReleaseGroups } from './config/filter-release-groups';
|
||||||
import { getVersionPlansAbsolutePath } from './config/version-plans';
|
import { getVersionPlansAbsolutePath } from './config/version-plans';
|
||||||
import { generateVersionPlanContent } from './utils/generate-version-plan-content';
|
import { generateVersionPlanContent } from './utils/generate-version-plan-content';
|
||||||
import { parseConventionalCommitsMessage } from './utils/git';
|
import { launchEditor } from './utils/launch-editor';
|
||||||
import { printDiff } from './utils/print-changes';
|
import { printDiff } from './utils/print-changes';
|
||||||
import { printConfigAndExit } from './utils/print-config';
|
import { printConfigAndExit } from './utils/print-config';
|
||||||
import { deepMergeJson } from './config/deep-merge-json';
|
|
||||||
|
|
||||||
export const releasePlanCLIHandler = (args: PlanOptions) =>
|
export const releasePlanCLIHandler = (args: PlanOptions) =>
|
||||||
handleErrors(args.verbose, () => createAPI({})(args));
|
handleErrors(args.verbose, () => createAPI({})(args));
|
||||||
@ -79,26 +80,6 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (args.message) {
|
|
||||||
const message = parseConventionalCommitsMessage(args.message);
|
|
||||||
if (!message) {
|
|
||||||
output.error({
|
|
||||||
title: 'Changelog message is not in conventional commits format.',
|
|
||||||
bodyLines: [
|
|
||||||
'Please ensure your message is in the form of:',
|
|
||||||
' type(optional scope): description',
|
|
||||||
'',
|
|
||||||
'For example:',
|
|
||||||
' feat(pkg-b): add new feature',
|
|
||||||
' fix(pkg-a): correct a bug',
|
|
||||||
' chore: update build process',
|
|
||||||
' fix(core)!: breaking change in core package',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releaseGroups[0].name === IMPLICIT_DEFAULT_RELEASE_GROUP) {
|
if (releaseGroups[0].name === IMPLICIT_DEFAULT_RELEASE_GROUP) {
|
||||||
const group = releaseGroups[0];
|
const group = releaseGroups[0];
|
||||||
if (group.projectsRelationship === 'independent') {
|
if (group.projectsRelationship === 'independent') {
|
||||||
@ -153,12 +134,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionPlanMessage = args.message || (await promptForMessage());
|
const versionPlanName = `version-plan-${new Date().getTime()}`;
|
||||||
|
const versionPlanMessage =
|
||||||
|
args.message || (await promptForMessage(versionPlanName));
|
||||||
const versionPlanFileContent = generateVersionPlanContent(
|
const versionPlanFileContent = generateVersionPlanContent(
|
||||||
versionPlanBumps,
|
versionPlanBumps,
|
||||||
versionPlanMessage
|
versionPlanMessage
|
||||||
);
|
);
|
||||||
const versionPlanFileName = `version-plan-${new Date().getTime()}.md`;
|
const versionPlanFileName = `${versionPlanName}.md`;
|
||||||
|
|
||||||
if (args.dryRun) {
|
if (args.dryRun) {
|
||||||
output.logSingleLine(
|
output.logSingleLine(
|
||||||
@ -202,47 +185,49 @@ async function promptForVersion(message: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptForMessage(): Promise<string> {
|
async function promptForMessage(versionPlanName: string): Promise<string> {
|
||||||
let message: string;
|
let message: string;
|
||||||
do {
|
do {
|
||||||
message = await _promptForMessage();
|
message = await _promptForMessage(versionPlanName);
|
||||||
} while (!message);
|
} while (!message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: support non-conventional commits messages (will require significant changelog renderer changes)
|
async function _promptForMessage(versionPlanName: string): Promise<string> {
|
||||||
async function _promptForMessage(): Promise<string> {
|
|
||||||
try {
|
try {
|
||||||
const reply = await prompt<{ message: string }>([
|
const reply = await prompt<{ message: string }>([
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
message:
|
message:
|
||||||
'What changelog message would you like associated with this change?',
|
'What changelog message would you like associated with this change? (Leave blank to open an external editor for multi-line messages/easier editing)',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const conventionalCommitsMessage = parseConventionalCommitsMessage(
|
let message = reply.message.trim();
|
||||||
reply.message
|
|
||||||
);
|
if (!message.length) {
|
||||||
if (!conventionalCommitsMessage) {
|
const tmpDir = dirSync().name;
|
||||||
output.warn({
|
const messageFilePath = join(
|
||||||
title: 'Changelog message is not in conventional commits format.',
|
tmpDir,
|
||||||
bodyLines: [
|
`DRAFT_MESSAGE__${versionPlanName}.md`
|
||||||
'Please ensure your message is in the form of:',
|
);
|
||||||
' type(optional scope): description',
|
writeFileSync(messageFilePath, '');
|
||||||
'',
|
await launchEditor(messageFilePath);
|
||||||
'For example:',
|
message = readFileSync(messageFilePath, 'utf-8');
|
||||||
' feat(pkg-b): add new feature',
|
|
||||||
' fix(pkg-a): correct a bug',
|
|
||||||
' chore: update build process',
|
|
||||||
' fix(core)!: breaking change in core package',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.message;
|
message = message.trim();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
output.warn({
|
||||||
|
title:
|
||||||
|
'A changelog message is required in order to create the version plan file',
|
||||||
|
bodyLines: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
output.log({
|
output.log({
|
||||||
title: 'Cancelled version plan creation.',
|
title: 'Cancelled version plan creation.',
|
||||||
|
|||||||
@ -29,4 +29,28 @@ describe('generateVersionPlanContent()', () => {
|
|||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work without a message', () => {
|
||||||
|
expect(generateVersionPlanContent({ proj: '1.0.0' }, ''))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"---
|
||||||
|
proj: 1.0.0
|
||||||
|
---
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multi-line messages', () => {
|
||||||
|
expect(generateVersionPlanContent({ proj: '1.0.0' }, 'foo\nbar\nbaz'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"---
|
||||||
|
proj: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
baz
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export function generateVersionPlanContent(
|
|||||||
bumps: Record<string, string>,
|
bumps: Record<string, string>,
|
||||||
message: string
|
message: string
|
||||||
): string {
|
): string {
|
||||||
return `---
|
const frontMatter = `---
|
||||||
${Object.entries(bumps)
|
${Object.entries(bumps)
|
||||||
.filter(([_, version]) => version !== 'none')
|
.filter(([_, version]) => version !== 'none')
|
||||||
.map(([projectOrGroup, version]) => {
|
.map(([projectOrGroup, version]) => {
|
||||||
@ -15,7 +15,7 @@ ${Object.entries(bumps)
|
|||||||
})
|
})
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
---
|
---
|
||||||
|
|
||||||
${message}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
return `${frontMatter}${message ? `\n${message}\n` : ''}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user