Linux AppImage self-update on the nightly channel (#119)
Ship CI / CI (push) Has been cancelled
Addon CI / addon (arm64, macos-14, mac) (push) Has been cancelled
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Has been cancelled
Addon CI / addon (x64, windows-latest, win) (push) Has been cancelled

* 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>
This commit is contained in:
Matthew Harris Glover
2026-07-19 12:15:55 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Byron Gamatos
parent 242cbf23bc
commit 2a4396b7b7
10 changed files with 583 additions and 242 deletions
+25 -2
View File
@@ -107,10 +107,15 @@ clone_slopsmith() {
# builds and the push/tag CI paths behave exactly as before.
# --branch accepts either a branch or a tag, both shallow-cloneable.
local slopsmith_ref="${SLOPSMITH_REF:-main}"
# SLOPSMITH_REPO overrides the core repo (default got-feedback/feedback),
# mirroring SLOPSMITH_REF. Lets a contributor without push access to the
# core repo bundle a branch pushed to their own fork for a test build:
# SLOPSMITH_REPO=me/feedBack SLOPSMITH_REF=my-branch
local slopsmith_repo="${SLOPSMITH_REPO:-got-feedback/feedback}"
local _auth=""
[[ -n "${GH_CLONE_TOKEN:-}" ]] && _auth="x-access-token:${GH_CLONE_TOKEN}@"
echo "Cloning Slopsmith repository (ref: ${slopsmith_ref})..."
git clone --depth 1 --branch "$slopsmith_ref" "https://${_auth}github.com/got-feedback/feedback.git" "$clone_dir"
echo "Cloning Slopsmith repository (${slopsmith_repo} ref: ${slopsmith_ref})..."
git clone --depth 1 --branch "$slopsmith_ref" "https://${_auth}github.com/${slopsmith_repo}.git" "$clone_dir"
# Remove broken symlinks from plugins dir
find "$clone_dir/plugins" -maxdepth 1 -type l -delete 2>/dev/null || true
@@ -478,6 +483,24 @@ bundle_soundfont() {
build_typescript() {
echo_step "Building TypeScript"
npm run build:ts
# Bake the source commit SHA into the packaged app so the Linux AppImage
# updater can tell whether the running build is behind the latest nightly
# (whose GitHub release target_commitish is this same commit). In CI
# GITHUB_SHA matches the nightly release target exactly; local builds fall
# back to the working-tree HEAD — which won't match any nightly, so the
# updater simply offers the latest official build. Packaged via the
# electron-builder `files: ["dist/**/*"]` glob and read at runtime by
# update-manager.ts (path.join(__dirname, 'build-info.json')).
local build_sha="${GITHUB_SHA:-$(git rev-parse HEAD 2>/dev/null || echo unknown)}"
# Also capture the bundled core (feedBack) repo's commit, cloned earlier
# in main() into $SLOPSMITH_DIR (exported by clone_slopsmith). A fix can
# live in either repo, so knowing only the desktop SHA isn't enough to
# answer "is this build stale" — this is read back by update-manager.ts's
# readBuildInfo() and surfaced in the renderer's diagnostic snapshot.
local core_sha="$(git -C "${SLOPSMITH_DIR:-}" rev-parse HEAD 2>/dev/null || echo unknown)"
node -e "require('fs').writeFileSync('dist/main/build-info.json', JSON.stringify({ sha: process.argv[1], coreSha: process.argv[2] }))" "$build_sha" "$core_sha"
echo " Build SHA: $build_sha"
echo " Core SHA: $core_sha"
echo_summary "TypeScript built"
echo ""
}
+12
View File
@@ -1,6 +1,15 @@
#!/bin/bash
# Docker-based Linux build wrapper
# Runs build-linux-ubuntu.sh inside a reproducible container
#
# --platform linux/amd64 is forced on both build and run: the Linux target is
# x86_64-only end to end (bundle-python.sh's python-build-standalone pin,
# vgmstream, onnxruntime, etc. have no arm64 Linux build, and Steam Deck
# itself is x86_64). On an Apple Silicon host, omitting --platform makes
# `docker build` produce a native arm64 image, so bundle-python.sh's hardcoded
# x86_64 download becomes a foreign-arch binary inside an otherwise-native
# container — Rosetta chokes trying to exec it directly instead of via full
# amd64 emulation. Forcing amd64 for the whole container sidesteps that.
set -euo pipefail
@@ -44,6 +53,7 @@ echo " (This will take a few minutes on first run)"
echo ""
docker build \
--platform linux/amd64 \
-f "$DEVCONTAINER_DIR/Dockerfile" \
-t slopsmith-ubuntu-builder \
"$PROJECT_DIR"
@@ -75,6 +85,7 @@ echo ""
set +e
docker run \
--platform linux/amd64 \
--name "$CONTAINER_NAME" \
-v "$PROJECT_DIR:/workspace" \
-w /workspace \
@@ -83,6 +94,7 @@ docker run \
-e GIT_TERMINAL_PROMPT=0 \
-e "GH_CLONE_TOKEN=${GH_CLONE_TOKEN:-}" \
-e "SLOPSMITH_REF=${SLOPSMITH_REF:-main}" \
-e "SLOPSMITH_REPO=${SLOPSMITH_REPO:-got-feedback/feedback}" \
-t \
slopsmith-ubuntu-builder \
bash -c './scripts/build-linux-ubuntu.sh'