mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
chore: prep v0.5.5 release
This commit is contained in:
@@ -34,12 +34,22 @@ test('writeChangelogArtifacts ignores README, groups fragments by type, writes r
|
||||
const { writeChangelogArtifacts } = await loadModule();
|
||||
const workspace = createWorkspace('write-artifacts');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
const existingChangelog = ['# Changelog', '', '## v0.4.0 (2026-03-01)', '- Existing fix', ''].join('\n');
|
||||
const existingChangelog = [
|
||||
'# Changelog',
|
||||
'',
|
||||
'## v0.4.0 (2026-03-01)',
|
||||
'- Existing fix',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), existingChangelog, 'utf8');
|
||||
fs.writeFileSync(path.join(projectRoot, 'changes', 'README.md'), '# Changelog Fragments\n\nIgnored helper text.\n', 'utf8');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', 'README.md'),
|
||||
'# Changelog Fragments\n\nIgnored helper text.\n',
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Added release fragments.'].join('\n'),
|
||||
@@ -59,13 +69,10 @@ test('writeChangelogArtifacts ignores README, groups fragments by type, writes r
|
||||
});
|
||||
|
||||
assert.deepEqual(result.outputPaths, [path.join(projectRoot, 'CHANGELOG.md')]);
|
||||
assert.deepEqual(
|
||||
result.deletedFragmentPaths,
|
||||
[
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
path.join(projectRoot, 'changes', '002.md'),
|
||||
],
|
||||
);
|
||||
assert.deepEqual(result.deletedFragmentPaths, [
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
path.join(projectRoot, 'changes', '002.md'),
|
||||
]);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '001.md')), false);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '002.md')), false);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', 'README.md')), true);
|
||||
@@ -76,7 +83,10 @@ test('writeChangelogArtifacts ignores README, groups fragments by type, writes r
|
||||
/^# Changelog\n\n## v0\.4\.1 \(2026-03-07\)\n\n### Added\n- Overlay: Added release fragments\.\n\n### Fixed\n- Release: Fixed release notes generation\.\n\n## v0\.4\.0 \(2026-03-01\)\n- Existing fix\n$/m,
|
||||
);
|
||||
|
||||
const releaseNotes = fs.readFileSync(path.join(projectRoot, 'release', 'release-notes.md'), 'utf8');
|
||||
const releaseNotes = fs.readFileSync(
|
||||
path.join(projectRoot, 'release', 'release-notes.md'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(releaseNotes, /## Highlights\n### Added\n- Overlay: Added release fragments\./);
|
||||
assert.match(releaseNotes, /### Fixed\n- Release: Fixed release notes generation\./);
|
||||
assert.match(releaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
|
||||
@@ -92,7 +102,11 @@ test('verifyChangelogReadyForRelease ignores README but rejects pending fragment
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), '# Changelog\n', 'utf8');
|
||||
fs.writeFileSync(path.join(projectRoot, 'changes', 'README.md'), '# Changelog Fragments\n', 'utf8');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', 'README.md'),
|
||||
'# Changelog Fragments\n',
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(path.join(projectRoot, 'changes', '001.md'), '- Pending fragment.\n', 'utf8');
|
||||
|
||||
try {
|
||||
@@ -112,6 +126,33 @@ test('verifyChangelogReadyForRelease ignores README but rejects pending fragment
|
||||
}
|
||||
});
|
||||
|
||||
test('verifyChangelogReadyForRelease rejects explicit release versions that do not match package.json', async () => {
|
||||
const { verifyChangelogReadyForRelease } = await loadModule();
|
||||
const workspace = createWorkspace('verify-release-version-match');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.4.0' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'CHANGELOG.md'),
|
||||
'# Changelog\n\n## v0.4.1 (2026-03-09)\n- Ready.\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() => verifyChangelogReadyForRelease({ cwd: projectRoot, version: '0.4.1' }),
|
||||
/package\.json version \(0\.4\.0\) does not match requested release version \(0\.4\.1\)/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('verifyChangelogFragments rejects invalid metadata', async () => {
|
||||
const { verifyChangelogFragments } = await loadModule();
|
||||
const workspace = createWorkspace('lint-invalid');
|
||||
|
||||
@@ -56,7 +56,10 @@ function resolveDate(date?: string): string {
|
||||
return date ?? new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function resolvePackageVersion(cwd: string, readFileSync: (candidate: string, encoding: BufferEncoding) => string): string {
|
||||
function resolvePackageVersion(
|
||||
cwd: string,
|
||||
readFileSync: (candidate: string, encoding: BufferEncoding) => string,
|
||||
): string {
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version?: string };
|
||||
if (!packageJson.version) {
|
||||
@@ -65,22 +68,42 @@ function resolvePackageVersion(cwd: string, readFileSync: (candidate: string, en
|
||||
return normalizeVersion(packageJson.version);
|
||||
}
|
||||
|
||||
function resolveVersion(
|
||||
options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>,
|
||||
): string {
|
||||
function resolveVersion(options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>): string {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const readFileSync = options.deps?.readFileSync ?? fs.readFileSync;
|
||||
return normalizeVersion(options.version ?? resolvePackageVersion(cwd, readFileSync));
|
||||
}
|
||||
|
||||
function verifyRequestedVersionMatchesPackageVersion(
|
||||
options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>,
|
||||
): void {
|
||||
if (!options.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const existsSync = options.deps?.existsSync ?? fs.existsSync;
|
||||
const readFileSync = options.deps?.readFileSync ?? fs.readFileSync;
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageVersion = resolvePackageVersion(cwd, readFileSync);
|
||||
const requestedVersion = normalizeVersion(options.version);
|
||||
|
||||
if (packageVersion !== requestedVersion) {
|
||||
throw new Error(
|
||||
`package.json version (${packageVersion}) does not match requested release version (${requestedVersion}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChangesDir(cwd: string): string {
|
||||
return path.join(cwd, 'changes');
|
||||
}
|
||||
|
||||
function resolveFragmentPaths(
|
||||
cwd: string,
|
||||
deps?: ChangelogFsDeps,
|
||||
): string[] {
|
||||
function resolveFragmentPaths(cwd: string, deps?: ChangelogFsDeps): string[] {
|
||||
const changesDir = resolveChangesDir(cwd);
|
||||
const existsSync = deps?.existsSync ?? fs.existsSync;
|
||||
const readdirSync = deps?.readdirSync ?? fs.readdirSync;
|
||||
@@ -90,7 +113,10 @@ function resolveFragmentPaths(
|
||||
}
|
||||
|
||||
return readdirSync(changesDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'readme.md')
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isFile() && entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'readme.md',
|
||||
)
|
||||
.map((entry) => path.join(changesDir, entry.name))
|
||||
.sort();
|
||||
}
|
||||
@@ -112,7 +138,10 @@ function normalizeFragmentBullets(content: string): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function parseFragmentMetadata(content: string, fragmentPath: string): {
|
||||
function parseFragmentMetadata(
|
||||
content: string,
|
||||
fragmentPath: string,
|
||||
): {
|
||||
area: string;
|
||||
body: string;
|
||||
type: FragmentType;
|
||||
@@ -144,9 +173,7 @@ function parseFragmentMetadata(content: string, fragmentPath: string): {
|
||||
|
||||
const type = metadata.get('type');
|
||||
if (!type || !CHANGE_TYPES.includes(type as FragmentType)) {
|
||||
throw new Error(
|
||||
`${fragmentPath} must declare type as one of: ${CHANGE_TYPES.join(', ')}.`,
|
||||
);
|
||||
throw new Error(`${fragmentPath} must declare type as one of: ${CHANGE_TYPES.join(', ')}.`);
|
||||
}
|
||||
|
||||
const area = metadata.get('area');
|
||||
@@ -166,10 +193,7 @@ function parseFragmentMetadata(content: string, fragmentPath: string): {
|
||||
};
|
||||
}
|
||||
|
||||
function readChangeFragments(
|
||||
cwd: string,
|
||||
deps?: ChangelogFsDeps,
|
||||
): ChangeFragment[] {
|
||||
function readChangeFragments(cwd: string, deps?: ChangelogFsDeps): ChangeFragment[] {
|
||||
const readFileSync = deps?.readFileSync ?? fs.readFileSync;
|
||||
return resolveFragmentPaths(cwd, deps).map((fragmentPath) => {
|
||||
const parsed = parseFragmentMetadata(readFileSync(fragmentPath, 'utf8'), fragmentPath);
|
||||
@@ -202,7 +226,9 @@ function renderGroupedChanges(fragments: ChangeFragment[]): string {
|
||||
}
|
||||
|
||||
const bullets = typeFragments
|
||||
.flatMap((fragment) => fragment.bullets.map((bullet) => renderFragmentBullet(fragment, bullet)))
|
||||
.flatMap((fragment) =>
|
||||
fragment.bullets.map((bullet) => renderFragmentBullet(fragment, bullet)),
|
||||
)
|
||||
.join('\n');
|
||||
return [`### ${CHANGE_TYPE_HEADINGS[type]}\n${bullets}`];
|
||||
});
|
||||
@@ -215,9 +241,7 @@ function buildReleaseSection(version: string, date: string, fragments: ChangeFra
|
||||
throw new Error('No changelog fragments found in changes/.');
|
||||
}
|
||||
|
||||
return [`## v${version} (${date})`, '', renderGroupedChanges(fragments), ''].join(
|
||||
'\n',
|
||||
);
|
||||
return [`## v${version} (${date})`, '', renderGroupedChanges(fragments), ''].join('\n');
|
||||
}
|
||||
|
||||
function ensureChangelogHeader(existingChangelog: string): string {
|
||||
@@ -231,7 +255,11 @@ function ensureChangelogHeader(existingChangelog: string): string {
|
||||
return `${CHANGELOG_HEADER}\n\n${trimmed}\n`;
|
||||
}
|
||||
|
||||
function prependReleaseSection(existingChangelog: string, releaseSection: string, version: string): string {
|
||||
function prependReleaseSection(
|
||||
existingChangelog: string,
|
||||
releaseSection: string,
|
||||
version: string,
|
||||
): string {
|
||||
const normalizedExisting = ensureChangelogHeader(existingChangelog);
|
||||
if (extractReleaseSectionBody(normalizedExisting, version) !== null) {
|
||||
throw new Error(`CHANGELOG already contains a section for v${version}.`);
|
||||
@@ -263,9 +291,7 @@ function extractReleaseSectionBody(changelog: string, version: string): string |
|
||||
return body.trim();
|
||||
}
|
||||
|
||||
export function resolveChangelogOutputPaths(options?: {
|
||||
cwd?: string;
|
||||
}): string[] {
|
||||
export function resolveChangelogOutputPaths(options?: { cwd?: string }): string[] {
|
||||
const cwd = options?.cwd ?? process.cwd();
|
||||
return [path.join(cwd, 'CHANGELOG.md')];
|
||||
}
|
||||
@@ -290,11 +316,7 @@ function renderReleaseNotes(changes: string): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function writeReleaseNotesFile(
|
||||
cwd: string,
|
||||
changes: string,
|
||||
deps?: ChangelogFsDeps,
|
||||
): string {
|
||||
function writeReleaseNotesFile(cwd: string, changes: string, deps?: ChangelogFsDeps): string {
|
||||
const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync;
|
||||
const writeFileSync = deps?.writeFileSync ?? fs.writeFileSync;
|
||||
const releaseNotesPath = path.join(cwd, RELEASE_NOTES_PATH);
|
||||
@@ -359,10 +381,13 @@ export function verifyChangelogFragments(options?: ChangelogOptions): void {
|
||||
export function verifyChangelogReadyForRelease(options?: ChangelogOptions): void {
|
||||
const cwd = options?.cwd ?? process.cwd();
|
||||
const readFileSync = options?.deps?.readFileSync ?? fs.readFileSync;
|
||||
verifyRequestedVersionMatchesPackageVersion(options ?? {});
|
||||
const version = resolveVersion(options ?? {});
|
||||
const pendingFragments = resolveFragmentPaths(cwd, options?.deps);
|
||||
if (pendingFragments.length > 0) {
|
||||
throw new Error(`Pending changelog fragments must be released first: ${pendingFragments.join(', ')}`);
|
||||
throw new Error(
|
||||
`Pending changelog fragments must be released first: ${pendingFragments.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const changelogPath = path.join(cwd, 'CHANGELOG.md');
|
||||
@@ -382,14 +407,14 @@ function isFragmentPath(candidate: string): boolean {
|
||||
|
||||
function isIgnoredPullRequestPath(candidate: string): boolean {
|
||||
return (
|
||||
candidate === 'CHANGELOG.md'
|
||||
|| candidate === 'release/release-notes.md'
|
||||
|| candidate === 'AGENTS.md'
|
||||
|| candidate === 'README.md'
|
||||
|| candidate.startsWith('changes/')
|
||||
|| candidate.startsWith('docs/')
|
||||
|| candidate.startsWith('.github/')
|
||||
|| candidate.startsWith('backlog/')
|
||||
candidate === 'CHANGELOG.md' ||
|
||||
candidate === 'release/release-notes.md' ||
|
||||
candidate === 'AGENTS.md' ||
|
||||
candidate === 'README.md' ||
|
||||
candidate.startsWith('changes/') ||
|
||||
candidate.startsWith('docs/') ||
|
||||
candidate.startsWith('.github/') ||
|
||||
candidate.startsWith('backlog/')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -412,9 +437,7 @@ export function verifyPullRequestChangelog(options: PullRequestChangelogOptions)
|
||||
const hasFragment = normalizedEntries.some(
|
||||
(entry) => entry.status !== 'D' && isFragmentPath(entry.path),
|
||||
);
|
||||
const requiresFragment = normalizedEntries.some(
|
||||
(entry) => !isIgnoredPullRequestPath(entry.path),
|
||||
);
|
||||
const requiresFragment = normalizedEntries.some((entry) => !isIgnoredPullRequestPath(entry.path));
|
||||
|
||||
if (requiresFragment && !hasFragment) {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user