From abdcfe295023f46c8724463940fff6a220434fad Mon Sep 17 00:00:00 2001 From: Tim Knight Date: Tue, 31 Oct 2023 10:13:31 +0000 Subject: [PATCH] fix: preserve frontmatter when updating changelog (#108) * chore: add failing test for retaining frontmatter Signed-off-by: Tim Knight * fix: preserve frontmatter when updating changelog fixes #106 Signed-off-by: Tim Knight * chore: add tests for upgrade path from standard-version Signed-off-by: Tim Knight * fix: preserve front matter when swapping from standard-version - Matches based off # Changelog title position rather than new header position Signed-off-by: Tim Knight * chore: run linting Signed-off-by: Tim Knight * chore: refactoring, extract functions Signed-off-by: Tim Knight * chore: front matter usually on separate lines to title Signed-off-by: Tim Knight * chore: apply eslint . --fix Signed-off-by: Tim Knight --------- Signed-off-by: Tim Knight --- lib/lifecycles/changelog.js | 35 +++++++++++++++++----- test/core.spec.js | 60 +++++++++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/lib/lifecycles/changelog.js b/lib/lifecycles/changelog.js index 6d4ba990a..5bdd573ac 100644 --- a/lib/lifecycles/changelog.js +++ b/lib/lifecycles/changelog.js @@ -18,17 +18,38 @@ Changelog.START_OF_LAST_RELEASE_PATTERN = START_OF_LAST_RELEASE_PATTERN module.exports = Changelog +/** + * Front matter is only extracted and therefore retained in final output where Changelog "header" begins with #Changelog, + * e.g. meets Standard-Version (last release) or commit-and-tag-version(current) format + */ +function extractFrontMatter (oldContent) { + const headerStart = oldContent.indexOf('# Changelog') + return headerStart !== -1 || headerStart !== 0 + ? oldContent.substring(0, headerStart) + : '' +} + +/** + * find the position of the last release and remove header + */ +function extractChangelogBody (oldContent) { + const oldContentStart = oldContent.search(START_OF_LAST_RELEASE_PATTERN) + return oldContentStart !== -1 + ? oldContent.substring(oldContentStart) + : oldContent +} + function outputChangelog (args, newVersion) { return new Promise((resolve, reject) => { createIfMissing(args) const header = args.header - let oldContent = args.dryRun || args.releaseCount === 0 ? '' : fs.readFileSync(args.infile, 'utf-8') - const oldContentStart = oldContent.search(START_OF_LAST_RELEASE_PATTERN) - // find the position of the last release and remove header: - if (oldContentStart !== -1) { - oldContent = oldContent.substring(oldContentStart) - } + const oldContent = args.dryRun || args.releaseCount === 0 ? '' : fs.readFileSync(args.infile, 'utf-8') + + const oldContentBody = extractChangelogBody(oldContent) + + const changelogFrontMatter = extractFrontMatter(oldContent) + let content = '' const context = { version: newVersion } const changelogStream = conventionalChangelog({ @@ -48,7 +69,7 @@ function outputChangelog (args, newVersion) { changelogStream.on('end', function () { checkpoint(args, 'outputting changes to %s', [args.infile]) if (args.dryRun) console.info(`\n---\n${chalk.gray(content.trim())}\n---\n`) - else writeFile(args, args.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n')) + else writeFile(args, args.infile, changelogFrontMatter + header + '\n' + (content + oldContentBody).replace(/\n+$/, '\n')) return resolve() }) }) diff --git a/test/core.spec.js b/test/core.spec.js index ab097ba57..8b7f17c69 100644 --- a/test/core.spec.js +++ b/test/core.spec.js @@ -165,17 +165,73 @@ describe('cli', function () { }) describe('CHANGELOG.md exists', function () { - it('appends the new release above the last release, removing the old header (legacy format)', async function () { + it('appends the new release above the last release, removing the old header (legacy format), and does not retain any front matter', async function () { + const frontMatter = + '---\nstatus: new\n---\n' mock({ bump: 'patch', changelog: 'release 1.0.1\n', - fs: { 'CHANGELOG.md': 'legacy header format\n' }, + fs: { 'CHANGELOG.md': frontMatter + 'legacy header format\n' }, tags: ['v1.0.0'] }) await exec() const content = fs.readFileSync('CHANGELOG.md', 'utf-8') content.should.match(/1\.0\.1/) content.should.not.match(/legacy header format/) + content.should.not.match(/---status: new---/) + }) + + it('appends the new release above the last release, replacing the old header (standard-version format) with header (new format), and retains any front matter', async function () { + const { header } = require('../defaults') + + const standardVersionHeader = + '# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.' + + const frontMatter = + '---\nstatus: new\n---\n' + + const changelog101 = + '### [1.0.1](/compare/v1.0.0...v1.0.1) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* patch release ABCDEFXY\n' + + const changelog100 = + '### [1.0.0](/compare/v0.0.1...v1.0.0) (YYYY-MM-DD)\n\n\n### Features\n\n* Version one feature set\n' + + const initialChangelog = frontMatter + '\n' + standardVersionHeader + '\n' + changelog100 + + mock({ + bump: 'patch', + changelog: changelog101, + fs: { 'CHANGELOG.md': initialChangelog }, + tags: ['v1.0.0'] + }) + await exec() + const content = fs.readFileSync('CHANGELOG.md', 'utf-8') + content.should.equal(frontMatter + '\n' + header + '\n' + changelog101 + changelog100) + }) + + it('appends the new release above the last release, removing the old header (new format), and retains any front matter', async function () { + const { header } = require('../defaults') + const frontMatter = + '---\nstatus: new\n---\n' + + const changelog101 = + '### [1.0.1](/compare/v1.0.0...v1.0.1) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* patch release ABCDEFXY\n' + + const changelog100 = + '### [1.0.0](/compare/v0.0.1...v1.0.0) (YYYY-MM-DD)\n\n\n### Features\n\n* Version one feature set\n' + + const initialChangelog = frontMatter + '\n' + header + '\n' + changelog100 + + mock({ + bump: 'patch', + changelog: changelog101, + fs: { 'CHANGELOG.md': initialChangelog }, + tags: ['v1.0.0'] + }) + await exec() + + const content = fs.readFileSync('CHANGELOG.md', 'utf-8') + content.should.equal(frontMatter + '\n' + header + '\n' + changelog101 + changelog100) }) it('appends the new release above the last release, removing the old header (new format)', async function () {