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: {