diff --git a/CHANGELOG.md b/CHANGELOG.md index 547f4d2..ec1b839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,44 @@ # Changelog +## v0.5.5 (2026-03-09) + +### Changed + +- Overlay: Added `f` as the default overlay fullscreen toggle and changed the default AniSkip intro-jump key to `Tab`. +- Dictionary: Aligned AniList character dictionary generation more closely with the upstream reference by preserving duplicate shared names across characters, skipping characters without native Japanese names, restoring richer character info fields, and using upstream-style role mapping plus hint-aware kanji readings. +- Startup: Ordered startup OSD messages so tokenization loads first, annotation loading appears next if still pending, and character dictionary sync progress waits until annotation loading finishes. +- Dictionary: Added a visible startup OSD step for merged character-dictionary building so long rebuilds show progress before the later import/upload phase. + +### Fixed + +- Dictionary: Fixed AniList media guessing for character dictionary auto-sync by using filename-only `guessit` input and preserving multi-part guessit titles instead of truncating them to the first segment. +- Dictionary: Refresh the current subtitle after character dictionary auto-sync completes so newly imported character names highlight on the active line instead of waiting for the next subtitle change. +- Dictionary: Show character dictionary auto-sync progress on the mpv OSD without sending desktop notifications. +- Dictionary: Keep character dictionary auto-sync non-blocking during startup by letting snapshot/build work run in parallel and delaying only the Yomitan import/settings phase until current-media tokenization is already ready. +- Overlay: Fixed visible overlay keyboard handling so pressing `Tab` still reaches mpv and triggers the default AniSkip skip-intro binding while the overlay has focus. +- Plugin: Fix Windows mpv plugin binary override lookup so `SUBMINER_BINARY_PATH` still resolves to `SubMiner.exe` when no AppImage override is set. + ## v0.5.3 (2026-03-09) ### Changed + - Release: Publish unsigned Windows `.exe` and `.zip` artifacts directly from release CI instead of routing them through SignPath. - Release: Added `bun run build:win:unsigned` for explicit local unsigned Windows packaging. ## v0.5.2 (2026-03-09) ### Internal + - Release: Pinned the Windows SignPath submission workflow to an explicit artifact-configuration slug instead of relying on the SignPath project's default configuration. ## v0.5.1 (2026-03-09) ### Changed + - Launcher: Removed the YouTube subtitle generation mode switch so YouTube playback always preloads subtitles before mpv starts. ### Fixed + - Launcher: Hardened YouTube AI subtitle fixing so fenced SRT output and text-only one-cue-per-block responses can still be applied without losing original cue timing. - Launcher: Skipped AniSkip lookup during URL playback and YouTube subtitle-preload playback, limiting AniSkip to local file targets where it can actually resolve anime metadata. - Launcher: Keep the background SubMiner process running after a launcher-managed mpv session exits so the next mpv instance can reconnect without restarting the app. @@ -24,6 +46,7 @@ - Windows: Acquire the app single-instance lock earlier so Windows overlay/video launches reuse the running background SubMiner process instead of booting a second full app and repeating startup warmups. ## v0.3.0 (2026-03-05) + - Added keyboard-driven Yomitan navigation and popup controls, including optional auto-pause. - Added subtitle/jump keyboard handling fixes for smoother subtitle playback control. - Improved Anki/Yomitan reliability with stronger Yomitan proxy syncing and safer extension refresh logic. @@ -34,6 +57,7 @@ - Removed docs Plausible integration and cleaned associated tracker settings. ## v0.2.3 (2026-03-02) + - Added performance and tokenization optimizations (faster warmup, persistent MeCab usage, reduced enrichment lookups). - Added subtitle controls for no-jump delay shifts. - Improved subtitle highlight logic with priority and reliability fixes. @@ -42,30 +66,36 @@ - Updated startup flow to load dictionaries asynchronously and unblock first tokenization sooner. ## v0.2.2 (2026-03-01) + - Improved subtitle highlighting reliability for frequency modes. - Fixed Jellyfin misc info formatting cleanup. - Version bump maintenance for 0.2.2. ## v0.2.1 (2026-03-01) + - Delivered Jellyfin and Subsync fixes from release patch cycle. - Version bump maintenance for 0.2.1. ## v0.2.0 (2026-03-01) + - Added task-related release work for the overlay 2.0 cycle. - Introduced Overlay 2.0. - Improved release automation reliability. ## v0.1.2 (2026-02-24) + - Added encrypted AniList token handling and default GNOME keyring support. - Added launcher passthrough for password-store flows (Jellyfin path). - Updated docs for auth and integration behavior. - Version bump maintenance for 0.1.2. ## v0.1.1 (2026-02-23) + - Fixed overlay modal focus handling (`grab input`) behavior. - Version bump maintenance for 0.1.1. ## v0.1.0 (2026-02-23) + - Bootstrapped Electron runtime, services, and composition model. - Added runtime asset packaging and dependency vendoring. - Added project docs baseline, setup guides, architecture notes, and submodule/runtime assets. diff --git a/backlog/tasks/task-149 - Cut-patch-release-v0.5.5-for-character-dictionary-updates-and-release-guarding.md b/backlog/tasks/task-149 - Cut-patch-release-v0.5.5-for-character-dictionary-updates-and-release-guarding.md new file mode 100644 index 0000000..5b51f54 --- /dev/null +++ b/backlog/tasks/task-149 - Cut-patch-release-v0.5.5-for-character-dictionary-updates-and-release-guarding.md @@ -0,0 +1,71 @@ +--- +id: TASK-149 +title: Cut patch release v0.5.5 for character dictionary updates and release guarding +status: Done +assignee: + - codex +created_date: '2026-03-09 01:10' +updated_date: '2026-03-09 01:14' +labels: + - release + - patch +dependencies: + - TASK-140 + - TASK-141 + - TASK-142 + - TASK-143 + - TASK-144 + - TASK-145 + - TASK-146 + - TASK-148 +references: + - package.json + - CHANGELOG.md + - scripts/build-changelog.ts + - scripts/build-changelog.test.ts + - docs/RELEASING.md +priority: high +--- + +## Description + + +Prepare and publish patch release `v0.5.5` after the failed `v0.5.4` tag by aligning package version metadata, generating committed changelog output from the pending release fragments, and hardening release validation so a future tag cannot ship with a mismatched `package.json` version. + + +## Acceptance Criteria + +- [x] #1 Repository version metadata is updated to `0.5.5`. +- [x] #2 `CHANGELOG.md` contains the committed `v0.5.5` section and the consumed fragments are removed. +- [x] #3 Release validation rejects a requested release version when it differs from `package.json`. +- [x] #4 Release docs capture the required version/changelog prep before tagging. +- [x] #5 New `v0.5.5` release-prep commit and tag are pushed to `origin/main`. + + +## Implementation Plan + + +1. Add a regression test for tagged-release/package version mismatch. +2. Update changelog validation to reject mismatched explicit release versions. +3. Bump `package.json`, generate committed `v0.5.5` changelog output, and remove consumed fragments. +4. Add a short `docs/RELEASING.md` checklist for the prep flow. +5. Run release verification, commit, tag, and push. + + +## Implementation Notes + + +Added a regression test in `scripts/build-changelog.test.ts` that proves `changelog:check --version ...` rejects tag/package mismatches. Updated `scripts/build-changelog.ts` so tagged release validation now compares the explicit requested version against `package.json` before looking for pending fragments or the committed changelog section. + +Bumped `package.json` from `0.5.3` to `0.5.5`, ran `bun run changelog:build --version 0.5.5 --date 2026-03-09`, and committed the generated `CHANGELOG.md` output while removing the consumed task fragments. Added `docs/RELEASING.md` with the required release-prep checklist so version bump + changelog generation happen before tagging. + +Verification: `bun run changelog:lint`, `bun run changelog:check --version 0.5.5`, `bun run typecheck`, `bun run test:fast`, and `bun test scripts/build-changelog.test.ts src/release-workflow.test.ts`. `bun run format:check` still reports many unrelated pre-existing repo-wide Prettier warnings, so touched files were checked/formatted separately with `bunx prettier`. + + +## Final Summary + + +Prepared patch release `v0.5.5` after the failed `v0.5.4` release attempt. Release metadata now matches the upcoming tag, the pending character-dictionary/overlay/plugin fragments are committed into `CHANGELOG.md`, and release validation now blocks future tag/package mismatches before publish. + +Docs now include a short release checklist in `docs/RELEASING.md`. Validation passed for changelog lint/check, typecheck, targeted workflow tests, and the full fast test suite. Repo-wide Prettier remains noisy from unrelated existing files, but touched release files were formatted and verified. + diff --git a/changes/task-131.md b/changes/task-131.md deleted file mode 100644 index 296be1c..0000000 --- a/changes/task-131.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: overlay - -- Added `f` as the default overlay fullscreen toggle and changed the default AniSkip intro-jump key to `Tab`. diff --git a/changes/task-133.md b/changes/task-133.md deleted file mode 100644 index 72070aa..0000000 --- a/changes/task-133.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: dictionary - -- Aligned AniList character dictionary generation more closely with the upstream reference by preserving duplicate shared names across characters, skipping characters without native Japanese names, restoring richer character info fields, and using upstream-style role mapping plus hint-aware kanji readings. diff --git a/changes/task-140.md b/changes/task-140.md deleted file mode 100644 index 15b40ba..0000000 --- a/changes/task-140.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: dictionary - -- Fixed AniList media guessing for character dictionary auto-sync by using filename-only `guessit` input and preserving multi-part guessit titles instead of truncating them to the first segment. diff --git a/changes/task-141.md b/changes/task-141.md deleted file mode 100644 index cddd9b4..0000000 --- a/changes/task-141.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: dictionary - -- Refresh the current subtitle after character dictionary auto-sync completes so newly imported character names highlight on the active line instead of waiting for the next subtitle change. diff --git a/changes/task-142.md b/changes/task-142.md deleted file mode 100644 index cce02ab..0000000 --- a/changes/task-142.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: dictionary - -- Show character dictionary auto-sync progress on the mpv OSD without sending desktop notifications. diff --git a/changes/task-143.md b/changes/task-143.md deleted file mode 100644 index f59a08c..0000000 --- a/changes/task-143.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: dictionary - -- Keep character dictionary auto-sync non-blocking during startup by letting snapshot/build work run in parallel and delaying only the Yomitan import/settings phase until current-media tokenization is already ready. diff --git a/changes/task-144.md b/changes/task-144.md deleted file mode 100644 index e47af98..0000000 --- a/changes/task-144.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: startup - -- Ordered startup OSD messages so tokenization loads first, annotation loading appears next if still pending, and character dictionary sync progress waits until annotation loading finishes. diff --git a/changes/task-145.md b/changes/task-145.md deleted file mode 100644 index 9747344..0000000 --- a/changes/task-145.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: dictionary - -- Added a visible startup OSD step for merged character-dictionary building so long rebuilds show progress before the later import/upload phase. diff --git a/changes/task-146.md b/changes/task-146.md deleted file mode 100644 index e7a77b7..0000000 --- a/changes/task-146.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed visible overlay keyboard handling so pressing `Tab` still reaches mpv and triggers the default AniSkip skip-intro binding while the overlay has focus. diff --git a/changes/task-148.md b/changes/task-148.md deleted file mode 100644 index db6bfb6..0000000 --- a/changes/task-148.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: plugin - -- Fix Windows mpv plugin binary override lookup so `SUBMINER_BINARY_PATH` still resolves to `SubMiner.exe` when no AppImage override is set. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..f8d9f69 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,21 @@ + + +# Releasing + +1. Confirm `main` is green: `gh run list --workflow CI --limit 5`. +2. Bump `package.json` to the release version. +3. Build release metadata before tagging: + `bun run changelog:build --version ` +4. Review `CHANGELOG.md`. +5. Run release gate locally: + `bun run changelog:check --version ` + `bun run test:fast` + `bun run typecheck` +6. Commit release prep. +7. Tag the commit: `git tag v`. +8. Push commit + tag. + +Notes: + +- `changelog:check` now rejects tag/package version mismatches. +- Do not tag while `changes/*.md` fragments still exist. diff --git a/package.json b/package.json index ccf6735..44f68ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "subminer", - "version": "0.5.3", + "version": "0.5.5", "description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration", "packageManager": "bun@1.3.5", "main": "dist/main-entry.js", diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index 295dbcd..4d1939f 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -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'); diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 3062e88..142f650 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -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, -): string { +function resolveVersion(options: Pick): 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, +): 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(