diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ab8d1d..9523c5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,8 @@ env: # Ubuntu 22.04 and 24.04, Windows MSVC + WebView2 on windows-2025. The 22.04 # packages are extra release assets (`*_ubuntu2204_*`) because that glibc is # too old for a binary built on 24.04. Updater latest.json stays on the 24.04 -# deb and AppImage. Do not cross-compile the Windows installer from Linux here; +# deb and AppImage, plus the signed Apple Silicon disk image (ADR 0126). +# Do not cross-compile the Windows installer from Linux here; # the Linux Tauri CLI rejects --bundles nsis. concurrency: @@ -234,6 +235,18 @@ jobs: TAURI_PRIVATE_KEY="${TAURI_SIGNING_PRIVATE_KEY}" \ TAURI_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \ pnpm exec tauri signer sign "${debs[0]}" + - name: Sign macOS updater package + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + dmgs=(artifacts/macos/*.dmg) + (( ${#dmgs[@]} == 1 )) || { echo "Expected one macOS disk image to sign"; exit 1; } + TAURI_PRIVATE_KEY="${TAURI_SIGNING_PRIVATE_KEY}" \ + TAURI_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \ + pnpm exec tauri signer sign "${dmgs[0]}" + [[ -f "${dmgs[0]}.sig" ]] || { echo "macOS updater signature was not written"; exit 1; } - name: Stage portable Windows app if: runner.os == 'Windows' shell: bash @@ -270,7 +283,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: release-macos - path: artifacts/macos/*.dmg + path: | + artifacts/macos/*.dmg + artifacts/macos/*.dmg.sig if-no-files-found: error retention-days: 7 @@ -321,10 +336,11 @@ jobs: mapfile -t installers < <(find release-assets -type f -name '*-setup.exe' -print | sort) mapfile -t installer_sigs < <(find release-assets -type f -name '*-setup.exe.sig' -print | sort) mapfile -t manifests < <(find release-assets -type f -name 'latest.json' -print | sort) - mapfile -t dmgs < <(find release-assets -type f -name '*.dmg' -print | sort) + mapfile -t dmgs < <(find release-assets -type f -name '*.dmg' ! -name '*.dmg.sig' -print | sort) + mapfile -t dmg_sigs < <(find release-assets -type f -name '*.dmg.sig' -print | sort) - if (( ${#debs[@]} != 1 || ${#deb_sigs[@]} != 1 || ${#appimages[@]} != 1 || ${#appimage_sigs[@]} != 1 || ${#ubuntu_debs[@]} != 1 || ${#ubuntu_deb_sigs[@]} != 1 || ${#ubuntu_appimages[@]} != 1 || ${#ubuntu_appimage_sigs[@]} != 1 || ${#portable[@]} != 1 || ${#installers[@]} != 1 || ${#installer_sigs[@]} != 1 || ${#manifests[@]} != 1 || ${#dmgs[@]} != 1 )); then - echo "Expected Ubuntu 24.04 and Ubuntu 22.04 deb/AppImage pairs with signatures, one portable exe, one NSIS installer and signature, one macOS disk image, and latest.json" + if (( ${#debs[@]} != 1 || ${#deb_sigs[@]} != 1 || ${#appimages[@]} != 1 || ${#appimage_sigs[@]} != 1 || ${#ubuntu_debs[@]} != 1 || ${#ubuntu_deb_sigs[@]} != 1 || ${#ubuntu_appimages[@]} != 1 || ${#ubuntu_appimage_sigs[@]} != 1 || ${#portable[@]} != 1 || ${#installers[@]} != 1 || ${#installer_sigs[@]} != 1 || ${#manifests[@]} != 1 || ${#dmgs[@]} != 1 || ${#dmg_sigs[@]} != 1 )); then + echo "Expected Ubuntu 24.04 and Ubuntu 22.04 deb/AppImage pairs with signatures, one portable exe, one NSIS installer and signature, one signed macOS disk image, and latest.json" find release-assets -type f -print exit 1 fi @@ -337,7 +353,7 @@ jobs: git push origin "refs/tags/${tag}" fi - assets=("${debs[@]}" "${deb_sigs[@]}" "${appimages[@]}" "${appimage_sigs[@]}" "${ubuntu_debs[@]}" "${ubuntu_deb_sigs[@]}" "${ubuntu_appimages[@]}" "${ubuntu_appimage_sigs[@]}" "${portable[@]}" "${installers[@]}" "${installer_sigs[@]}" "${dmgs[@]}" "${manifests[@]}") + assets=("${debs[@]}" "${deb_sigs[@]}" "${appimages[@]}" "${appimage_sigs[@]}" "${ubuntu_debs[@]}" "${ubuntu_deb_sigs[@]}" "${ubuntu_appimages[@]}" "${ubuntu_appimage_sigs[@]}" "${portable[@]}" "${installers[@]}" "${installer_sigs[@]}" "${dmgs[@]}" "${dmg_sigs[@]}" "${manifests[@]}") if ! gh release view "${tag}" >/dev/null 2>&1; then gh release create "${tag}" \ --draft \ diff --git a/AGENTS.md b/AGENTS.md index 428e447..fe4e9c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -764,3 +764,11 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the status icon while that title is shown and restore the retained image when it is cleared. Widen both lines together when a fixed unit is longer (ADR 0122). + +- Apple Silicon in-app update asks the Tauri updater for platform + `macos-aarch64`. A `latest.json` that only lists Linux and Windows fails + with "the platform macos-aarch64 was not found in the response platforms + object" even when the GitHub Release already has + `BiFlow_*_aarch64.dmg`. Sign that disk image in the release job and publish + it as the `macos-aarch64` entry. The platform key must stay identical to + `signed_target` for `InstallKind::Dmg` on `aarch64` (ADR 0126). diff --git a/Cargo.lock b/Cargo.lock index d1e8d86..2c6a250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,7 +2046,7 @@ dependencies = [ [[package]] name = "iran-split-cli" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2064,7 +2064,7 @@ dependencies = [ [[package]] name = "iran-split-clients" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "ipnet", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "iran-split-config" -version = "6.2.74" +version = "6.2.75" dependencies = [ "hex", "rand 0.9.5", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "iran-split-core" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "iran-split-desktop" -version = "6.2.74" +version = "6.2.75" dependencies = [ "chrono", "dirs 6.0.0", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "iran-split-helper" -version = "6.2.74" +version = "6.2.75" dependencies = [ "clap", "hex", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iran-split-helper-winacl" -version = "6.2.74" +version = "6.2.75" dependencies = [ "tokio", "windows", @@ -2185,7 +2185,7 @@ dependencies = [ [[package]] name = "iran-split-ipc" -version = "6.2.74" +version = "6.2.75" dependencies = [ "bytes", "chrono", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "iran-split-mihomo" -version = "6.2.74" +version = "6.2.75" dependencies = [ "chrono", "futures-util", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "iran-split-netstats" -version = "6.2.74" +version = "6.2.75" dependencies = [ "libc", "tempfile", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "iran-split-platform-linux" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2254,7 +2254,7 @@ dependencies = [ [[package]] name = "iran-split-platform-macos" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "iran-split-platform-win" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2301,7 +2301,7 @@ dependencies = [ [[package]] name = "iran-split-rules" -version = "6.2.74" +version = "6.2.75" dependencies = [ "async-trait", "chrono", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "iran-split-taskbar" -version = "6.2.74" +version = "6.2.75" dependencies = [ "iran-split-config", "objc2", diff --git a/Cargo.toml b/Cargo.toml index 0035845..de75fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "6.2.74" +version = "6.2.75" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.88" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index eb1bc13..0b70f3b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@iran-split/desktop", - "version": "6.2.74", + "version": "6.2.75", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/tsconfig.app.tsbuildinfo b/apps/desktop/tsconfig.app.tsbuildinfo index b925587..63b108b 100644 --- a/apps/desktop/tsconfig.app.tsbuildinfo +++ b/apps/desktop/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.test.tsx","./src/App.tsx","./src/installContextMenuGuard.ts","./src/main.tsx","./src/palette.test.ts","./src/shell.test.ts","./src/version.test.ts","./src/version.ts","./src/vite-env.d.ts","./src/api/desktop.native.test.ts","./src/api/desktop.ts","./src/api/mock.test.ts","./src/api/mock.ts","./src/api/models.ts","./src/components/About.test.tsx","./src/components/About.tsx","./src/components/AddSiteBar.tsx","./src/components/AppButton.test.tsx","./src/components/AppButton.tsx","./src/components/AppStatusBar.test.tsx","./src/components/AppStatusBar.tsx","./src/components/BasicDashboard.test.tsx","./src/components/BasicDashboard.tsx","./src/components/BottomNav.test.tsx","./src/components/BottomNav.tsx","./src/components/ClientRegistry.test.tsx","./src/components/ClientRegistry.tsx","./src/components/ConnectionActionButton.test.tsx","./src/components/ConnectionActionButton.tsx","./src/components/Dashboard.test.tsx","./src/components/Dashboard.tsx","./src/components/DefaultRouteSelect.tsx","./src/components/Diagnostics.test.tsx","./src/components/Diagnostics.tsx","./src/components/DirectRules.test.tsx","./src/components/DirectRules.tsx","./src/components/InputContextMenu.test.tsx","./src/components/InputContextMenu.tsx","./src/components/LifecycleCancelButton.tsx","./src/components/MihomoConfigDialog.tsx","./src/components/PageSkeleton.tsx","./src/components/RecentSites.test.tsx","./src/components/RecentSites.tsx","./src/components/Settings.test.tsx","./src/components/Settings.tsx","./src/components/SettingsApplyBanner.test.tsx","./src/components/SettingsApplyBanner.tsx","./src/components/SortHeader.tsx","./src/components/StatusPill.tsx","./src/components/TrafficFlow.tsx","./src/components/UiModeSwitch.test.tsx","./src/components/UiModeSwitch.tsx","./src/components/country.ts","./src/components/ui/InfoTip.tsx","./src/components/ui/PageHeader.tsx","./src/components/ui/Tabs.tsx","./src/components/ui/Toast.tsx","./src/components/ui/usePageTab.ts","./src/dev/local-deps.test.ts","./src/i18n/config.ts","./src/lib/clients.test.ts","./src/lib/clients.ts","./src/lib/clipboard.ts","./src/lib/connectComponentProgress.test.ts","./src/lib/connectRequirements.test.ts","./src/lib/connectRequirements.ts","./src/lib/connectionGroups.test.ts","./src/lib/connectionGroups.ts","./src/lib/connectionProgress.test.ts","./src/lib/connectionProgress.ts","./src/lib/directDns.test.ts","./src/lib/directDns.ts","./src/lib/editableTarget.test.ts","./src/lib/editableTarget.ts","./src/lib/failureReason.test.ts","./src/lib/failureReason.ts","./src/lib/formatTraffic.test.ts","./src/lib/formatTraffic.ts","./src/lib/hiddenHosts.test.ts","./src/lib/hiddenHosts.ts","./src/lib/host.test.ts","./src/lib/host.ts","./src/lib/lifecycle.test.ts","./src/lib/lifecycle.ts","./src/lib/navigation.ts","./src/lib/outbound.ts","./src/lib/presets.test.ts","./src/lib/presets.ts","./src/lib/settingsApply.test.ts","./src/lib/settingsApply.ts","./src/lib/sideTunnelConnect.test.ts","./src/lib/sideTunnelConnect.ts","./src/lib/tableSort.test.ts","./src/lib/tableSort.ts","./src/lib/uiMode.ts","./src/lib/viewport.test.ts","./src/lib/viewport.ts","./src/lib/yamlHighlight.test.ts","./src/lib/yamlHighlight.ts","./src/store/app.test.ts","./src/store/app.ts","./src/test/fixtures.ts","./src/test/localStorage-shim.ts","./src/test/setup.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.test.tsx","./src/app.tsx","./src/installcontextmenuguard.ts","./src/main.tsx","./src/palette.test.ts","./src/shell.test.ts","./src/version.test.ts","./src/version.ts","./src/vite-env.d.ts","./src/api/desktop.native.test.ts","./src/api/desktop.ts","./src/api/mock.test.ts","./src/api/mock.ts","./src/api/models.ts","./src/components/about.test.tsx","./src/components/about.tsx","./src/components/addsitebar.tsx","./src/components/appbutton.test.tsx","./src/components/appbutton.tsx","./src/components/appstatusbar.test.tsx","./src/components/appstatusbar.tsx","./src/components/basicdashboard.test.tsx","./src/components/basicdashboard.tsx","./src/components/bottomnav.test.tsx","./src/components/bottomnav.tsx","./src/components/clientregistry.test.tsx","./src/components/clientregistry.tsx","./src/components/connectionactionbutton.test.tsx","./src/components/connectionactionbutton.tsx","./src/components/dashboard.test.tsx","./src/components/dashboard.tsx","./src/components/defaultrouteselect.tsx","./src/components/diagnostics.test.tsx","./src/components/diagnostics.tsx","./src/components/directrules.test.tsx","./src/components/directrules.tsx","./src/components/inputcontextmenu.test.tsx","./src/components/inputcontextmenu.tsx","./src/components/lifecyclecancelbutton.tsx","./src/components/mihomoconfigdialog.tsx","./src/components/pageskeleton.tsx","./src/components/recentsites.test.tsx","./src/components/recentsites.tsx","./src/components/settings.test.tsx","./src/components/settings.tsx","./src/components/settingsapplybanner.test.tsx","./src/components/settingsapplybanner.tsx","./src/components/sortheader.tsx","./src/components/statuspill.tsx","./src/components/trafficflow.tsx","./src/components/uimodeswitch.test.tsx","./src/components/uimodeswitch.tsx","./src/components/country.ts","./src/components/ui/infotip.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/ui/usepagetab.ts","./src/dev/local-deps.test.ts","./src/i18n/config.ts","./src/lib/clients.test.ts","./src/lib/clients.ts","./src/lib/clipboard.ts","./src/lib/connectcomponentprogress.test.ts","./src/lib/connectrequirements.test.ts","./src/lib/connectrequirements.ts","./src/lib/connectiongroups.test.ts","./src/lib/connectiongroups.ts","./src/lib/connectionprogress.test.ts","./src/lib/connectionprogress.ts","./src/lib/directdns.test.ts","./src/lib/directdns.ts","./src/lib/editabletarget.test.ts","./src/lib/editabletarget.ts","./src/lib/failurereason.test.ts","./src/lib/failurereason.ts","./src/lib/formattraffic.test.ts","./src/lib/formattraffic.ts","./src/lib/hiddenhosts.test.ts","./src/lib/hiddenhosts.ts","./src/lib/host.test.ts","./src/lib/host.ts","./src/lib/lifecycle.test.ts","./src/lib/lifecycle.ts","./src/lib/navigation.ts","./src/lib/outbound.ts","./src/lib/presets.test.ts","./src/lib/presets.ts","./src/lib/settingsapply.test.ts","./src/lib/settingsapply.ts","./src/lib/sidetunnelconnect.test.ts","./src/lib/sidetunnelconnect.ts","./src/lib/tablesort.test.ts","./src/lib/tablesort.ts","./src/lib/uimode.ts","./src/lib/viewport.test.ts","./src/lib/viewport.ts","./src/lib/yamlhighlight.test.ts","./src/lib/yamlhighlight.ts","./src/store/app.test.ts","./src/store/app.ts","./src/test/fixtures.ts","./src/test/localstorage-shim.ts","./src/test/setup.ts"],"version":"5.8.3"} \ No newline at end of file diff --git a/docs/adr/0123-publish-macos-release.md b/docs/adr/0123-publish-macos-release.md index 4efd73d..6ae1c01 100644 --- a/docs/adr/0123-publish-macos-release.md +++ b/docs/adr/0123-publish-macos-release.md @@ -14,8 +14,9 @@ version file still publishes, including a re-run. The macOS job runs fallback, and the publish job uploads `BiFlow__aarch64.dmg` next to the signed Linux and Windows packages. -The disk image is a manual download. It is not an updater target: `latest.json` -still lists only the signed Debian package, AppImage, and NSIS installer. +Apple Silicon in-app updates install this disk image. +[0126](./0126-macos-updater-platform.md) signs it and publishes the +`macos-aarch64` platform in `latest.json`. The publish job creates the missing tag itself. A tag pushed with the workflow token does not start another workflow, so the tag cannot be relied on diff --git a/docs/adr/0126-macos-updater-platform.md b/docs/adr/0126-macos-updater-platform.md new file mode 100644 index 0000000..2c9ec10 --- /dev/null +++ b/docs/adr/0126-macos-updater-platform.md @@ -0,0 +1,21 @@ +# ADR 0126: Publish the Apple Silicon disk image as an updater platform + +Status: Accepted (6.2.75) + +About → Check for updates on Apple Silicon asks the Tauri updater for platform +`macos-aarch64`. Release 6.2.74 uploaded `BiFlow_6.2.74_aarch64.dmg` but +`latest.json` listed only `linux-deb-x86_64`, `linux-x86_64`, and +`windows-x86_64`. The updater then failed with "the platform macos-aarch64 was +not found in the response platforms object" before the disk image could be +installed. + +The macOS release job signs `artifacts/macos/BiFlow__aarch64.dmg` +with the same minisign key used for the Debian package and uploads the `.sig` +beside the disk image. `scripts/generate-latest-json.mjs` publishes that pair +as `platforms["macos-aarch64"]`. The key stays identical to +`signed_target(InstallKind::Dmg)` on `aarch64`. Intel Macs are still not an +updater target; this release builds only the Apple Silicon disk image. + +An already published release cannot grow this platform after the fact: the +6.2.74 disk image has no signature. The next version bump publishes a manifest +that installed Apple Silicon builds can install. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1711af9..2583490 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -129,3 +129,4 @@ Keep this index current. Add a new ADR for each non-obvious change, or update th | [0123](./0123-publish-macos-release.md) | Publish the macOS disk image when the version reaches main | Accepted | | [0124](./0124-pr-validation-and-safe-archives.md) | Cross-platform PR validation and safe archive dependencies | Accepted | | [0125](./0125-native-release-bootstrap.md) | Native release bootstrap on macOS and Windows | Accepted | +| [0126](./0126-macos-updater-platform.md) | Publish the Apple Silicon disk image as an updater platform | Accepted | diff --git a/package.json b/package.json index 2a7af55..ef0648f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iran-split-desktop-workspace", - "version": "6.2.74", + "version": "6.2.75", "private": true, "packageManager": "pnpm@9.0.1", "engines": { diff --git a/scripts/build-plan.test.mjs b/scripts/build-plan.test.mjs index e97a1f2..5edfde1 100644 --- a/scripts/build-plan.test.mjs +++ b/scripts/build-plan.test.mjs @@ -496,7 +496,10 @@ describe("release artifact names", () => { assert.match(workflow, /platform: macos-15/); assert.match(workflow, /\.\/build\.sh ci-macos/); assert.match(workflow, /artifacts\/macos\/\*\.dmg/); + assert.match(workflow, /artifacts\/macos\/\*\.dmg\.sig/); + assert.match(workflow, /Sign macOS updater package/); assert.match(workflow, /-name '\*\.dmg'/); + assert.match(workflow, /-name '\*\.dmg\.sig'/); assert.match(workflow, /git push origin "refs\/tags\/\$\{tag\}"/); assert.doesNotMatch(workflow, /ref_protected/); assert.match(workflow, /--bundles deb,appimage/); diff --git a/scripts/generate-latest-json.mjs b/scripts/generate-latest-json.mjs index 0291d4f..06ca71e 100644 --- a/scripts/generate-latest-json.mjs +++ b/scripts/generate-latest-json.mjs @@ -3,7 +3,14 @@ import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; -const PLATFORM_KEYS = ["linux-deb-x86_64", "linux-x86_64", "windows-x86_64"]; +const PLATFORM_KEYS = [ + "linux-deb-x86_64", + "linux-x86_64", + "windows-x86_64", + // Apple Silicon asks the updater for this key. Keep it identical to + // `signed_target` in src-tauri/src/github_update.rs. + "macos-aarch64", +]; const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; @@ -38,6 +45,7 @@ export function buildLatestJson(repo, releaseTag, artifacts, options = {}) { "linux-deb-x86_64": `BiFlow_${version}_amd64.deb`, "linux-x86_64": `BiFlow_${version}_amd64.AppImage`, "windows-x86_64": `BiFlow_${version}_x64-setup.exe`, + "macos-aarch64": `BiFlow_${version}_aarch64.dmg`, }; for (const artifact of artifacts) { @@ -193,6 +201,16 @@ export function discoverSignedArtifacts(directory) { }); } + const dmg = findUniqueBundle(files, "_aarch64.dmg"); + if (dmg) { + artifacts.push({ + platform: "macos-aarch64", + bundlePath: dmg, + fileName: basename(dmg), + signature: readSignature(`${dmg}.sig`), + }); + } + return artifacts; } diff --git a/scripts/generate-latest-json.test.mjs b/scripts/generate-latest-json.test.mjs index 9d30235..2c7329e 100644 --- a/scripts/generate-latest-json.test.mjs +++ b/scripts/generate-latest-json.test.mjs @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { describe, it } from "node:test"; +import { fileURLToPath } from "node:url"; import { buildLatestJson, discoverSignedArtifacts, @@ -11,6 +12,7 @@ import { validateLatestJson, } from "./generate-latest-json.mjs"; +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const FAKE_SIG = "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTVGVzdEtleVNpZ25hdHVyZQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzAwMDAwMDAwCWZpbGU6dGVzdC5idW5kbGUKdGVzdA=="; @@ -26,7 +28,7 @@ describe("generate-latest-json", () => { assert.equal(normalizeVersion("1.2.0"), "1.2.0"); }); - it("builds a signed manifest for deb, AppImage, and NSIS", () => { + it("builds a signed manifest for deb, AppImage, NSIS, and the Apple Silicon disk image", () => { const manifest = buildLatestJson( "devlifeX/BiFlow", "v1.2.0", @@ -49,6 +51,12 @@ describe("generate-latest-json", () => { fileName: "BiFlow_1.2.0_x64-setup.exe", signature: FAKE_SIG, }, + { + platform: "macos-aarch64", + bundlePath: "/tmp/BiFlow_1.2.0_aarch64.dmg", + fileName: "BiFlow_1.2.0_aarch64.dmg", + signature: FAKE_SIG, + }, ], { notes: "Signed updater test", pubDate: "2026-08-13T12:00:00.000Z" }, ); @@ -64,6 +72,11 @@ describe("generate-latest-json", () => { "https://github.com/devlifeX/BiFlow/releases/download/v1.2.0/BiFlow_1.2.0_amd64.AppImage", ); assert.equal(manifest.platforms["windows-x86_64"].signature, FAKE_SIG); + assert.equal( + manifest.platforms["macos-aarch64"].url, + "https://github.com/devlifeX/BiFlow/releases/download/v1.2.0/BiFlow_1.2.0_aarch64.dmg", + ); + assert.equal(manifest.platforms["macos-aarch64"].signature, FAKE_SIG); validateLatestJson(manifest); }); @@ -98,12 +111,14 @@ describe("generate-latest-json", () => { writeFakeBundle(directory, "BiFlow_1.2.0_amd64.deb"); writeFakeBundle(directory, "BiFlow_1.2.0_amd64.AppImage"); writeFakeBundle(directory, "BiFlow_1.2.0_x64-setup.exe"); + writeFakeBundle(directory, "BiFlow_1.2.0_aarch64.dmg"); const artifacts = discoverSignedArtifacts(directory); - assert.equal(artifacts.length, 3); + assert.equal(artifacts.length, 4); assert.deepEqual(artifacts.map((artifact) => artifact.platform).sort(), [ "linux-deb-x86_64", "linux-x86_64", + "macos-aarch64", "windows-x86_64", ]); @@ -132,6 +147,7 @@ describe("generate-latest-json", () => { ); writeFileSync(join(directory, "BiFlow.exe"), "portable", "utf8"); writeFakeBundle(join(directory, "deb"), "BiFlow_1.2.16_amd64.deb"); + writeFakeBundle(join(directory, "macos"), "BiFlow_1.2.16_aarch64.dmg"); const manifest = generateLatestJsonFromDirectory( directory, @@ -150,6 +166,10 @@ describe("generate-latest-json", () => { manifest.platforms["windows-x86_64"].url, "https://github.com/devlifeX/BiFlow/releases/download/v1.2.16/BiFlow_1.2.16_x64-setup.exe", ); + assert.equal( + manifest.platforms["macos-aarch64"].url, + "https://github.com/devlifeX/BiFlow/releases/download/v1.2.16/BiFlow_1.2.16_aarch64.dmg", + ); validateLatestJson(manifest); }); @@ -169,8 +189,10 @@ describe("generate-latest-json", () => { "BiFlow_1.2.16_ubuntu2204_amd64.deb", ); writeFakeBundle(join(directory, "nsis"), "BiFlow_1.2.16_x64-setup.exe"); + writeFakeBundle(join(directory, "macos"), "BiFlow_1.2.16_aarch64.dmg"); const artifacts = discoverSignedArtifacts(directory); assert.deepEqual(artifacts.map((artifact) => artifact.fileName).sort(), [ + "BiFlow_1.2.16_aarch64.dmg", "BiFlow_1.2.16_amd64.AppImage", "BiFlow_1.2.16_amd64.deb", "BiFlow_1.2.16_x64-setup.exe", @@ -207,7 +229,7 @@ describe("generate-latest-json", () => { ); }); - it("requires all three signed updater packages before publishing", () => { + it("requires every signed updater package, including the Apple Silicon disk image, before publishing", () => { const directory = mkdtempSync( join(tmpdir(), "biflow-latest-json-missing-"), ); @@ -216,7 +238,7 @@ describe("generate-latest-json", () => { assert.throws( () => generateLatestJsonFromDirectory(directory, "devlifeX/BiFlow", "v1.2.0"), - /expected 3 signed updater artifacts/, + /expected 4 signed updater artifacts/, ); }); @@ -234,4 +256,27 @@ describe("generate-latest-json", () => { /unexpected linux-deb-x86_64 asset/, ); }); + + it("uses the same macos-aarch64 key the Apple Silicon updater requests", () => { + const source = readFileSync( + join(root, "src-tauri/src/github_update.rs"), + "utf8", + ); + assert.match(source, /"macos-aarch64"/); + const directory = mkdtempSync(join(tmpdir(), "biflow-latest-json-macos-")); + writeFakeBundle(directory, "BiFlow_6.2.75_amd64.deb"); + writeFakeBundle(directory, "BiFlow_6.2.75_amd64.AppImage"); + writeFakeBundle(directory, "BiFlow_6.2.75_x64-setup.exe"); + writeFakeBundle(directory, "BiFlow_6.2.75_aarch64.dmg"); + const manifest = generateLatestJsonFromDirectory( + directory, + "devlifeX/BiFlow", + "v6.2.75", + ); + assert.ok(manifest.platforms["macos-aarch64"]); + assert.equal( + manifest.platforms["macos-aarch64"].url, + "https://github.com/devlifeX/BiFlow/releases/download/v6.2.75/BiFlow_6.2.75_aarch64.dmg", + ); + }); }); diff --git a/src-tauri/src/github_update.rs b/src-tauri/src/github_update.rs index 45bf2f7..871d317 100644 --- a/src-tauri/src/github_update.rs +++ b/src-tauri/src/github_update.rs @@ -160,6 +160,12 @@ pub fn pick_asset(release: &Release, kind: InstallKind) -> Result }) } +/// Platform key that `latest.json` must publish for this install kind. +/// +/// Apple Silicon requests `macos-aarch64`. The updater fails with that +/// platform missing from the response `platforms` object when the manifest +/// lists only Linux and Windows. Keep the string aligned with +/// `scripts/generate-latest-json.mjs`. #[must_use] pub const fn signed_target(kind: InstallKind) -> &'static str { match kind { @@ -792,6 +798,12 @@ mod tests { } } + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + #[test] + fn apple_silicon_updater_target_is_macos_aarch64() { + assert_eq!(signed_target(InstallKind::Dmg), "macos-aarch64"); + } + #[test] fn install_kind_prefers_appimage_env_on_linux() { assert_eq!( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7720cc6..68e8441 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BiFlow", - "version": "6.2.74", + "version": "6.2.75", "identifier": "app.biflow.desktop", "build": { "beforeDevCommand": "pnpm bundle:check && pnpm --dir apps/desktop dev", diff --git a/version b/version index e0de9dc..69b768b 100644 --- a/version +++ b/version @@ -1 +1 @@ -6.2.74 +6.2.75