mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-07-21 12:22:34 +00:00
* feat(update): Linux AppImage self-update on the nightly channel Adds a self-update engine for the Linux AppImage build. There's no Velopack pipeline for Linux (Windows/macOS use it, Linux doesn't), so this is a small, purpose-built GitHub-releases checker instead: - On the nightly channel, compares the commit baked into the running build (dist/main/build-info.json, written at build time) against the published nightly's target_commitish. A mismatch means the running build is behind, so it's offered as an update — this sidesteps the fact that the AppImage's filename and app.getVersion() never change between nightly builds, so semver comparison can't detect a new one. - The check returns immediately and the ~1.5GB download runs in the background with live progress (a new update:progress IPC event), so the UI never blocks or freezes waiting on it. - The download streams straight to disk (no buffering the whole file in memory) and is swapped in with an atomic rename next to the running AppImage. The stale-generation check (a channel switch or new check invalidating an in-flight download) runs before that swap, and a failed or superseded download always cleans up its temp file. - Applying the update spawns the (already-swapped-in) AppImage as a detached process and waits for a real 'spawn' confirmation before quitting this one, rather than assuming success — child_process.spawn can fail asynchronously, and quitting on an unconfirmed relaunch could leave the user with nothing running. - The pure idle/staged/download decision is split into linux-update-decision.ts with a small truth-table test, and every main-process decision point (and the equivalent renderer-side actions, in the companion feedBack PR) is traced through a new update:diag IPC event that lands in the app's existing "Export Diagnostics" console-capture bundle — this is how the handful of real bugs below were actually root-caused, from real device captures rather than guesswork. Also removes a forgotten, dead second implementation of the update-channel UI (src/renderer/screen.js's setupUpdateChannelControls() + its markup in settings.html), left over from before this work discovered the real, visible System-tab update UI lives in the feedBack repo. It was still wired up in the audio_engine plugin's own settings panel and silently called setChannel() with a stale channel value every time that panel rendered — invisibly corrupting the real UI's state. This was the actual root cause of several rounds of flaky, hard-to-reproduce on-device behavior (a stuck "unsupported" warning, downloads starting without an explicit check, etc.) chased down via the diagnostic tracing above; once found, no other logic needed to change. Dev tooling only, not used by CI: forces --platform linux/amd64 in the local Docker build wrapper (the Linux target is x86_64-only end to end — needed on Apple Silicon, where Rosetta chokes on a foreign-arch binary inside an otherwise-native container) and adds a SLOPSMITH_REPO override so a contributor without push access to the core repo can bundle a fork branch for a local test build. Verified end-to-end on a Steam Deck across many build/deploy rounds: fresh launch, channel selection, check, background download with live progress, atomic swap, and relaunch onto the new build — confirmed via a real Export Diagnostics capture showing a clean, fully-accounted-for trace with zero orphaned state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(update): fail safe when nightly release isn't pinned to a commit SHA GitHub sets a release's target_commitish to whatever it was published against — a 40-char SHA only if pinned, otherwise a branch name like "main". The Linux update decision compares it SHA-vs-SHA, so a branch name would never match the baked SHA and would re-download the ~1.5GB AppImage on every check forever, never reaching idle. Add isCommitSha() (pure, unit-tested) and have checkNowLinux() surface an error instead of entering that loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Byron Gamatos <xasiklas@gmail.com> --------- Signed-off-by: Byron Gamatos <xasiklas@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
48 lines
1.9 KiB
JavaScript
48 lines
1.9 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { loadTs } = require('./_load-ts');
|
|
|
|
const { linuxUpdateDecision, isCommitSha } = loadTs('src/main/linux-update-decision.ts');
|
|
|
|
// Two distinct 40-char commit SHAs.
|
|
const N = 'a'.repeat(40); // "current" nightly
|
|
const N1 = 'b'.repeat(40); // a newer nightly
|
|
const OLD = 'c'.repeat(40); // some earlier staged sha
|
|
|
|
test('running build IS the latest nightly → idle', () => {
|
|
assert.equal(linuxUpdateDecision(N, N, null), 'idle');
|
|
});
|
|
|
|
test('newer nightly available, nothing staged → download', () => {
|
|
assert.equal(linuxUpdateDecision(N, N1, null), 'download');
|
|
});
|
|
|
|
test('newer nightly already staged this session → staged (no re-download)', () => {
|
|
assert.equal(linuxUpdateDecision(N, N1, N1), 'staged');
|
|
});
|
|
|
|
test('unknown/dev build (no baked sha) always offers the update → download', () => {
|
|
assert.equal(linuxUpdateDecision(null, N, null), 'download');
|
|
});
|
|
|
|
test('remote advanced past the sha we staged → download the newer one', () => {
|
|
assert.equal(linuxUpdateDecision(N, N1, OLD), 'download');
|
|
});
|
|
|
|
test('no baked sha but this remote already staged → staged (not re-downloaded)', () => {
|
|
assert.equal(linuxUpdateDecision(null, N, N), 'staged');
|
|
});
|
|
|
|
test('isCommitSha accepts a full 40-char hex SHA (either case)', () => {
|
|
assert.equal(isCommitSha(N), true);
|
|
assert.equal(isCommitSha('A'.repeat(40)), true);
|
|
});
|
|
|
|
test('isCommitSha rejects branch names and malformed values (fail-safe gate)', () => {
|
|
// A branch-name target_commitish is exactly the case that would otherwise
|
|
// loop-download forever; checkNowLinux() errors out instead.
|
|
for (const bad of ['main', 'nightly', N.slice(0, 7), N + 'a', 'z'.repeat(40), '', null, undefined]) {
|
|
assert.equal(isCommitSha(bad), false, `expected ${JSON.stringify(bad)} to be rejected`);
|
|
}
|
|
});
|