From f7741a55f48d023d421ea05bbb26cb8a8ecda555 Mon Sep 17 00:00:00 2001 From: 233 <85852251+wfion@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:52:14 +0800 Subject: [PATCH] feat(updater): support portable auto updates --- .github/workflows/release.yml | 28 +- Cargo.lock | 3 + .../components/layout/UpdateDialog.spec.ts | 11 +- .../src/components/layout/UpdateDialog.vue | 2 +- apps/desktop/src/composables/useAppUpdater.ts | 6 +- apps/desktop/src/i18n/locales/en.ts | 2 +- apps/desktop/src/i18n/locales/es.ts | 2 +- apps/desktop/src/i18n/locales/it.ts | 2 +- apps/desktop/src/i18n/locales/ja.ts | 2 +- apps/desktop/src/i18n/locales/pt-BR.ts | 2 +- apps/desktop/src/i18n/locales/zh-CN.ts | 2 +- apps/desktop/src/i18n/locales/zh-TW.ts | 2 +- packages/app-tests/appUpdater.test.ts | 4 +- src-tauri/Cargo.toml | 3 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/update.rs | 248 ++++++++++- src-tauri/src/commands/update_portable.rs | 405 ++++++++++++++++++ 17 files changed, 692 insertions(+), 33 deletions(-) create mode 100644 src-tauri/src/commands/update_portable.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1462c5e09..fd0cdcd0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -258,11 +258,17 @@ jobs: $portableDir = Join-Path $portableRoot "DBX_${version}_${arch}" $zipName = "DBX_${version}_${arch}-portable.zip" $exePath = "target/${{ matrix.target }}/release/dbx.exe" + $cargoMetadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $appVersion = ($cargoMetadata.packages | Where-Object { $_.name -eq "dbx" } | Select-Object -First 1).version if (!(Test-Path $exePath)) { Write-Error "Missing Windows executable: $exePath" exit 1 } + if (!$appVersion -or $appVersion -ne $version) { + Write-Error "Release tag version $version does not match the built DBX package version $appVersion" + exit 1 + } New-Item -ItemType Directory -Force -Path $portableDir | Out-Null Copy-Item $exePath (Join-Path $portableDir "DBX.exe") -Force @@ -270,8 +276,26 @@ jobs: Copy-Item "README.md" (Join-Path $portableDir "README.md") -Force Set-Content -Path (Join-Path $portableDir "portable.dbx") -Value "" -NoNewline + $portableExe = Join-Path $portableDir "DBX.exe" + $manifest = [ordered]@{ + schema_version = 1 + version = $appVersion + arch = $arch + executable = "DBX.exe" + executable_sha256 = (Get-FileHash -LiteralPath $portableExe -Algorithm SHA256).Hash.ToLowerInvariant() + } | ConvertTo-Json + Set-Content -Path (Join-Path $portableDir "portable-update.json") -Value $manifest -Encoding utf8NoBOM + Compress-Archive -Path (Join-Path $portableDir "*") -DestinationPath $zipName -Force - gh release upload "${env:GITHUB_REF_NAME}" $zipName --repo "${env:GITHUB_REPOSITORY}" --clobber + pnpm tauri signer sign $zipName + + $signatureName = "${zipName}.sig" + if (!(Test-Path $signatureName)) { + Write-Error "Missing portable update signature: $signatureName" + exit 1 + } + + gh release upload "${env:GITHUB_REF_NAME}" $zipName $signatureName --repo "${env:GITHUB_REPOSITORY}" --clobber - name: Upload Windows WebView2 offline installer if: matrix.arch == 'x64' || matrix.arch == 'arm64' @@ -317,7 +341,7 @@ jobs: gh release view "${GITHUB_REF_NAME}" \ --repo "${GITHUB_REPOSITORY}" \ --json assets \ - --jq '.assets[].name | select(endswith(".sig"))' + --jq '.assets[].name | select(endswith(".sig") and (endswith("-portable.zip.sig") | not))' ) if [ "${#SIG_ASSETS[@]}" -eq 0 ]; then diff --git a/Cargo.lock b/Cargo.lock index f4c050630..8cad18cf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,6 +1825,7 @@ dependencies = [ "futures", "libloading 0.8.9", "log", + "minisign-verify", "mongodb", "objc2", "objc2-app-kit", @@ -1836,8 +1837,10 @@ dependencies = [ "russh", "rust_decimal", "rustls 0.23.40", + "semver", "serde", "serde_json", + "sha2 0.10.9", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", diff --git a/apps/desktop/src/components/layout/UpdateDialog.spec.ts b/apps/desktop/src/components/layout/UpdateDialog.spec.ts index 298c86d11..3a53913b8 100644 --- a/apps/desktop/src/components/layout/UpdateDialog.spec.ts +++ b/apps/desktop/src/components/layout/UpdateDialog.spec.ts @@ -13,6 +13,7 @@ const mountedApps: App[] = []; interface DialogState { open: boolean; + portableMode: boolean; isDownloadingUpdate: boolean; downloadProgress: number; updateDownloaded: boolean; @@ -28,6 +29,7 @@ async function flushDialog() { async function mountDialog(activeTaskCount: number, initialState: Partial = {}, installDownloaded = vi.fn(async () => {})) { const state = reactive({ open: true, + portableMode: false, isDownloadingUpdate: false, downloadProgress: 0, updateDownloaded: false, @@ -64,7 +66,7 @@ async function mountDialog(activeTaskCount: number, initialState: Partial { expect(downloadButton()?.disabled).toBe(false); }); + it("offers automatic installation for portable builds", async () => { + await mountDialog(0, { portableMode: true }); + + expect(document.body.textContent).toContain("portable ZIP"); + expect(downloadButton()?.disabled).toBe(false); + }); + it("retains the downloaded update and enables installation only after tasks finish", async () => { await mountDialog(1, { updateDownloaded: true, downloadProgress: 100 }); diff --git a/apps/desktop/src/components/layout/UpdateDialog.vue b/apps/desktop/src/components/layout/UpdateDialog.vue index 93f658dda..fd37a6073 100644 --- a/apps/desktop/src/components/layout/UpdateDialog.vue +++ b/apps/desktop/src/components/layout/UpdateDialog.vue @@ -107,7 +107,7 @@ watch( {{ t("updates.toUpdate") }}

- {{ t("updates.portableManualUpdate") }} + {{ t("updates.portableAutomaticUpdate") }}