mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(release): filter internal notes from public body
This commit is contained in:
1 parent
2290a0b4a2
commit
6a65ee4efb
2 files changed
+332
-46
No files matched your search
@@ -24,6 +24,11 @@ const workspaceRoot = path.resolve(
|
||||
'../..'
|
||||
);
|
||||
|
||||
const INTERNAL_DETAILS_BLOCK =
|
||||
/(?:^|\n\n)<details>\n<summary>Internal changes<\/summary>\n\n[\s\S]*?\n\n<\/details>(?=\n\n|$)/g;
|
||||
const CLI_USAGE =
|
||||
'Usage: extract-changelog-section.mjs [--public] <version>';
|
||||
|
||||
/**
|
||||
* @param {string} changelog full CHANGELOG.md content
|
||||
* @param {string} version bare semver, e.g. `0.24.0`
|
||||
@@ -59,38 +64,99 @@ export function extractSection(changelog, version) {
|
||||
return lines.slice(start + 1, end).join('\n').trim();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const version = process.argv[2];
|
||||
/**
|
||||
* @param {string} changelog full CHANGELOG.md content
|
||||
* @param {string} version bare semver, e.g. `0.24.0`
|
||||
* @returns {string | null} public section body without internal details
|
||||
*/
|
||||
export function extractPublicSection(changelog, version) {
|
||||
const section = extractSection(changelog, version);
|
||||
|
||||
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
||||
console.error(
|
||||
'Usage: extract-changelog-section.mjs <version> (for example 0.24.0)'
|
||||
);
|
||||
process.exit(2);
|
||||
return section === null
|
||||
? null
|
||||
: section.replace(INTERNAL_DETAILS_BLOCK, '').trim();
|
||||
}
|
||||
|
||||
export function parseExtractArguments(args) {
|
||||
const publicFlagCount = args.filter(
|
||||
(argument) => argument === '--public'
|
||||
).length;
|
||||
const positional = args.filter(
|
||||
(argument) => argument !== '--public'
|
||||
);
|
||||
|
||||
if (
|
||||
publicFlagCount > 1 ||
|
||||
positional.length !== 1 ||
|
||||
!/^\d+\.\d+\.\d+$/.test(positional[0])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changelogPath = path.join(workspaceRoot, 'CHANGELOG.md');
|
||||
const section = extractSection(readFileSync(changelogPath, 'utf8'), version);
|
||||
return {
|
||||
version: positional[0],
|
||||
publicOnly: publicFlagCount === 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function runExtractorCli(changelog, args) {
|
||||
const options = parseExtractArguments(args);
|
||||
|
||||
if (options === null) {
|
||||
return { exitCode: 2, stdout: '', stderr: `${CLI_USAGE}\n` };
|
||||
}
|
||||
|
||||
const section = options.publicOnly
|
||||
? extractPublicSection(changelog, options.version)
|
||||
: extractSection(changelog, options.version);
|
||||
|
||||
if (section === null) {
|
||||
console.error(
|
||||
[
|
||||
`CHANGELOG.md has no section for ${version}.`,
|
||||
return {
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: `${[
|
||||
`CHANGELOG.md has no section for ${options.version}.`,
|
||||
'The release flow writes it before tagging:',
|
||||
' pnpm run release:notes:changelog',
|
||||
' node tools/release/build-release-notes.mjs --consume',
|
||||
'Commit the changelog, then re-tag.',
|
||||
].join('\n')
|
||||
);
|
||||
process.exit(1);
|
||||
].join('\n')}\n`,
|
||||
};
|
||||
}
|
||||
|
||||
if (section === '') {
|
||||
console.error(`CHANGELOG.md section for ${version} is empty.`);
|
||||
process.exit(1);
|
||||
if (section === '' && !options.publicOnly) {
|
||||
return {
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: `CHANGELOG.md section for ${options.version} is empty.\n`,
|
||||
};
|
||||
}
|
||||
|
||||
process.stdout.write(`${section}\n`);
|
||||
return {
|
||||
exitCode: 0,
|
||||
stdout: section === '' ? '' : `${section}\n`,
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function runExtractorProcess(args, readChangelog) {
|
||||
if (parseExtractArguments(args) === null) {
|
||||
return runExtractorCli('', args);
|
||||
}
|
||||
|
||||
return runExtractorCli(readChangelog(), args);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const changelogPath = path.join(workspaceRoot, 'CHANGELOG.md');
|
||||
const result = runExtractorProcess(
|
||||
process.argv.slice(2),
|
||||
() => readFileSync(changelogPath, 'utf8')
|
||||
);
|
||||
|
||||
process.stdout.write(result.stdout);
|
||||
process.stderr.write(result.stderr);
|
||||
process.exitCode = result.exitCode;
|
||||
}
|
||||
|
||||
// Allow importing extractSection from tests without running the CLI.
|
||||
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
renderGithubBody,
|
||||
upsertChangelogSection,
|
||||
} from './release-notes-render.mjs';
|
||||
import { extractSection } from './extract-changelog-section.mjs';
|
||||
import {
|
||||
extractPublicSection,
|
||||
extractSection,
|
||||
parseExtractArguments,
|
||||
runExtractorCli,
|
||||
runExtractorProcess,
|
||||
} from './extract-changelog-section.mjs';
|
||||
|
||||
const tempDirs = [];
|
||||
|
||||
@@ -443,33 +449,45 @@ describe('upsertChangelogSection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const changelog = [
|
||||
'# Changelog',
|
||||
'',
|
||||
'Intro paragraph with a pointer.',
|
||||
'',
|
||||
'<!-- next-release -->',
|
||||
'',
|
||||
'# [0.24.0](https://github.com/4gray/iptvnator/compare/v0.23.0...v0.24.0) (2026-08-01)',
|
||||
'',
|
||||
'### Features',
|
||||
'',
|
||||
'- **playback** — Up Next rail.',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes</summary>',
|
||||
'',
|
||||
'- **deps** — parser bump.',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'# [0.12.0](https://github.com/4gray/iptvnator/compare/v0.11.1...v0.12.0) (2023-03-11)',
|
||||
'',
|
||||
'### Bug Fixes',
|
||||
'',
|
||||
'- old entry',
|
||||
].join('\n');
|
||||
|
||||
const internalOnlyChangelog = [
|
||||
'# 0.24.0 (2026-08-01)',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes</summary>',
|
||||
'',
|
||||
'- **deps** — parser bump.',
|
||||
'',
|
||||
'</details>',
|
||||
].join('\n');
|
||||
|
||||
describe('extractSection', () => {
|
||||
const changelog = [
|
||||
'# Changelog',
|
||||
'',
|
||||
'Intro paragraph with a pointer.',
|
||||
'',
|
||||
'<!-- next-release -->',
|
||||
'',
|
||||
'# [0.24.0](https://github.com/4gray/iptvnator/compare/v0.23.0...v0.24.0) (2026-08-01)',
|
||||
'',
|
||||
'### Features',
|
||||
'',
|
||||
'- **playback** — Up Next rail.',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes</summary>',
|
||||
'',
|
||||
'- **deps** — parser bump.',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'# [0.12.0](https://github.com/4gray/iptvnator/compare/v0.11.1...v0.12.0) (2023-03-11)',
|
||||
'',
|
||||
'### Bug Fixes',
|
||||
'',
|
||||
'- old entry',
|
||||
].join('\n');
|
||||
|
||||
it('returns the section body without its own heading', () => {
|
||||
const section = extractSection(changelog, '0.24.0');
|
||||
@@ -491,6 +509,16 @@ describe('extractSection', () => {
|
||||
assert.match(extractSection(plain, '0.24.0'), /^### Fixes/);
|
||||
});
|
||||
|
||||
it('normalizes CRLF line endings in the extracted section', () => {
|
||||
const crlf =
|
||||
'# 0.24.0 (2026-08-01)\r\n\r\n### Fixes\r\n\r\n- entry\r\n';
|
||||
|
||||
assert.equal(
|
||||
extractSection(crlf, '0.24.0'),
|
||||
'### Fixes\n\n- entry'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not match a different patch of the same minor', () => {
|
||||
assert.equal(extractSection(changelog, '0.24.1'), null);
|
||||
});
|
||||
@@ -509,6 +537,198 @@ describe('extractSection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPublicSection', () => {
|
||||
it('strips the generated internal block from a mixed release', () => {
|
||||
assert.equal(
|
||||
extractPublicSection(changelog, '0.24.0'),
|
||||
'### Features\n\n- **playback** — Up Next rail.'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps a public-only release unchanged', () => {
|
||||
const publicOnly = '# 0.24.0 (2026-08-01)\n\n### Fixes\n\n- Fixed it.';
|
||||
|
||||
assert.equal(
|
||||
extractPublicSection(publicOnly, '0.24.0'),
|
||||
'### Fixes\n\n- Fixed it.'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty string for an internal-only release', () => {
|
||||
assert.equal(extractPublicSection(internalOnlyChangelog, '0.24.0'), '');
|
||||
});
|
||||
|
||||
it('preserves unrelated details blocks', () => {
|
||||
const release = [
|
||||
'# 0.24.0 (2026-08-01)',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Migration guide</summary>',
|
||||
'',
|
||||
'Run the migration.',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes</summary>',
|
||||
'',
|
||||
'- **deps** — parser bump.',
|
||||
'',
|
||||
'</details>',
|
||||
].join('\n');
|
||||
|
||||
assert.equal(
|
||||
extractPublicSection(release, '0.24.0'),
|
||||
'<details>\n<summary>Migration guide</summary>\n\nRun the migration.\n\n</details>'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves near-match internal summaries', () => {
|
||||
const release = [
|
||||
'# 0.24.0 (2026-08-01)',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes </summary>',
|
||||
'',
|
||||
'- trailing space.',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>internal changes</summary>',
|
||||
'',
|
||||
'- different case.',
|
||||
'',
|
||||
'</details>',
|
||||
].join('\n');
|
||||
|
||||
assert.equal(
|
||||
extractPublicSection(release, '0.24.0'),
|
||||
release.split('\n').slice(2).join('\n')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extract-changelog-section CLI contracts', () => {
|
||||
it('parses the public flag', () => {
|
||||
assert.deepEqual(parseExtractArguments(['--public', '0.24.0']), {
|
||||
version: '0.24.0',
|
||||
publicOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('reports usage for invalid arguments', () => {
|
||||
for (const args of [
|
||||
[],
|
||||
['0.24'],
|
||||
['--unknown', '0.24.0'],
|
||||
['--public', '--public', '0.24.0'],
|
||||
['0.24.0', 'extra'],
|
||||
]) {
|
||||
assert.equal(parseExtractArguments(args), null);
|
||||
const result = runExtractorCli(changelog, args);
|
||||
|
||||
assert.equal(result.exitCode, 2);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, /Usage:/);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows an empty public body for an internal-only release', () => {
|
||||
assert.deepEqual(
|
||||
runExtractorCli(internalOnlyChangelog, ['--public', '0.24.0']),
|
||||
{ exitCode: 0, stdout: '', stderr: '' }
|
||||
);
|
||||
});
|
||||
|
||||
it('validates process arguments before reading the changelog', () => {
|
||||
const result = runExtractorProcess(['--public'], () => {
|
||||
throw new Error('changelog should not be read');
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 2);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, /Usage:/);
|
||||
});
|
||||
|
||||
it('keeps raw process output and errors compatible', () => {
|
||||
assert.deepEqual(
|
||||
runExtractorProcess(['0.24.0'], () => '# 0.24.0 (2026-08-01)'),
|
||||
{
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: 'CHANGELOG.md section for 0.24.0 is empty.\n',
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
runExtractorProcess(['9.9.9'], () => changelog),
|
||||
{
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: [
|
||||
'CHANGELOG.md has no section for 9.9.9.',
|
||||
'The release flow writes it before tagging:',
|
||||
' pnpm run release:notes:changelog',
|
||||
' node tools/release/build-release-notes.mjs --consume',
|
||||
'Commit the changelog, then re-tag.',
|
||||
'',
|
||||
].join('\n'),
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
runExtractorProcess(['0.24.0'], () => changelog),
|
||||
{
|
||||
exitCode: 0,
|
||||
stdout:
|
||||
'### Features\n\n- **playback** — Up Next rail.\n\n<details>\n<summary>Internal changes</summary>\n\n- **deps** — parser bump.\n\n</details>\n',
|
||||
stderr: '',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes CRLF output and appends exactly one trailing LF', () => {
|
||||
const crlf =
|
||||
'# 0.24.0 (2026-08-01)\r\n\r\n### Fixes\r\n\r\n- entry\r\n';
|
||||
const expected = {
|
||||
exitCode: 0,
|
||||
stdout: '### Fixes\n\n- entry\n',
|
||||
stderr: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(runExtractorCli(crlf, ['0.24.0']), expected);
|
||||
assert.deepEqual(
|
||||
runExtractorProcess(['0.24.0'], () => crlf),
|
||||
expected
|
||||
);
|
||||
assert.match(expected.stdout, /[^\n]\n$/);
|
||||
assert.doesNotMatch(expected.stdout, /\n\n$/);
|
||||
});
|
||||
|
||||
it('reports the detailed missing-version diagnostic in public mode', () => {
|
||||
const expected = {
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: [
|
||||
'CHANGELOG.md has no section for 9.9.9.',
|
||||
'The release flow writes it before tagging:',
|
||||
' pnpm run release:notes:changelog',
|
||||
' node tools/release/build-release-notes.mjs --consume',
|
||||
'Commit the changelog, then re-tag.',
|
||||
'',
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
runExtractorCli(changelog, ['--public', '9.9.9']),
|
||||
expected
|
||||
);
|
||||
assert.deepEqual(
|
||||
runExtractorProcess(['--public', '9.9.9'], () => changelog),
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('helpers', () => {
|
||||
it('formats dates and release slugs', () => {
|
||||
assert.equal(formatLongDate('2026-08-01'), 'August 1, 2026');
|
||||
|
||||
Reference in new issue
Block a user