diff --git a/package.json b/package.json index 071a0e85..ef188830 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "generate:translations": "node tools/generate-translations.js", "validate:translations": "node tools/validate-translations.js", "validate:schema-i18n": "node tools/validate-schema-i18n.js", + "changelog:insert": "node tools/extract-changelog.js --insert", "typecheck": "tsc --noEmit", "lint": "eslint src/ tests/ tools/ index.js install-service.js uninstall-service.js --max-warnings 0", "lint:fix": "eslint src/ tests/ tools/ index.js install-service.js uninstall-service.js --fix", diff --git a/tests/extractChangelog.test.js b/tests/extractChangelog.test.js index 7fae3152..fda817a2 100644 --- a/tests/extractChangelog.test.js +++ b/tests/extractChangelog.test.js @@ -1,4 +1,4 @@ -const { extractChangelogSection } = require('../tools/extract-changelog'); +const { extractChangelogSection, insertChangelogVersion } = require('../tools/extract-changelog'); describe('extractChangelogSection', () => { const md = `# Changelog @@ -62,3 +62,34 @@ describe('extractChangelogSection', () => { expect(previous).not.toContain(`## [${current}]`); }); }); + +describe('insertChangelogVersion', () => { + const md = `# Changelog + +## [1.27.0] - 2026-08-21 + +- First item. + +## [1.26.0] - 2026-08-15 + +- Older item. +`; + + it('inserts the new heading above the current top version', () => { + const next = insertChangelogVersion(md, '1.27.1', '2026-09-11'); + expect(next).toContain('## [1.27.1] - 2026-09-11'); + expect(next.indexOf('## [1.27.1]')).toBeLessThan(next.indexOf('## [1.27.0]')); + expect(next).toContain('## [1.27.0] - 2026-08-21'); + expect(extractChangelogSection(next, '1.27.1')).not.toContain('First item.'); + expect(extractChangelogSection(next, '1.27.0')).toContain('First item.'); + }); + + it('refuses to replace or duplicate an existing version', () => { + expect(() => insertChangelogVersion(md, '1.27.0', '2026-09-11')).toThrow(/already has/); + }); + + it('rejects a non-semver version and a non-ISO date', () => { + expect(() => insertChangelogVersion(md, '1.27', '2026-09-11')).toThrow(/invalid version/); + expect(() => insertChangelogVersion(md, '1.27.1', '11/09/2026')).toThrow(/invalid date/); + }); +}); diff --git a/tools/extract-changelog.js b/tools/extract-changelog.js index 2ae5862e..51b98b8b 100644 --- a/tools/extract-changelog.js +++ b/tools/extract-changelog.js @@ -4,7 +4,9 @@ /** * Print the CHANGELOG section for one version (for GitHub Release bodies). * - * Usage: node tools/extract-changelog.js 1.27.0 + * Usage: + * node tools/extract-changelog.js 1.27.0 + * node tools/extract-changelog.js --insert 1.27.1 2026-09-11 */ const fs = require('fs'); @@ -35,7 +37,43 @@ function extractChangelogSection(markdown, version) { return `${lines.slice(start, end).join('\n').trim()}\n`; } +/** + * Insert a new version heading above the current top section. + * Never replaces an existing heading — that is how 1.34.8 and 1.34.7 + * notes were swallowed into the next release. + * + * @param {string} markdown + * @param {string} version + * @param {string} date YYYY-MM-DD + * @returns {string} + */ +function insertChangelogVersion(markdown, version, date) { + if (!version || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`extract-changelog: invalid version "${version}"`); + } + if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new Error(`extract-changelog: invalid date "${date}"`); + } + if (markdown.includes(`## [${version}]`)) { + throw new Error(`extract-changelog: CHANGELOG already has ${version}`); + } + const firstHeading = markdown.search(/^## \[/m); + if (firstHeading === -1) { + throw new Error('extract-changelog: no existing version heading to insert above'); + } + return `${markdown.slice(0, firstHeading)}## [${version}] - ${date}\n\n${markdown.slice(firstHeading)}`; +} + function main() { + if (process.argv[2] === '--insert') { + const version = process.argv[3]; + const date = process.argv[4]; + const changelogPath = process.argv[5] + || path.join(__dirname, '..', 'homeassistant-addon', 'CHANGELOG.md'); + const markdown = fs.readFileSync(changelogPath, 'utf8'); + fs.writeFileSync(changelogPath, insertChangelogVersion(markdown, version, date)); + return; + } const version = process.argv[2]; const changelogPath = process.argv[3] || path.join(__dirname, '..', 'homeassistant-addon', 'CHANGELOG.md'); @@ -47,4 +85,4 @@ if (require.main === module) { main(); } -module.exports = { extractChangelogSection }; +module.exports = { extractChangelogSection, insertChangelogVersion };