From 17d97f0b7ebc8dbaab67fa1ca3877278626ed59d Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 24 May 2026 23:47:02 -0700 Subject: [PATCH] fix: rename Windows ZIPs and fix macOS manual update checks (#81) --- changes/macos-manual-update-required.md | 2 +- changes/macos-updater-zip-collision.md | 4 ++ docs-site/installation.md | 2 +- docs/RELEASING.md | 5 +- package.json | 6 +++ src/main.ts | 4 +- .../update/release-metadata-policy.test.ts | 29 ++++++++++- .../runtime/update/release-metadata-policy.ts | 10 +++- .../runtime/update/update-service.test.ts | 52 +++++++++++++++++++ src/prerelease-workflow.test.ts | 5 ++ src/release-workflow.test.ts | 22 ++++++++ 11 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 changes/macos-updater-zip-collision.md diff --git a/changes/macos-manual-update-required.md b/changes/macos-manual-update-required.md index e84acfe4..52f91288 100644 --- a/changes/macos-manual-update-required.md +++ b/changes/macos-manual-update-required.md @@ -1,4 +1,4 @@ type: fixed area: updater -- Fixed tray update checks for builds that cannot install native app updates, showing a manual install message instead of a restart prompt that cannot apply the update. +- Fixed macOS tray update checks for builds that cannot install native app updates, so newer stable or prerelease GitHub releases are reported instead of incorrectly saying the current build is up to date. diff --git a/changes/macos-updater-zip-collision.md b/changes/macos-updater-zip-collision.md new file mode 100644 index 00000000..c4d1364d --- /dev/null +++ b/changes/macos-updater-zip-collision.md @@ -0,0 +1,4 @@ +type: fixed +area: release + +- Fixed macOS updater metadata mismatches by giving macOS and Windows ZIP release assets distinct build-time filenames. diff --git a/docs-site/installation.md b/docs-site/installation.md index 020c0cea..54cec3b6 100644 --- a/docs-site/installation.md +++ b/docs-site/installation.md @@ -173,7 +173,7 @@ If you prefer to install it manually, see [manual launcher install](#manual-laun Download the latest installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest): - `SubMiner-.exe` — installer (recommended) -- `SubMiner-.zip` — portable fallback +- `SubMiner--win.zip` — portable fallback Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config during first-run setup. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index a4bf694a..ec6b34e3 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -88,10 +88,11 @@ Notes: - AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed. - Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation. - Release and prerelease workflows upload updater metadata (`*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled. -- macOS tray app updates use the standard `electron-updater`/Squirrel path. Keep `latest-mac.yml`, the macOS ZIP, and ZIP blockmap published; Squirrel uses the ZIP payload even when the DMG remains the user-facing installer. +- macOS tray app updates use the standard `electron-updater`/Squirrel path. Keep `latest-mac.yml`, the macOS `SubMiner--mac.zip`, and ZIP blockmap published; Squirrel uses the ZIP payload even when the DMG remains the user-facing installer. - macOS update metadata and full ZIP downloads are routed through `/usr/bin/curl` before Squirrel installation to avoid Electron main-process network crashes on update checks. - Windows tray app updates use the standard `electron-updater`/NSIS path. Keep `latest.yml`, the Windows NSIS installer, and installer blockmap published; updater HTTP is routed through main-process fetch to avoid Electron main-process network crashes during update checks. +- Build config emits distinct ZIP names: `SubMiner--mac.zip` for the macOS Squirrel updater payload and `SubMiner--win.zip` for the Windows portable fallback. The user-facing DMG and Windows installer keep the unqualified `SubMiner-` basename. - Linux GitHub release metadata and asset downloads also use `/usr/bin/curl` instead of Electron networking for the same reason. -- Local macOS build-output apps outside `/Applications` or `~/Applications` skip native update checks. To validate auto-update end to end, install the signed and notarized app bundle into one of those Applications folders and point it at a published updater feed. +- Local macOS build-output apps outside `/Applications` or `~/Applications` skip native update checks. Manual tray and launcher checks still use GitHub release metadata to report newer releases, but automatic notifications stay quiet when native app installation is unsupported. To validate auto-update end to end, install the signed and notarized app bundle into one of those Applications folders and point it at a published updater feed. - The first updater-enabled release cannot update older installs automatically. Users need one manual install to get the updater code. - Stable auto-update checks ignore beta/RC prereleases by default. Set `updates.channel` to `"prerelease"` on a test install when validating beta/RC updater behavior. diff --git a/package.json b/package.json index df2d3a78..24f5bb59 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ ] }, "mac": { + "artifactName": "SubMiner-${version}-mac.${ext}", "target": [ "dmg", "zip" @@ -174,7 +175,11 @@ } ] }, + "dmg": { + "artifactName": "SubMiner-${version}.${ext}" + }, "win": { + "artifactName": "SubMiner-${version}-win.${ext}", "target": [ "nsis", "zip" @@ -182,6 +187,7 @@ "icon": "assets/SubMiner.ico" }, "nsis": { + "artifactName": "SubMiner-${version}.${ext}", "oneClick": false, "perMachine": false, "allowToChangeInstallationDirectory": true, diff --git a/src/main.ts b/src/main.ts index 39851182..416bbf54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5227,8 +5227,8 @@ function getUpdateService() { readState: () => updateStateStore.readState(), writeState: (state) => updateStateStore.writeState(state), checkAppUpdate: (channel) => appUpdater.checkForUpdates(channel), - shouldFetchReleaseMetadata: ({ appUpdate }) => - shouldFetchReleaseMetadataForPlatform(process.platform, appUpdate), + shouldFetchReleaseMetadata: ({ request, appUpdate }) => + shouldFetchReleaseMetadataForPlatform(process.platform, appUpdate, request), fetchLatestStableRelease: (channel) => fetchLatestStableRelease({ fetch: getFetchForUpdater(), channel }), updateLauncher: (launcherPath, channel, release) => diff --git a/src/main/runtime/update/release-metadata-policy.test.ts b/src/main/runtime/update/release-metadata-policy.test.ts index beb6c68f..08e91d30 100644 --- a/src/main/runtime/update/release-metadata-policy.test.ts +++ b/src/main/runtime/update/release-metadata-policy.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { shouldFetchReleaseMetadataForPlatform } from './release-metadata-policy'; -test('macOS release metadata fetch is skipped only when native updater is unsupported', () => { +test('macOS automatic release metadata fetch is skipped when native updater is unsupported', () => { assert.equal( shouldFetchReleaseMetadataForPlatform('darwin', { available: false, @@ -28,6 +28,33 @@ test('macOS release metadata fetch is skipped only when native updater is unsupp ); }); +test('macOS manual checks fetch release metadata when native updater is unsupported', () => { + const unsupportedUpdate = { + available: false, + version: '0.15.0-beta.4', + canUpdate: false, + }; + + assert.equal( + shouldFetchReleaseMetadataForPlatform('darwin', unsupportedUpdate, { + source: 'manual', + }), + true, + ); + assert.equal( + shouldFetchReleaseMetadataForPlatform('darwin', unsupportedUpdate, { + source: 'launcher', + }), + true, + ); + assert.equal( + shouldFetchReleaseMetadataForPlatform('darwin', unsupportedUpdate, { + source: 'automatic', + }), + false, + ); +}); + test('non-macOS release metadata fetch is not gated by native updater support', () => { assert.equal( shouldFetchReleaseMetadataForPlatform('linux', { diff --git a/src/main/runtime/update/release-metadata-policy.ts b/src/main/runtime/update/release-metadata-policy.ts index ef9fc9b4..b1a75e12 100644 --- a/src/main/runtime/update/release-metadata-policy.ts +++ b/src/main/runtime/update/release-metadata-policy.ts @@ -4,12 +4,20 @@ type AppUpdateMetadata = { canUpdate?: boolean; }; +type UpdateMetadataRequest = { + source?: 'manual' | 'automatic' | 'launcher'; +}; + export function shouldFetchReleaseMetadataForPlatform( platform: NodeJS.Platform, appUpdate: AppUpdateMetadata, + request: UpdateMetadataRequest = {}, ): boolean { if (platform !== 'darwin') { return true; } - return appUpdate.canUpdate !== false; + if (appUpdate.canUpdate !== false) { + return true; + } + return request.source === 'manual' || request.source === 'launcher'; } diff --git a/src/main/runtime/update/update-service.test.ts b/src/main/runtime/update/update-service.test.ts index 29709ee0..da01f7a7 100644 --- a/src/main/runtime/update/update-service.test.ts +++ b/src/main/runtime/update/update-service.test.ts @@ -1,5 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import { shouldFetchReleaseMetadataForPlatform } from './release-metadata-policy'; import { createUpdateService, type UpdateServiceDeps, type UpdateState } from './update-service'; function createDeps(overrides: Partial = {}) { @@ -362,6 +363,57 @@ test('manual prerelease update check uses prerelease release and launcher channe ]); }); +test('manual macOS prerelease check reports GitHub update when native updater is unsupported', async () => { + const { deps, calls } = createDeps({ + getConfig: () => ({ + enabled: true, + checkIntervalHours: 24, + notificationType: 'system', + channel: 'prerelease', + }), + getCurrentVersion: () => '0.15.0-beta.4', + checkAppUpdate: async (channel) => { + calls.push(`app:${channel}`); + return { + available: false, + version: '0.15.0-beta.4', + canUpdate: false, + }; + }, + shouldFetchReleaseMetadata: ({ request, appUpdate }) => + shouldFetchReleaseMetadataForPlatform('darwin', appUpdate, request), + fetchLatestStableRelease: async (channel) => { + calls.push(`fetch:${channel}`); + return { + tag_name: 'v0.15.0-beta.5', + prerelease: true, + draft: false, + assets: [], + }; + }, + showUpdateAvailableDialog: async (version) => { + calls.push(`available-dialog:${version}`); + return 'update'; + }, + updateLauncher: async (_launcherPath, channel, release) => { + calls.push(`launcher:${channel}:${release?.tag_name ?? 'none'}`); + return { status: 'skipped' }; + }, + }); + const service = createUpdateService(deps); + + const result = await service.checkForUpdates({ source: 'manual' }); + + assert.equal(result.status, 'update-available'); + assert.deepEqual(calls, [ + 'app:prerelease', + 'fetch:prerelease', + 'available-dialog:0.15.0-beta.5', + 'launcher:prerelease:v0.15.0-beta.5', + 'manual-install:0.15.0-beta.5', + ]); +}); + test('manual update check keeps current prerelease builds on configured stable channel', async () => { const { deps, calls } = createDeps({ getCurrentVersion: () => '0.15.0-beta.3', diff --git a/src/prerelease-workflow.test.ts b/src/prerelease-workflow.test.ts index 270db094..1b1ea5f9 100644 --- a/src/prerelease-workflow.test.ts +++ b/src/prerelease-workflow.test.ts @@ -87,6 +87,11 @@ test('prerelease workflow writes checksum entries using release asset basenames' ); }); +test('prerelease workflow relies on builder artifact names without post-build zip renames', () => { + assert.doesNotMatch(prereleaseWorkflow, /Rename Windows ZIP artifacts/); + assert.doesNotMatch(prereleaseWorkflow, /Rename-Item[\s\S]*-win\.zip/); +}); + test('prerelease workflow validates artifacts before publishing the release and only undrafts after upload', () => { const artifactsIndex = prereleaseWorkflow.indexOf('artifacts=('); const createIndex = prereleaseWorkflow.indexOf('gh release create'); diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 95076c17..7e152bf4 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -18,15 +18,28 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { afterPack?: string; electronUpdaterCompatibility?: string; files?: string[]; + artifactName?: string; + dmg?: { + artifactName?: string; + }; extraResources?: Array<{ from?: string; to?: string; }>; + mac?: { + artifactName?: string; + }; + nsis?: { + artifactName?: string; + }; publish?: Array<{ provider?: string; owner?: string; repo?: string; }>; + win?: { + artifactName?: string; + }; }; }; @@ -199,6 +212,15 @@ test('windows release workflow publishes unsigned artifacts directly without Sig assert.ok(!releaseWorkflow.includes('SIGNPATH_')); }); +test('release artifact names are distinct before upload', () => { + assert.equal(packageJson.build?.mac?.artifactName, 'SubMiner-${version}-mac.${ext}'); + assert.equal(packageJson.build?.dmg?.artifactName, 'SubMiner-${version}.${ext}'); + assert.equal(packageJson.build?.win?.artifactName, 'SubMiner-${version}-win.${ext}'); + assert.equal(packageJson.build?.nsis?.artifactName, 'SubMiner-${version}.${ext}'); + assert.doesNotMatch(releaseWorkflow, /Rename Windows ZIP artifacts/); + assert.doesNotMatch(releaseWorkflow, /Rename-Item[\s\S]*-win\.zip/); +}); + test('release workflow publishes subminer-bin to AUR from tagged release artifacts', () => { assert.match(releaseWorkflow, /aur-publish:/); assert.match(releaseWorkflow, /needs:\s*\[release\]/);