feat: allow to define multiple generateNotes
plugins
Each `generateNotes` plugin will be called in the order defined and will receive the concatenation of the previous one in `nextRelease.notes`. That gives each plugin the ability to test if there is a notes part that will precede it's own. Each plugin is expected to return it's own part of the release notes only. **semantic-release** will take care of concatenating all the notes parts.
This commit is contained in:
parent
576eb6027f
commit
5989989452
@ -167,13 +167,13 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
|||||||
|
|
||||||
### generateNotes
|
### generateNotes
|
||||||
|
|
||||||
Type: `String`, `Object`
|
Type: `Array`, `String`, `Object`
|
||||||
|
|
||||||
Default: `['@semantic-release/release-notes-generator']`
|
Default: `['@semantic-release/release-notes-generator']`
|
||||||
|
|
||||||
CLI argument: `--generate-notes`
|
CLI argument: `--generate-notes`
|
||||||
|
|
||||||
Define the [generate notes plugin](plugins.md#generatenotes-plugin).
|
Define the [generate notes plugins](plugins.md#generatenotes-plugin).
|
||||||
|
|
||||||
See [Plugins configuration](plugins.md#configuration) for more details.
|
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ Default implementation: none.
|
|||||||
|
|
||||||
### generateNotes plugin
|
### generateNotes plugin
|
||||||
|
|
||||||
Responsible for generating release notes.
|
Responsible for generating release notes. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of plugin output.
|
||||||
|
|
||||||
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
|
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
|
||||||
|
|
||||||
|
41
index.js
41
index.js
@ -15,7 +15,7 @@ const getGitAuthUrl = require('./lib/get-git-auth-url');
|
|||||||
const logger = require('./lib/logger');
|
const logger = require('./lib/logger');
|
||||||
const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
|
const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
|
||||||
const getError = require('./lib/get-error');
|
const getError = require('./lib/get-error');
|
||||||
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
const {COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR} = require('./lib/definitions/constants');
|
||||||
|
|
||||||
marked.setOptions({renderer: new TerminalRenderer()});
|
marked.setOptions({renderer: new TerminalRenderer()});
|
||||||
|
|
||||||
@ -101,14 +101,34 @@ async function run(options, plugins) {
|
|||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
logger.log('Call plugin %s', 'generate-notes');
|
logger.log('Call plugin %s', 'generate-notes');
|
||||||
const [notes] = await plugins.generateNotes(generateNotesParam);
|
const notes = (await plugins.generateNotes(generateNotesParam, {
|
||||||
|
getNextInput: ({nextRelease, ...generateNotesParam}, notes) => ({
|
||||||
|
...generateNotesParam,
|
||||||
|
nextRelease: {
|
||||||
|
...nextRelease,
|
||||||
|
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(RELEASE_NOTES_SEPARATOR);
|
||||||
logger.log('Release note for version %s:\n', nextRelease.version);
|
logger.log('Release note for version %s:\n', nextRelease.version);
|
||||||
if (notes) {
|
if (notes) {
|
||||||
process.stdout.write(`${marked(notes)}\n`);
|
process.stdout.write(`${marked(notes)}\n`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.log('Call plugin %s', 'generateNotes');
|
logger.log('Call plugin %s', 'generateNotes');
|
||||||
[nextRelease.notes] = await plugins.generateNotes(generateNotesParam);
|
nextRelease.notes = (await plugins.generateNotes(generateNotesParam, {
|
||||||
|
getNextInput: ({nextRelease, ...generateNotesParam}, notes) => ({
|
||||||
|
...generateNotesParam,
|
||||||
|
nextRelease: {
|
||||||
|
...nextRelease,
|
||||||
|
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(RELEASE_NOTES_SEPARATOR);
|
||||||
|
|
||||||
logger.log('Call plugin %s', 'prepare');
|
logger.log('Call plugin %s', 'prepare');
|
||||||
await plugins.prepare(
|
await plugins.prepare(
|
||||||
@ -121,7 +141,20 @@ async function run(options, plugins) {
|
|||||||
nextRelease.gitHead = newGitHead;
|
nextRelease.gitHead = newGitHead;
|
||||||
// Regenerate the release notes
|
// Regenerate the release notes
|
||||||
logger.log('Call plugin %s', 'generateNotes');
|
logger.log('Call plugin %s', 'generateNotes');
|
||||||
[nextRelease.notes] = await plugins.generateNotes({nextRelease, ...prepareParam});
|
nextRelease.notes = (await plugins.generateNotes(
|
||||||
|
{nextRelease, ...prepareParam},
|
||||||
|
{
|
||||||
|
getNextInput: ({nextRelease, ...generateNotesParam}, notes) => ({
|
||||||
|
...generateNotesParam,
|
||||||
|
nextRelease: {
|
||||||
|
...nextRelease,
|
||||||
|
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(RELEASE_NOTES_SEPARATOR);
|
||||||
}
|
}
|
||||||
// Call the next publish plugin with the updated `nextRelease`
|
// Call the next publish plugin with the updated `nextRelease`
|
||||||
return {...prepareParam, nextRelease};
|
return {...prepareParam, nextRelease};
|
||||||
|
@ -6,4 +6,6 @@ const COMMIT_NAME = 'semantic-release-bot';
|
|||||||
|
|
||||||
const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
|
const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
|
||||||
|
|
||||||
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL};
|
const RELEASE_NOTES_SEPARATOR = '\n\n';
|
||||||
|
|
||||||
|
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR};
|
||||||
|
@ -18,8 +18,8 @@ module.exports = {
|
|||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||||
},
|
},
|
||||||
generateNotes: {
|
generateNotes: {
|
||||||
default: '@semantic-release/release-notes-generator',
|
default: ['@semantic-release/release-notes-generator'],
|
||||||
configValidator: conf => !conf || validatePluginConfig(conf),
|
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||||
outputValidator: output => !output || isString(output),
|
outputValidator: output => !output || isString(output),
|
||||||
},
|
},
|
||||||
prepare: {
|
prepare: {
|
||||||
|
@ -34,15 +34,15 @@ test('The "verifyRelease" plugin, if defined, must be a single or an array of pl
|
|||||||
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
|
test('The "generateNotes" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||||
t.false(plugins.generateNotes.configValidator({}));
|
t.false(plugins.generateNotes.configValidator({}));
|
||||||
t.false(plugins.generateNotes.configValidator({path: null}));
|
t.false(plugins.generateNotes.configValidator({path: null}));
|
||||||
t.false(plugins.generateNotes.configValidator([]));
|
|
||||||
|
|
||||||
t.true(plugins.generateNotes.configValidator());
|
|
||||||
t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'}));
|
t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'}));
|
||||||
|
t.true(plugins.generateNotes.configValidator());
|
||||||
t.true(plugins.generateNotes.configValidator('plugin-path.js'));
|
t.true(plugins.generateNotes.configValidator('plugin-path.js'));
|
||||||
t.true(plugins.generateNotes.configValidator(() => {}));
|
t.true(plugins.generateNotes.configValidator(() => {}));
|
||||||
|
t.true(plugins.generateNotes.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
|
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||||
|
@ -61,12 +61,16 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
|
|
||||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||||
const notes = 'Release notes';
|
const notes1 = 'Release notes 1';
|
||||||
|
const notes2 = 'Release notes 2';
|
||||||
|
const notes3 = 'Release notes 3';
|
||||||
const verifyConditions1 = stub().resolves();
|
const verifyConditions1 = stub().resolves();
|
||||||
const verifyConditions2 = stub().resolves();
|
const verifyConditions2 = stub().resolves();
|
||||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||||
const verifyRelease = stub().resolves();
|
const verifyRelease = stub().resolves();
|
||||||
const generateNotes = stub().resolves(notes);
|
const generateNotes1 = stub().resolves(notes1);
|
||||||
|
const generateNotes2 = stub().resolves(notes2);
|
||||||
|
const generateNotes3 = stub().resolves(notes3);
|
||||||
const release1 = {name: 'Release 1', url: 'https://release1.com'};
|
const release1 = {name: 'Release 1', url: 'https://release1.com'};
|
||||||
const prepare = stub().resolves();
|
const prepare = stub().resolves();
|
||||||
const publish1 = stub().resolves(release1);
|
const publish1 = stub().resolves(release1);
|
||||||
@ -78,7 +82,7 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||||
analyzeCommits,
|
analyzeCommits,
|
||||||
verifyRelease,
|
verifyRelease,
|
||||||
generateNotes,
|
generateNotes: [generateNotes1, generateNotes2, generateNotes3],
|
||||||
prepare,
|
prepare,
|
||||||
publish: [publish1, pluginNoop],
|
publish: [publish1, pluginNoop],
|
||||||
success,
|
success,
|
||||||
@ -113,14 +117,32 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
|
t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
|
||||||
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
|
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
|
||||||
|
|
||||||
t.is(generateNotes.callCount, 1);
|
t.is(generateNotes1.callCount, 1);
|
||||||
t.deepEqual(generateNotes.args[0][0], config);
|
t.deepEqual(generateNotes1.args[0][0], config);
|
||||||
t.deepEqual(generateNotes.args[0][1].options, options);
|
t.deepEqual(generateNotes1.args[0][1].options, options);
|
||||||
t.deepEqual(generateNotes.args[0][1].logger, t.context.logger);
|
t.deepEqual(generateNotes1.args[0][1].logger, t.context.logger);
|
||||||
t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(generateNotes1.args[0][1].lastRelease, lastRelease);
|
||||||
t.deepEqual(generateNotes.args[0][1].commits[0].hash, commits[0].hash);
|
t.deepEqual(generateNotes1.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
|
t.deepEqual(generateNotes1.args[0][1].commits[0].message, commits[0].message);
|
||||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
t.deepEqual(generateNotes1.args[0][1].nextRelease, nextRelease);
|
||||||
|
|
||||||
|
t.is(generateNotes2.callCount, 1);
|
||||||
|
t.deepEqual(generateNotes2.args[0][0], config);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].options, options);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].logger, t.context.logger);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].lastRelease, lastRelease);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].commits[0].message, commits[0].message);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].nextRelease, {...nextRelease, notes: notes1});
|
||||||
|
|
||||||
|
t.is(generateNotes3.callCount, 1);
|
||||||
|
t.deepEqual(generateNotes3.args[0][0], config);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].options, options);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].logger, t.context.logger);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].lastRelease, lastRelease);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].commits[0].message, commits[0].message);
|
||||||
|
t.deepEqual(generateNotes3.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}`});
|
||||||
|
|
||||||
t.is(prepare.callCount, 1);
|
t.is(prepare.callCount, 1);
|
||||||
t.deepEqual(prepare.args[0][0], config);
|
t.deepEqual(prepare.args[0][0], config);
|
||||||
@ -129,7 +151,7 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
|
||||||
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
|
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
|
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
|
||||||
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
|
||||||
|
|
||||||
t.is(publish1.callCount, 1);
|
t.is(publish1.callCount, 1);
|
||||||
t.deepEqual(publish1.args[0][0], config);
|
t.deepEqual(publish1.args[0][0], config);
|
||||||
@ -138,7 +160,7 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
t.deepEqual(publish1.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(publish1.args[0][1].lastRelease, lastRelease);
|
||||||
t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash);
|
t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message);
|
t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message);
|
||||||
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
|
||||||
|
|
||||||
t.is(success.callCount, 1);
|
t.is(success.callCount, 1);
|
||||||
t.deepEqual(success.args[0][0], config);
|
t.deepEqual(success.args[0][0], config);
|
||||||
@ -147,10 +169,10 @@ test.serial('Plugins are called with expected values', async t => {
|
|||||||
t.deepEqual(success.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(success.args[0][1].lastRelease, lastRelease);
|
||||||
t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash);
|
t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash);
|
||||||
t.deepEqual(success.args[0][1].commits[0].message, commits[0].message);
|
t.deepEqual(success.args[0][1].commits[0].message, commits[0].message);
|
||||||
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
|
||||||
t.deepEqual(success.args[0][1].releases, [
|
t.deepEqual(success.args[0][1].releases, [
|
||||||
{...release1, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
{...release1, ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: proxy]'},
|
||||||
{...nextRelease, ...{notes}, ...{pluginName: pluginNoop}},
|
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||||
@ -625,7 +647,9 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
|||||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||||
const verifyRelease = stub().resolves();
|
const verifyRelease = stub().resolves();
|
||||||
const generateNotes = stub().resolves();
|
const generateNotes1 = stub().resolves();
|
||||||
|
const notes2 = 'Release notes 2';
|
||||||
|
const generateNotes2 = stub().resolves(notes2);
|
||||||
const publish = stub().resolves();
|
const publish = stub().resolves();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@ -634,7 +658,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
|||||||
verifyConditions: stub().resolves(),
|
verifyConditions: stub().resolves(),
|
||||||
analyzeCommits,
|
analyzeCommits,
|
||||||
verifyRelease,
|
verifyRelease,
|
||||||
generateNotes,
|
generateNotes: [generateNotes1, generateNotes2],
|
||||||
prepare: stub().resolves(),
|
prepare: stub().resolves(),
|
||||||
publish,
|
publish,
|
||||||
success: stub().resolves(),
|
success: stub().resolves(),
|
||||||
@ -653,12 +677,15 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
|||||||
t.is(verifyRelease.callCount, 1);
|
t.is(verifyRelease.callCount, 1);
|
||||||
t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
|
||||||
|
|
||||||
t.is(generateNotes.callCount, 1);
|
t.is(generateNotes1.callCount, 1);
|
||||||
t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(generateNotes1.args[0][1].lastRelease, lastRelease);
|
||||||
|
|
||||||
|
t.is(generateNotes2.callCount, 1);
|
||||||
|
t.deepEqual(generateNotes2.args[0][1].lastRelease, lastRelease);
|
||||||
|
|
||||||
t.is(publish.callCount, 1);
|
t.is(publish.callCount, 1);
|
||||||
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
|
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
|
||||||
t.falsy(publish.args[0][1].nextRelease.notes);
|
t.is(publish.args[0][1].nextRelease.notes, notes2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('Returns falsy value if triggered by a PR', async t => {
|
test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user