mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(desktop): retry an update that never installed (#3354)
This commit is contained in:
1 parent
0fc6fad11d
commit
ec681e6061
3 files changed
+49
-7
No files matched your search
@@ -23,6 +23,10 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ""
|
default: ""
|
||||||
|
stale_apply_record:
|
||||||
|
description: Before each update, leave behind an hour-old record of an update that never installed (passes only when the installed versions retry it)
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/desktop-windows-release-check.yaml
|
- .github/workflows/desktop-windows-release-check.yaml
|
||||||
@@ -53,6 +57,7 @@ jobs:
|
|||||||
VERSION: ${{ inputs.version || '2026.1009.20411' }}
|
VERSION: ${{ inputs.version || '2026.1009.20411' }}
|
||||||
EXPECT_FAILURE: ${{ github.event_name == 'pull_request' && 'true' || inputs.expect_failure }}
|
EXPECT_FAILURE: ${{ github.event_name == 'pull_request' && 'true' || inputs.expect_failure }}
|
||||||
UPDATE_FROM: ${{ inputs.update_from || '' }}
|
UPDATE_FROM: ${{ inputs.update_from || '' }}
|
||||||
|
STALE_APPLY_RECORD: ${{ inputs.stale_apply_record && 'true' || 'false' }}
|
||||||
ARCH: ${{ matrix.arch }}
|
ARCH: ${{ matrix.arch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
@@ -108,6 +113,13 @@ jobs:
|
|||||||
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
$exe = Get-ChildItem -Path (Join-Path $installRoot 'current') -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
|
$exe = Get-ChildItem -Path (Join-Path $installRoot 'current') -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
|
||||||
if ($null -eq $exe) { $failures += "$from did not install"; continue }
|
if ($null -eq $exe) { $failures += "$from did not install"; continue }
|
||||||
|
if ($env:STALE_APPLY_RECORD -eq 'true') {
|
||||||
|
$dataRoot = Split-Path (Split-Path $logPath -Parent) -Parent
|
||||||
|
New-Item -ItemType Directory -Force -Path $dataRoot | Out-Null
|
||||||
|
$attemptedAt = [DateTimeOffset]::UtcNow.AddHours(-1).ToUnixTimeMilliseconds()
|
||||||
|
Set-Content -Path (Join-Path $dataRoot 'update-apply-state.json') -Value "{`"version`":`"$env:VERSION`",`"attemptedAt`":$attemptedAt}" -NoNewline
|
||||||
|
Write-Host "left a stale apply record for $env:VERSION"
|
||||||
|
}
|
||||||
Start-Process -FilePath $exe.FullName -ArgumentList '--disable-gpu'
|
Start-Process -FilePath $exe.FullName -ArgumentList '--disable-gpu'
|
||||||
$deadline = (Get-Date).AddMinutes(8)
|
$deadline = (Get-Date).AddMinutes(8)
|
||||||
$reported = $null
|
$reported = $null
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ describe('velopack self update', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
selfUpdateTest('an apply that never landed falls back instead of applying it again', async ({start}) => {
|
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'};
|
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
|
||||||
const {calls, hooks} = createHooks();
|
const {calls, hooks} = createHooks();
|
||||||
|
|
||||||
@@ -355,6 +355,24 @@ describe('velopack self update', () => {
|
|||||||
assert.deepEqual(quits, []);
|
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}) => {
|
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}));
|
writeFileSync(applyStatePath, JSON.stringify({version: '2026.800.0', attemptedAt: 1}));
|
||||||
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
|
velopack.pendingRestart = {id: 'staged', Version: '2026.900.0'};
|
||||||
|
|||||||
@@ -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();
|
const attempt = readVelopackApplyAttempt();
|
||||||
if (attempt == null) return false;
|
if (attempt == null) return 'none';
|
||||||
if (compareShellVersions(readInstalledVelopackVersion(manager), attempt.version) >= 0) {
|
if (compareShellVersions(readInstalledVelopackVersion(manager), attempt.version) >= 0) {
|
||||||
clearVelopackApplyAttempt();
|
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 {
|
function getStagedVelopackVersion(staged: UpdateInfo | VelopackAsset): string | null {
|
||||||
@@ -122,17 +131,20 @@ function downloadVelopackUpdateOnce(
|
|||||||
async function runVelopackSelfUpdate(control: SelfUpdateControl, hooks: ShellSelfUpdateHooks): Promise<void> {
|
async function runVelopackSelfUpdate(control: SelfUpdateControl, hooks: ShellSelfUpdateHooks): Promise<void> {
|
||||||
const {UpdateManager} = requireModule('velopack') as typeof import('velopack');
|
const {UpdateManager} = requireModule('velopack') as typeof import('velopack');
|
||||||
const manager = new UpdateManager(getUpdateBaseUrl());
|
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'});
|
control.settle({reason: 'install-failed', detail: 'the last downloaded update was never installed'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let staged: UpdateInfo | VelopackAsset | null;
|
let staged: UpdateInfo | VelopackAsset | null = null;
|
||||||
|
if (unlanded === 'none') {
|
||||||
try {
|
try {
|
||||||
staged = manager.getUpdatePendingRestart();
|
staged = manager.getUpdatePendingRestart();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
control.settle({reason: 'check-failed', detail: errorDetail(error)});
|
control.settle({reason: 'check-failed', detail: errorDetail(error)});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (control.isSettled()) return;
|
if (control.isSettled()) return;
|
||||||
if (staged == null) {
|
if (staged == null) {
|
||||||
let available: UpdateInfo | null;
|
let available: UpdateInfo | null;
|
||||||
|
|||||||
Reference in new issue
Block a user