From b3b45521b69af754008166a8a7162f444d91722e Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 10 Jun 2026 23:53:31 -0700 Subject: [PATCH] fix(release): preserve attribution placement; default update notifs to o - Move What's Changed/New Contributors before Installation in release notes - Preserve committed attribution when regenerating via writeReleaseNotesForVersion - Change notificationType default from 'both' to 'overlay' for new installs --- .../release-notes-attribution-placement.md | 4 + .../update-notification-default-overlay.md | 4 + config.example.jsonc | 2 +- docs-site/changelog.md | 2 +- docs-site/configuration.md | 4 +- docs-site/public/config.example.jsonc | 2 +- docs/RELEASING.md | 2 +- release/release-notes.md | 22 ++--- scripts/build-changelog.test.ts | 96 ++++++++++++++++++- scripts/build-changelog.ts | 51 +++++++++- src/config/config.test.ts | 4 +- src/config/definitions/defaults-core.ts | 2 +- 12 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 changes/release-notes-attribution-placement.md create mode 100644 changes/update-notification-default-overlay.md diff --git a/changes/release-notes-attribution-placement.md b/changes/release-notes-attribution-placement.md new file mode 100644 index 00000000..9c70edec --- /dev/null +++ b/changes/release-notes-attribution-placement.md @@ -0,0 +1,4 @@ +type: fixed +area: release + +- Kept the GitHub release `What's Changed` and `New Contributors` attribution sections when CI regenerates release notes from the committed changelog. diff --git a/changes/update-notification-default-overlay.md b/changes/update-notification-default-overlay.md new file mode 100644 index 00000000..7e521426 --- /dev/null +++ b/changes/update-notification-default-overlay.md @@ -0,0 +1,4 @@ +type: changed +area: updates + +- New installs now default update notifications to overlay-only instead of overlay + system notifications. diff --git a/config.example.jsonc b/config.example.jsonc index ad0a1c4a..4bbeb33f 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -172,7 +172,7 @@ "updates": { "enabled": true, // Run automatic update checks in the background. Values: true | false "checkIntervalHours": 24, // Minimum hours between automatic update checks. - "notificationType": "both", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system + "notificationType": "overlay", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system "channel": "stable" // Release channel used for update checks. Values: stable | prerelease }, // Automatic update check behavior. diff --git a/docs-site/changelog.md b/docs-site/changelog.md index de59d9f6..969a7f7e 100644 --- a/docs-site/changelog.md +++ b/docs-site/changelog.md @@ -24,7 +24,7 @@ - **Stats Browsing**: Remembers library card size; retries stored cover art without extra AniList lookups; preserves PNG/WebP MIME types; honors custom AnkiConnect URLs for Browse; shows progress during session deletes. - **Startup Notifications**: Tokenization, subtitle annotation, and character dictionary status now route through queued overlay notifications in `overlay`/`both` mode instead of falling back to mpv OSD while the overlay loads. - **Notification Deduplication**: Cycling subtitle modes updates the active overlay card in place rather than stacking duplicates; repeated progress updates (e.g. subsync) tick in place without flickering. -- **Update Notification Default**: New installs default `notificationType` to `both` so update alerts appear in both overlay and system notifications. +- **Update Notification Default**: New installs default `notificationType` to `overlay`, while `both` remains available for overlay + system notifications. **Fixed** diff --git a/docs-site/configuration.md b/docs-site/configuration.md index 1ef94580..cc36995a 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -197,7 +197,7 @@ Configure automatic update checks and update notifications: "updates": { "enabled": true, "checkIntervalHours": 24, - "notificationType": "both", + "notificationType": "overlay", "channel": "stable" } } @@ -207,7 +207,7 @@ Configure automatic update checks and update notifications: | -------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `updates.enabled` | `true`, `false` | Enable automatic background update checks. Manual tray and `subminer -u` checks are always allowed. | | `checkIntervalHours` | number | Minimum hours between automatic update checks. Default `24`. | -| `notificationType` | `"overlay"` \| `"system"` \| `"both"` \| `"none"` | How SubMiner announces available updates. Default `"both"`, which means overlay + system. | +| `notificationType` | `"overlay"` \| `"system"` \| `"both"` \| `"none"` | How SubMiner announces available updates. Default `"overlay"`. `"both"` means overlay + system. | | `channel` | `"stable"` \| `"prerelease"` | Release channel used for update checks. Use `"prerelease"` to test beta/RC releases. | When `notificationType` is `"overlay"` or `"both"`, update-available overlay notifications include an **Update** button that starts the app update flow. diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index ad0a1c4a..4bbeb33f 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -172,7 +172,7 @@ "updates": { "enabled": true, // Run automatic update checks in the background. Values: true | false "checkIntervalHours": 24, // Minimum hours between automatic update checks. - "notificationType": "both", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system + "notificationType": "overlay", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system "channel": "stable" // Release channel used for update checks. Values: stable | prerelease }, // Automatic update check behavior. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 40ed7b3c..a7e3e4de 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -77,7 +77,7 @@ Notes: - `changelog:check` now rejects tag/package version mismatches. - `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files. When that file already exists, the generator includes it in the Claude prompt so later beta/RC notes reuse the reviewed text instead of starting over. - `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` (both polished by `claude -p`) and removes the released `changes/*.md` fragments. The CHANGELOG keeps internal notes inside a `
Internal changes` collapse; the release notes drop them entirely. -- `release/release-notes.md` (and `release/prerelease-notes.md`) end with GitHub-style attribution: a `## What’s Changed` list crediting each released fragment as `by @ in #`, plus a `## New Contributors` section for first-time authors. Attribution is resolved per fragment via `git log` (the commit that added the fragment) + `gh api .../commits//pulls`, with one `gh` search per author for the first-contribution check. It needs `gh` installed and authenticated; if `gh` is unavailable or a lookup fails, the generator warns and emits notes without the attribution sections rather than failing. The CHANGELOG itself stays attribution-free. +- `release/release-notes.md` (and `release/prerelease-notes.md`) include GitHub-style attribution after `## Highlights`: a `## What's Changed` list crediting each released fragment as `by @ in #`, plus a `## New Contributors` section for first-time authors. Attribution is resolved per fragment via `git log` (the commit that added the fragment) + `gh api .../commits//pulls`, with one `gh` search per author for the first-contribution check. It needs `gh` installed and authenticated; if `gh` is unavailable or a lookup fails, the generator warns and emits notes without the attribution sections rather than failing. The CHANGELOG itself stays attribution-free. - The release workflow no longer auto-runs `changelog:build`. If pending `changes/*.md` fragments are present on a tag-based run, CI exits with a clear `::error::` pointing at the local fix. Run `bun run changelog:build --version ` locally, commit the polished output, then tag. - Do not tag while `changes/*.md` fragments still exist. - Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut. `make clean` preserves `release/prerelease-notes.md` while deleting generated build artifacts. diff --git a/release/release-notes.md b/release/release-notes.md index 00479341..0568ea24 100644 --- a/release/release-notes.md +++ b/release/release-notes.md @@ -55,6 +55,17 @@
+## What's Changed + +- feat(notifications): add overlay notifications with position config by @ksyasuda in #110 +- feat(stats): speed up session maintenance and improve stats UI by @ksyasuda in #111 +- [codex] Restart Jellyfin remote session after setup login by @bee-san in #112 +- docs(changelog): require reconciled fragments, not just new ones by @ksyasuda in #113 +- feat(release): add contributor attribution to release notes by @ksyasuda in #114 +- fix(anilist): mark entry completed when final episode is reached by @ksyasuda in #115 +- feat(aniskip): move intro detection from mpv plugin to app runtime by @ksyasuda in #117 +- fix(anki): write sentence card audio only to sentence audio field by @ksyasuda in #118 + ## Installation See the README and docs/installation guide for full setup steps. @@ -67,14 +78,3 @@ See the README and docs/installation guide for full setup steps. - Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`. - -## What’s Changed - -- feat(notifications): add overlay notifications with position config by @ksyasuda in #110 -- feat(stats): speed up session maintenance and improve stats UI by @ksyasuda in #111 -- [codex] Restart Jellyfin remote session after setup login by @bee-san in #112 -- docs(changelog): require reconciled fragments, not just new ones by @ksyasuda in #113 -- feat(release): add contributor attribution to release notes by @ksyasuda in #114 -- fix(anilist): mark entry completed when final episode is reached by @ksyasuda in #115 -- feat(aniskip): move intro detection from mpv plugin to app runtime by @ksyasuda in #117 -- fix(anki): write sentence card audio only to sentence audio field by @ksyasuda in #118 diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index fa7359ca..9d6e443e 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -1122,13 +1122,22 @@ test('writeChangelogArtifacts appends contributor attribution and a new-contribu path.join(projectRoot, 'release', 'release-notes.md'), 'utf8', ); - assert.match(releaseNotes, /## What’s Changed\n\n/); + assert.match(releaseNotes, /## What's Changed\n\n/); assert.match(releaseNotes, /- feat\(overlay\): add a feature by @ksyasuda in #110\n/); assert.match(releaseNotes, /- fix\(jellyfin\): restart remote session by @bee-san in #112\n/); assert.match( releaseNotes, /## New Contributors\n\n- @bee-san made their first contribution in #112/, ); + assert.ok( + releaseNotes.indexOf("## What's Changed") > releaseNotes.indexOf('## Highlights'), + "What's Changed should follow Highlights", + ); + assert.ok( + releaseNotes.indexOf('## New Contributors') < releaseNotes.indexOf('## Installation'), + 'contributor attribution should appear before Installation', + ); + assert.doesNotMatch(releaseNotes, /## What’s Changed/); assert.doesNotMatch( releaseNotes, /ksyasuda made their first contribution/, @@ -1137,13 +1146,96 @@ test('writeChangelogArtifacts appends contributor attribution and a new-contribu // Attribution is a release-notes concern only; the CHANGELOG stays clean. const changelog = fs.readFileSync(path.join(projectRoot, 'CHANGELOG.md'), 'utf8'); - assert.doesNotMatch(changelog, /What’s Changed/); + assert.doesNotMatch(changelog, /What's Changed|What’s Changed/); assert.doesNotMatch(changelog, /New Contributors/); } finally { fs.rmSync(workspace, { recursive: true, force: true }); } }); +test('writeReleaseNotesForVersion preserves committed contributor attribution before installation', async () => { + const { writeReleaseNotesForVersion } = await loadModule(); + const workspace = createWorkspace('release-notes-preserve-attribution'); + const projectRoot = path.join(workspace, 'SubMiner'); + const existingChangelog = [ + '# Changelog', + '', + '## v0.8.0 (2026-04-17)', + '### Added', + '- Polished: released feature.', + '', + '
', + 'Internal changes', + '', + '### Internal', + '- Polished: internal release note.', + '', + '
', + '', + ].join('\n'); + const committedReleaseNotes = [ + '## Highlights', + '### Added', + '- Old generated body.', + '', + '## Installation', + '', + 'See the README and docs/installation guide for full setup steps.', + '', + '## Assets', + '', + '- Linux: `SubMiner.AppImage`', + '', + '## What’s Changed', + '', + '- feat(release): add contributor attribution by @ksyasuda in #114', + '', + '## New Contributors', + '', + '- @bee-san made their first contribution in #112', + '', + ].join('\n'); + + fs.mkdirSync(path.join(projectRoot, 'release'), { recursive: true }); + fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), existingChangelog, 'utf8'); + fs.writeFileSync( + path.join(projectRoot, 'release', 'release-notes.md'), + committedReleaseNotes, + 'utf8', + ); + + try { + const outputPath = writeReleaseNotesForVersion({ + cwd: projectRoot, + version: '0.8.0', + }); + const releaseNotes = fs.readFileSync(outputPath, 'utf8'); + + assert.match(releaseNotes, /## Highlights\n### Added\n- Polished: released feature\./); + assert.doesNotMatch(releaseNotes, /
/); + assert.doesNotMatch(releaseNotes, /### Internal/); + assert.match( + releaseNotes, + /## What's Changed\n\n- feat\(release\): add contributor attribution by @ksyasuda in #114/, + ); + assert.match( + releaseNotes, + /## New Contributors\n\n- @bee-san made their first contribution in #112/, + ); + assert.ok( + releaseNotes.indexOf("## What's Changed") > releaseNotes.indexOf('## Highlights'), + "What's Changed should follow Highlights", + ); + assert.ok( + releaseNotes.indexOf('## New Contributors') < releaseNotes.indexOf('## Installation'), + 'New Contributors should appear before Installation', + ); + assert.doesNotMatch(releaseNotes, /## What’s Changed/); + } finally { + fs.rmSync(workspace, { recursive: true, force: true }); + } +}); + test('writeChangelogArtifacts strips
blocks from release notes when reusing an existing CHANGELOG section', async () => { const { writeChangelogArtifacts } = await loadModule(); const workspace = createWorkspace('reuse-existing-section'); diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 47dd800b..65197d15 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -433,12 +433,45 @@ function resolveContributionsForFragments( ); } +function isWhatsChangedHeading(line: string): boolean { + return line === "## What's Changed" || line === '## What’s Changed'; +} + +function extractContributorSections(releaseNotes: string): string[] { + const lines = releaseNotes.split(/\r?\n/); + const start = lines.findIndex(isWhatsChangedHeading); + if (start === -1) { + return []; + } + + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + const line = lines[index]!; + if (line.startsWith('## ') && !isWhatsChangedHeading(line) && line !== '## New Contributors') { + end = index; + break; + } + } + + const block = lines.slice(start, end); + while (block.length > 0 && block[block.length - 1] === '') { + block.pop(); + } + if (block.length === 0) { + return []; + } + + block[0] = "## What's Changed"; + block.push(''); + return block; +} + function renderContributorsSections(contributions: Contribution[]): string[] { if (contributions.length === 0) { return []; } - const lines: string[] = ['## What’s Changed', '']; + const lines: string[] = ["## What's Changed", '']; for (const contribution of contributions) { lines.push(`- ${contribution.title} by @${contribution.login} in #${contribution.prNumber}`); } @@ -635,14 +668,18 @@ function renderReleaseNotes( options?: { disclaimer?: string; contributions?: Contribution[]; + contributorSections?: string[]; }, ): string { const prefix = options?.disclaimer ? [options.disclaimer, ''] : []; + const contributorSections = + options?.contributorSections ?? renderContributorsSections(options?.contributions ?? []); return [ ...prefix, '## Highlights', changes, '', + ...contributorSections, '## Installation', '', 'See the README and docs/installation guide for full setup steps.', @@ -656,7 +693,6 @@ function renderReleaseNotes( '', 'Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.', '', - ...renderContributorsSections(options?.contributions ?? []), ].join('\n'); } @@ -668,6 +704,7 @@ function writeReleaseNotesFile( disclaimer?: string; outputPath?: string; contributions?: Contribution[]; + contributorSections?: string[]; }, ): string { const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync; @@ -960,6 +997,7 @@ export function generateDocsChangelog(options?: Pick { assert.equal(config.stats.autoOpenBrowser, false); assert.equal(config.updates.enabled, true); assert.equal(config.updates.checkIntervalHours, 24); - assert.equal(config.updates.notificationType, 'both'); + assert.equal(config.updates.notificationType, 'overlay'); assert.equal(config.updates.channel, 'stable'); assert.equal(config.mpv.socketPath, DEFAULT_CONFIG.mpv.socketPath); assert.equal(config.mpv.backend, 'auto'); @@ -2814,7 +2814,7 @@ test('template generator includes known keys', () => { ); assert.match( output, - /"notificationType": "both",? \/\/ How SubMiner announces available updates\..*Values: overlay \| system \| both \| none \| osd \| osd-system/, + /"notificationType": "overlay",? \/\/ How SubMiner announces available updates\..*Values: overlay \| system \| both \| none \| osd \| osd-system/, ); assert.match( output, diff --git a/src/config/definitions/defaults-core.ts b/src/config/definitions/defaults-core.ts index f602d151..f17b79fc 100644 --- a/src/config/definitions/defaults-core.ts +++ b/src/config/definitions/defaults-core.ts @@ -128,7 +128,7 @@ export const CORE_DEFAULT_CONFIG: Pick< updates: { enabled: true, checkIntervalHours: 24, - notificationType: 'both', + notificationType: 'overlay', channel: 'stable', }, notifications: {