mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-11 03:13:32 -07:00
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
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: updates
|
||||
|
||||
- New installs now default update notifications to overlay-only instead of overlay + system notifications.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+1
-1
@@ -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 `<details><summary>Internal changes</summary>` 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 @<author> in #<pr>`, 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/<sha>/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 @<author> in #<pr>`, 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/<sha>/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 <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.
|
||||
|
||||
+11
-11
@@ -55,6 +55,17 @@
|
||||
|
||||
</details>
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Internal changes</summary>',
|
||||
'',
|
||||
'### Internal',
|
||||
'- Polished: internal release note.',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
].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, /<details>/);
|
||||
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 <details> blocks from release notes when reusing an existing CHANGELOG section', async () => {
|
||||
const { writeChangelogArtifacts } = await loadModule();
|
||||
const workspace = createWorkspace('reuse-existing-section');
|
||||
|
||||
@@ -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<ChangelogOptions, 'cwd' | '
|
||||
|
||||
export function writeReleaseNotesForVersion(options?: ChangelogOptions): string {
|
||||
const cwd = options?.cwd ?? process.cwd();
|
||||
const existsSync = options?.deps?.existsSync ?? fs.existsSync;
|
||||
const readFileSync = options?.deps?.readFileSync ?? fs.readFileSync;
|
||||
const version = resolveVersion(options ?? {});
|
||||
const changelogPath = path.join(cwd, 'CHANGELOG.md');
|
||||
@@ -970,7 +1008,14 @@ export function writeReleaseNotesForVersion(options?: ChangelogOptions): string
|
||||
throw new Error(`Missing CHANGELOG section for v${version}.`);
|
||||
}
|
||||
|
||||
return writeReleaseNotesFile(cwd, stripDetailsBlocks(changes), options?.deps);
|
||||
const releaseNotesPath = path.join(cwd, RELEASE_NOTES_PATH);
|
||||
const contributorSections = existsSync(releaseNotesPath)
|
||||
? extractContributorSections(readFileSync(releaseNotesPath, 'utf8'))
|
||||
: [];
|
||||
|
||||
return writeReleaseNotesFile(cwd, stripDetailsBlocks(changes), options?.deps, {
|
||||
contributorSections,
|
||||
});
|
||||
}
|
||||
|
||||
export function writePrereleaseNotesForVersion(options?: ChangelogOptions): string {
|
||||
|
||||
@@ -153,7 +153,7 @@ test('loads defaults when config is missing', () => {
|
||||
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,
|
||||
|
||||
@@ -128,7 +128,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
updates: {
|
||||
enabled: true,
|
||||
checkIntervalHours: 24,
|
||||
notificationType: 'both',
|
||||
notificationType: 'overlay',
|
||||
channel: 'stable',
|
||||
},
|
||||
notifications: {
|
||||
|
||||
Reference in New Issue
Block a user