fix(desktop): retry an update that never installed (#3354)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 15:03:36 +02:00
1 parent 0fc6fad11d
commit ec681e6061
3 files changed
+54 -12

No files matched your search

@@ -342,7 +342,7 @@ describe('velopack self update', () => {
});
selfUpdateTest('an apply that never landed falls back instead of applying it again', async ({start}) => {
writeFileSync(applyStatePath, JSON.stringify({version: '2026.900.0', attemptedAt: 1}));
writeFileSync(applyStatePath, JSON.stringify({version: '2026.900.0', attemptedAt: Date.now() - 60_000}));
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
const {calls, hooks} = createHooks();
@@ -355,6 +355,24 @@ describe('velopack self update', () => {
assert.deepEqual(quits, []);
});
selfUpdateTest(
'an old apply that never landed retries from the feed instead of blocking every update',
async ({start, stillPending}) => {
writeFileSync(applyStatePath, JSON.stringify({version: '2026.900.0', attemptedAt: Date.now() - 3_600_000}));
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
velopack.checkResult = {id: 'v3', TargetFullRelease: {Version: '2026.950.0'}};
const {hooks} = createHooks();
assert.equal(await stillPending(start('velopack', hooks)), true);
assert.deepEqual(velopack.calls, [
'checkForUpdatesAsync',
'downloadUpdateAsync',
'waitExitThenApplyUpdate:v3:true:true',
]);
assert.equal(JSON.parse(readFileSync(applyStatePath, 'utf8')).version, '2026.950.0');
},
);
selfUpdateTest('an apply that landed is forgotten and the next one is recorded', async ({start, stillPending}) => {
writeFileSync(applyStatePath, JSON.stringify({version: '2026.800.0', attemptedAt: 1}));
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
+23 -11
View File
@@ -73,14 +73,23 @@ function readInstalledVelopackVersion(manager: VelopackUpdateManager): string {
}
}
function hasUnlandedVelopackApply(manager: VelopackUpdateManager): boolean {
const UNLANDED_APPLY_RETRY_AFTER_MS = 30 * 60_000;
type UnlandedApply = 'none' | 'recent' | 'stale';
function classifyUnlandedVelopackApply(manager: VelopackUpdateManager): UnlandedApply {
const attempt = readVelopackApplyAttempt();
if (attempt == null) return false;
if (attempt == null) return 'none';
if (compareShellVersions(readInstalledVelopackVersion(manager), attempt.version) >= 0) {
clearVelopackApplyAttempt();
return false;
return 'none';
}
return true;
if (Date.now() - attempt.attemptedAt < UNLANDED_APPLY_RETRY_AFTER_MS) {
return 'recent';
}
logger.warn('An earlier update never installed, retrying from the feed', {version: attempt.version});
clearVelopackApplyAttempt();
return 'stale';
}
function getStagedVelopackVersion(staged: UpdateInfo | VelopackAsset): string | null {
@@ -122,16 +131,19 @@ function downloadVelopackUpdateOnce(
async function runVelopackSelfUpdate(control: SelfUpdateControl, hooks: ShellSelfUpdateHooks): Promise<void> {
const {UpdateManager} = requireModule('velopack') as typeof import('velopack');
const manager = new UpdateManager(getUpdateBaseUrl());
if (hasUnlandedVelopackApply(manager)) {
const unlanded = classifyUnlandedVelopackApply(manager);
if (unlanded === 'recent') {
control.settle({reason: 'install-failed', detail: 'the last downloaded update was never installed'});
return;
}
let staged: UpdateInfo | VelopackAsset | null;
try {
staged = manager.getUpdatePendingRestart();
} catch (error) {
control.settle({reason: 'check-failed', detail: errorDetail(error)});
return;
let staged: UpdateInfo | VelopackAsset | null = null;
if (unlanded === 'none') {
try {
staged = manager.getUpdatePendingRestart();
} catch (error) {
control.settle({reason: 'check-failed', detail: errorDetail(error)});
return;
}
}
if (control.isSettled()) return;
if (staged == null) {