diff --git a/README.md b/README.md index f4905bdb..287bc6fd 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ Create or update the changelog file in the local project repository. ### Options -| Options | Description | Default | -|-----------------|-----------------------------|----------------| -| `changelogFile` | File path of the changelog. | `CHANGELOG.md` | +| Options | Description | Default | +|------------------|-----------------------------|----------------| +| `changelogFile` | File path of the changelog. | `CHANGELOG.md` | +| `changelogTitle` | Title in the changelog. | None | + ### Usage diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index e92d7c99..cc96fe6f 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -13,4 +13,12 @@ module.exports = { Your configuration for the \`assets\` option is \`${changelogFile}\`.`, }), + EINVALIDCHANGELOGTITLE: ({changelogTitle}) => ({ + message: 'Invalid `changelogTitle` option.', + details: `The [changelogTitle option](${linkify( + 'README.md#options' + )}) option, if defined, must be a non empty \`String\`. + +Your configuration for the \`changelogTitle\` option is \`${changelogTitle}\`.`, + }), }; diff --git a/lib/prepare.js b/lib/prepare.js index 753ebe8b..352a335c 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -2,7 +2,7 @@ const {readFile, writeFile, ensureFile} = require('fs-extra'); const resolveConfig = require('./resolve-config'); module.exports = async (pluginConfig, notes, logger) => { - const {changelogFile} = resolveConfig(pluginConfig); + const {changelogFile, changelogTitle} = resolveConfig(pluginConfig); if (notes) { await ensureFile(changelogFile); @@ -13,6 +13,13 @@ module.exports = async (pluginConfig, notes, logger) => { } else { logger.log('Create %s', changelogFile); } - await writeFile(changelogFile, `${notes.trim()}\n${currentFile ? `\n${currentFile}\n` : ''}`); + + const currentContent = + changelogTitle && currentFile.startsWith(changelogTitle) + ? currentFile.slice(changelogTitle.length).trim() + : currentFile; + const content = `${notes.trim()}\n${currentContent ? `\n${currentContent}\n` : ''}`; + + await writeFile(changelogFile, changelogTitle ? `${changelogTitle}\n\n${content}` : content); } }; diff --git a/lib/resolve-config.js b/lib/resolve-config.js index 29416659..ca305a6c 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -1,5 +1,6 @@ const {isUndefined} = require('lodash'); -module.exports = ({changelogFile}) => ({ +module.exports = ({changelogFile, changelogTitle}) => ({ changelogFile: isUndefined(changelogFile) ? 'CHANGELOG.md' : changelogFile, + changelogTitle, }); diff --git a/lib/verify.js b/lib/verify.js index d942de51..b6b9e507 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -4,13 +4,17 @@ const getError = require('./get-error'); const resolveConfig = require('./resolve-config'); module.exports = pluginConfig => { - const {changelogFile} = resolveConfig(pluginConfig); + const {changelogFile, changelogTitle} = resolveConfig(pluginConfig); const errors = []; if (!isUndefined(changelogFile) && !(isString(changelogFile) && changelogFile.trim())) { errors.push(getError('EINVALIDCHANGELOGFILE', {changelogFile})); } + if (changelogTitle && !(isString(changelogTitle) && changelogTitle.trim())) { + errors.push(getError('EINVALIDCHANGELOGTITLE', {changelogTitle})); + } + if (errors.length > 0) { throw new AggregateError(errors); } diff --git a/test/prepare.test.js b/test/prepare.test.js index b4791322..e7f20e3c 100644 --- a/test/prepare.test.js +++ b/test/prepare.test.js @@ -53,3 +53,33 @@ test.serial('Prepend the CHANGELOG.md if there is an existing one', async t => { t.is((await readFile('CHANGELOG.md')).toString(), `${notes}\n\nInitial CHANGELOG\n`); t.deepEqual(t.context.log.args[0], ['Update %s', 'CHANGELOG.md']); }); + +test.serial('Prepend title in the CHANGELOG.md if there is none', async t => { + const notes = 'Test release note'; + await outputFile('CHANGELOG.md', 'Initial CHANGELOG'); + + const changelogTitle = '# My Changelog Title'; + await prepare({changelogTitle}, notes, t.context.logger); + + t.is((await readFile('CHANGELOG.md')).toString(), `${changelogTitle}\n\n${notes}\n\nInitial CHANGELOG\n`); +}); + +test.serial('Keep the title at the top of the CHANGELOG.md', async t => { + const notes = 'Test release note'; + const changelogTitle = '# My Changelog Title'; + await outputFile('CHANGELOG.md', `${changelogTitle}\n\nInitial CHANGELOG`); + + await prepare({changelogTitle}, notes, t.context.logger); + + t.is((await readFile('CHANGELOG.md')).toString(), `${changelogTitle}\n\n${notes}\n\nInitial CHANGELOG\n`); +}); + +test.serial('Create new changelog with title if specified', async t => { + const notes = 'Test release note'; + const changelogTitle = '# My Changelog Title'; + const changelogFile = 'HISTORY.md'; + + await prepare({changelogTitle, changelogFile}, notes, t.context.logger); + + t.is((await readFile(changelogFile)).toString(), `${changelogTitle}\n\n${notes}\n`); +}); diff --git a/test/verify.test.js b/test/verify.test.js index e7e1e419..6914ca91 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -1,12 +1,13 @@ import test from 'ava'; import verify from '../lib/verify'; -test.serial('Verify String "changelogFile"', t => { +test.serial('Verify String "changelogFile" and "chagngelogTitle"', t => { const changelogFile = 'docs/changelog.txt'; - t.notThrows(() => verify({changelogFile})); + const changelogTitle = '# My title here'; + t.notThrows(() => verify({changelogFile, changelogTitle})); }); -test.serial('Verify undefined "changelogFile"', t => { +test.serial('Verify undefined "changelogFile" and "chagngelogTitle"', t => { t.notThrows(() => verify({})); }); @@ -33,3 +34,19 @@ test('Throw SemanticReleaseError if "changelogFile" option is a whitespace Strin t.is(error.name, 'SemanticReleaseError'); t.is(error.code, 'EINVALIDCHANGELOGFILE'); }); + +test('Throw SemanticReleaseError if "changelogTitle" option is not a String', t => { + const changelogTitle = 42; + const [error] = t.throws(() => verify({changelogTitle})); + + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDCHANGELOGTITLE'); +}); + +test('Throw SemanticReleaseError if "changelogTitle" option is a whitespace String', t => { + const changelogTitle = ' \n \r '; + const [error] = t.throws(() => verify({changelogTitle})); + + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDCHANGELOGTITLE'); +});