fix(portable): in-app update the portable version app, closes #2983 (#2988)

This commit is contained in:
Huang Xin
2026-01-18 11:20:15 +01:00
committed by GitHub
parent 2670b0dc0b
commit 36b7386e30
3 changed files with 120 additions and 2 deletions
+38
View File
@@ -353,6 +353,44 @@ jobs:
echo "Uploading $bin_file to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
echo "Signing portable binary"
pnpm tauri signer sign $bin_file
echo "Uploading signature to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file.sig --clobber
- name: download and update latest.json for Windows portable release
if: matrix.config.os == 'windows-latest'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
curl -sL https://github.com/readest/readest/releases/latest/download/latest.json -o latest.json
version=${{ needs.get-release.outputs.release_version }}
arch=${{ matrix.config.arch }}
if [ "$arch" = "x86_64" ]; then
bin_file="Readest_${version}_x64-portable.exe"
platform_key="windows-x86_64-portable"
elif [ "$arch" = "aarch64" ]; then
bin_file="Readest_${version}_arm64-portable.exe"
platform_key="windows-aarch64-portable"
else
echo "Unknown architecture: $arch"
exit 1
fi
portable_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/$bin_file"
portable_sig=$(cat $bin_file.sig)
jq --arg url "$portable_url" \
--arg sig "$portable_sig" \
--arg key "$platform_key" \
'.platforms[$key] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json
echo "Uploading updated latest.json to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
upload-to-r2:
needs: [get-release, build-tauri]
uses: ./.github/workflows/upload-to-r2.yml
@@ -152,6 +152,16 @@
}
]
},
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "start-readest",
"cmd": "cmd",
"args": ["/C", "start", "", { "validator": "^.*Readest(.*)\\.exe$" }]
}
]
},
"haptics:allow-vibrate",
"haptics:allow-impact-feedback",
"haptics:allow-notification-feedback",
@@ -5,8 +5,10 @@ import { useEnv } from '@/context/EnvContext';
import { useEffect, useState } from 'react';
import { type as osType, arch as osArch } from '@tauri-apps/plugin-os';
import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import { relaunch, exit } from '@tauri-apps/plugin-process';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { Command } from '@tauri-apps/plugin-shell';
import { invoke } from '@tauri-apps/api/core';
import { isTauriAppPlatform } from '@/services/environment';
import { useTranslator } from '@/hooks/useTranslator';
import { useTranslation } from '@/hooks/useTranslation';
@@ -14,6 +16,7 @@ import { useSearchParams } from 'next/navigation';
import { getAppVersion } from '@/utils/version';
import { tauriDownload } from '@/utils/transfer';
import { installPackage } from '@/utils/bridge';
import { join } from '@tauri-apps/api/path';
import { getLocale } from '@/utils/misc';
import { setLastShownReleaseNotesVersion } from '@/helpers/updater';
import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants';
@@ -167,9 +170,76 @@ export const UpdaterContent = ({
} as GenericUpdate);
}
};
const checkWindowsPortableUpdate = async () => {
if (!appService) return;
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
const response = await fetch(READEST_UPDATER_FILE);
const data = await response.json();
if (semver.gt(data.version, currentVersion)) {
const OS_ARCH = osArch();
const platformKey =
OS_ARCH === 'x86_64' ? 'windows-x86_64-portable' : 'windows-aarch64-portable';
const arch = OS_ARCH === 'x86_64' ? 'x64' : 'arm64';
const downloadUrl = data.platforms[platformKey]?.url as string;
const execDir = await invoke<string>('get_executable_dir');
const exeFileName = `Readest_${data.version}_${arch}-portable.exe`;
const exeFilePath = await join(execDir, exeFileName);
setUpdate({
currentVersion,
version: data.version,
date: data.pub_date,
body: data.notes,
downloadAndInstall: async (onEvent) => {
await new Promise<void>(async (resolve, reject) => {
let downloaded = 0;
let total = 0;
await tauriDownload(downloadUrl, exeFilePath, (progress) => {
if (!onEvent) return;
if (!total && progress.total) {
total = progress.total;
onEvent({
event: 'Started',
data: { contentLength: total },
});
} else if (downloaded > 0 && progress.progress === progress.total) {
console.log('Portable EXE downloaded to', exeFilePath);
onEvent?.({ event: 'Finished' });
setTimeout(() => {
resolve();
}, 1000);
}
onEvent({
event: 'Progress',
data: { chunkLength: progress.progress - downloaded },
});
downloaded = progress.progress;
}).catch((error) => {
console.error('Download failed:', error);
reject(error);
});
});
try {
console.log('Launching new executable:', exeFilePath);
const command = Command.create('start-readest', ['/C', 'start', '', exeFilePath]);
await command.spawn();
console.log('New executable launched, exiting current app...');
setTimeout(async () => {
await exit(0);
}, 500);
} catch (error) {
console.error('Failed to launch new executable:', error);
}
},
} as GenericUpdate);
}
};
const checkForUpdates = async () => {
const OS_TYPE = osType();
if (['macos', 'windows', 'linux'].includes(OS_TYPE)) {
if (appService?.isPortableApp && OS_TYPE === 'windows') {
checkWindowsPortableUpdate();
} else if (['macos', 'windows', 'linux'].includes(OS_TYPE)) {
checkDesktopUpdate();
} else if (OS_TYPE === 'android') {
checkAndroidUpdate();