diff --git a/Makefile b/Makefile index 525df635..3c229cb9 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app APPIMAGE_SRC = $(firstword $(wildcard release/SubMiner-*.AppImage)) MACOS_APP_SRC = $(firstword $(wildcard release/*.app release/*/*.app)) MACOS_ZIP_SRC = $(firstword $(wildcard release/SubMiner-*.zip)) +PRERELEASE_NOTES := release/prerelease-notes.md UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown) ifeq ($(OS),Windows_NT) @@ -161,7 +162,15 @@ build-launcher: clean: @printf '%s\n' "[INFO] Removing build artifacts" - @rm -rf dist release + @if [ -f "$(PRERELEASE_NOTES)" ]; then \ + PRERELEASE_NOTES_BACKUP="$$(mktemp -t subminer-prerelease-notes.XXXXXX)" && \ + cp "$(PRERELEASE_NOTES)" "$$PRERELEASE_NOTES_BACKUP" && \ + rm -rf dist release && \ + install -d release && \ + mv "$$PRERELEASE_NOTES_BACKUP" "$(PRERELEASE_NOTES)"; \ + else \ + rm -rf dist release; \ + fi @rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage" generate-config: ensure-bun diff --git a/changes/prerelease-notes-reuse.md b/changes/prerelease-notes-reuse.md new file mode 100644 index 00000000..3695ffa5 --- /dev/null +++ b/changes/prerelease-notes-reuse.md @@ -0,0 +1,4 @@ +type: changed +area: release + +- Prerelease note generation now reuses existing reviewed prerelease notes and asks Claude to merge only new fragment material, while `make clean` preserves `release/prerelease-notes.md`. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 737cc4ba..0cda2672 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -57,8 +57,11 @@ `*.yml` and `*.blockmap` files under `release/`. 5. Commit the prerelease prep (package.json version bump + the generated `release/prerelease-notes.md`). CI does not regenerate notes — it uses the - committed file — so review it before committing. Do not run - `bun run changelog:build`. + committed file — so review it before committing. If you add more + `changes/*.md` fragments for a later beta/RC, rerun + `bun run changelog:prerelease-notes --version `; the generator uses + the existing prerelease notes as the baseline and asks Claude to merge only + the new fragment material. Do not run `bun run changelog:build`. 6. Tag the commit: `git tag v`. 7. Push commit + tag. @@ -70,11 +73,11 @@ Notes: - Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`. - Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night. - `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. +- `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. - 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. +- 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. - If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`. - Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job. - Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication. diff --git a/package.json b/package.json index 3250f0a0..b47fedf4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "subminer", "productName": "SubMiner", "desktopName": "SubMiner.desktop", - "version": "0.15.0-beta.2", + "version": "0.15.0-beta.3", "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/release/prerelease-notes.md b/release/prerelease-notes.md new file mode 100644 index 00000000..f8b081d1 --- /dev/null +++ b/release/prerelease-notes.md @@ -0,0 +1,44 @@ +> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release. + +## Highlights +### Added + +**Auto-Updater:** SubMiner can now check for and apply updates from the system tray or by running `subminer -u`. Checks include checksum verification, configurable notifications, and an opt-in channel for prerelease builds. The `subminer` launcher and Linux rofi theme are also updated automatically. + +**First-Run Setup:** A new optional setup flow installs Bun and the `subminer` command-line launcher on Linux, macOS, and Windows. Windows users get a `subminer.cmd` PATH shim so `subminer` works in any terminal without manually adding `SubMiner.exe` to PATH. + +### Fixed + +**macOS Overlay:** Significantly improved overlay focus and stability — the overlay now hides when mpv loses focus or is minimized, stays stable through transient window-tracking misses, remains correctly layered during stats mouse passthrough, and opens over fullscreen mpv without switching Spaces. Passthrough is also fixed so mpv controls stay clickable before hovering a subtitle bar. Background tracking overhead is reduced while mpv is stably focused. + +**Subtitle Sync Modal:** Fixed a macOS issue where opening the subtitle sync modal would flash and disappear on the first attempt, or leave stale state after syncing. + +**Controller:** Controller config and debug shortcuts now stay closed while controller support is disabled, with a notice to enable `controller.enabled`. Learn mode can be entered from the edit pencil or binding badge, remaps are saved per controller profile, and individual bindings can be reset to their defaults. + +**AniList Progress:** Progress threshold checks now use fresh playback position data, so updates fire correctly when playback reaches or skips past the watched threshold. Season-specific results are preferred for multi-season files, and a clear message is shown when the matched season is not in Planning or Watching status. + +**Character Dictionary:** Cached media matches are reused when loading a title with an existing snapshot, avoiding redundant AniList search requests on repeat visits. + +**Updater — Linux:** The tray app now uses GitHub release metadata for update checks instead of the native Electron updater, preventing crashes. `subminer -u` performs updates independently of any running tray instance and correctly reports "up to date" without downloading assets when no newer release exists. + +**Updater — macOS:** Update dialogs now come to the front when launched from `subminer --update`. Builds that cannot install native updates show a manual-install message instead of an inapplicable restart prompt. Signed macOS builds remain on the native updater path without triggering premature Squirrel install checks. + +**Setup — macOS:** First-run setup now recognizes existing `subminer` launcher installs in Homebrew or user PATH directories, and manual setup avoids writing into Homebrew-owned paths. `subminer app --setup` opens the setup flow even when SubMiner is already running in the background. The standalone setup app quits after completing first-run setup, returning control to the terminal. + +**Launcher — Linux:** First-run launcher installs are now built with a valid Bun shebang, fixing installs that previously failed silently. + +**Tray App:** Fixed several lifecycle issues with tray-launched Yomitan settings: the tray stays running when settings are closed; settings loading no longer blocks other tray actions; the settings window uses a close-only menu to prevent accidentally quitting the tray app; an in-page close button is provided on Hyprland where native window controls are unavailable; the embedded popup preview is disabled to prevent renderer hangs during sidebar navigation; extension refreshes at startup are serialized to prevent race conditions; and the session help modal can now close correctly without mpv running. + +**Build — Linux Install:** Fixed one-shot `make clean build install` flows so the install step correctly picks up the AppImage produced earlier in the same make invocation. + +## Installation + +See the README and docs/installation guide for full setup steps. + +## Assets + +- Linux: `SubMiner.AppImage` +- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip` +- 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`. diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index ebb5d33e..c25daa30 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -509,6 +509,72 @@ test('writePrereleaseNotesForVersion writes cumulative beta notes without mutati } }); +test('writePrereleaseNotesForVersion reuses existing prerelease notes when adding new fragments', async () => { + const { writePrereleaseNotesForVersion } = await loadModule(); + const workspace = createWorkspace('prerelease-reuse-existing-notes'); + const projectRoot = path.join(workspace, 'SubMiner'); + const existingNotes = [ + '> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.', + '', + '## Highlights', + '### Added', + '- Overlay: Previous beta entry.', + '', + '## Installation', + '', + 'See the README and docs/installation guide for full setup steps.', + '', + '## Assets', + '', + '- Linux: `SubMiner.AppImage`', + '', + ].join('\n'); + + fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'release'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, 'package.json'), + JSON.stringify({ name: 'subminer', version: '0.11.3-beta.2' }, null, 2), + 'utf8', + ); + fs.writeFileSync(path.join(projectRoot, 'release', 'prerelease-notes.md'), existingNotes, 'utf8'); + fs.writeFileSync( + path.join(projectRoot, 'changes', '001.md'), + ['type: fixed', 'area: launcher', '', '- Fixed launcher prerelease packaging.'].join('\n'), + 'utf8', + ); + + try { + const stub = recordingRunClaude((input) => { + if (!input.includes('Overlay: Previous beta entry.')) { + return '### Fixed\n- Launcher: Added only the latest fix.'; + } + return [ + '### Added', + '- Overlay: Previous beta entry.', + '', + '### Fixed', + '- Launcher: Added only the latest fix.', + ].join('\n'); + }); + + const outputPath = writePrereleaseNotesForVersion({ + cwd: projectRoot, + version: '0.11.3-beta.2', + deps: { runClaude: stub.runClaude }, + }); + + assert.equal(stub.calls.length, 1, 'prerelease should issue exactly one Claude call'); + assert.match(stub.calls[0]!.input, /EXISTING PRERELEASE NOTES/); + + const prereleaseNotes = fs.readFileSync(outputPath, 'utf8'); + assert.match(prereleaseNotes, /- Overlay: Previous beta entry\./); + assert.match(prereleaseNotes, /- Launcher: Added only the latest fix\./); + } finally { + fs.rmSync(workspace, { recursive: true, force: true }); + } +}); + test('writePrereleaseNotesForVersion supports rc prereleases', async () => { const { writePrereleaseNotesForVersion } = await loadModule(); const workspace = createWorkspace('prerelease-rc-notes'); diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 352dce4d..dafd2af4 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -290,6 +290,7 @@ function serializeFragmentsForPrompt( mode: PolishMode, version: string, date?: string, + existingReleaseNotes?: string, ): string { const header: string[] = [`MODE: ${mode}`, `VERSION: ${version}`]; if (date) { @@ -307,7 +308,11 @@ function serializeFragmentsForPrompt( ].join('\n'); }); - return [...header, '', ...fragmentBlocks].join('\n\n'); + const existingNotesBlock = existingReleaseNotes?.trim() + ? ['EXISTING PRERELEASE NOTES', existingReleaseNotes.trim()] + : []; + + return [...header, '', ...existingNotesBlock, '', ...fragmentBlocks].join('\n\n'); } function validatePolishedOutput( @@ -340,10 +345,11 @@ function polishFragmentsWithClaude( mode: PolishMode; version: string; date?: string; + existingReleaseNotes?: string; deps?: ChangelogFsDeps; }, ): string { - const { mode, version, date } = options; + const { mode, version, date, existingReleaseNotes } = options; const runClaude = options.deps?.runClaude ?? defaultRunClaude; const filtered = @@ -361,8 +367,18 @@ function polishFragmentsWithClaude( ); } + const reuseInstructions = existingReleaseNotes?.trim() + ? [ + '## Existing Prerelease Notes', + '', + 'The input includes EXISTING PRERELEASE NOTES before the fragment list. Reuse those highlight bullets as the baseline, preserve their meaning and wording where possible, then merge in only new or changed fragment material. Deduplicate instead of restating existing bullets. Output only the final highlights body using the section headings above; do not include the prerelease disclaimer, Installation, or Assets sections.', + '', + ].join('\n') + : ''; const prompt = - POLISH_PROMPT_INSTRUCTIONS + serializeFragmentsForPrompt(filtered, mode, version, date); + POLISH_PROMPT_INSTRUCTIONS + + reuseInstructions + + serializeFragmentsForPrompt(filtered, mode, version, date, existingReleaseNotes); const output = runClaude(prompt, CLAUDE_CLI_ARGS); return validatePolishedOutput(output, mode, hasInternalFragments); } @@ -780,6 +796,8 @@ export function writePrereleaseNotesForVersion(options?: ChangelogOptions): stri verifyRequestedVersionMatchesPackageVersion(options ?? {}); const cwd = options?.cwd ?? process.cwd(); + const existsSync = options?.deps?.existsSync ?? fs.existsSync; + const readFileSync = options?.deps?.readFileSync ?? fs.readFileSync; const version = resolveVersion(options ?? {}); if (!isSupportedPrereleaseVersion(version)) { throw new Error( @@ -792,9 +810,14 @@ export function writePrereleaseNotesForVersion(options?: ChangelogOptions): stri throw new Error('No changelog fragments found in changes/.'); } + const prereleaseNotesPath = path.join(cwd, PRERELEASE_NOTES_PATH); + const existingReleaseNotes = existsSync(prereleaseNotesPath) + ? readFileSync(prereleaseNotesPath, 'utf8') + : undefined; const changes = polishFragmentsWithClaude(fragments, { mode: 'release-notes', version, + existingReleaseNotes, deps: options?.deps, }); return writeReleaseNotesFile(cwd, changes, options?.deps, { diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 07d8c4bc..64e89d21 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -167,6 +167,12 @@ test('release packaging stages generated launcher as an app resource', () => { assert.match(packageJson.scripts['build:launcher'] ?? '', /--banner='#!\/usr\/bin\/env bun'/); }); +test('Makefile clean preserves committed prerelease notes', () => { + assert.match(makefile, /PRERELEASE_NOTES_BACKUP/); + assert.match(makefile, /release\/prerelease-notes\.md/); + assert.doesNotMatch(makefile, /clean:[\s\S]*@rm -rf dist release\n/); +}); + test('config example generation runs directly from source without unrelated bundle prerequisites', () => { assert.equal( packageJson.scripts['generate:config-example'],