Skip to content

Self-update: check, download on click, restart into the new release - #294

Merged
stippi merged 5 commits into
mainfrom
feature/self-update
Oct 30, 2026
Merged

stippi merged 5 commits into
mainfrom
feature/self-update

Conversation

@stippi

@stippi stippi commented Oct 30, 2026

Copy link
Copy Markdown
Owner

The GPUI app now updates itself from GitHub releases, modelled on T3 Code. When a newer release is out, a chip appears in the title bar. A click downloads it, and a second click restarts the app into it. docs/self-update.md describes the design.

Releases

  • Every release publishes an update-manifest.json with the version and each archive's URL, sha256 and size (scripts/update-manifest.sh).
  • The release workflow compiles the feed URL (…/releases/latest/download/update-manifest.json) into its builds through CODE_ASSISTANT_UPDATE_FEED. Only those builds update; local cargo builds show "Disabled" and keep their binary. The feed is a plain download rather than the REST API, so the 60 requests/hour limit doesn't apply.
  • The runtime env CODE_ASSISTANT_UPDATE_FEED overrides the feed, and CODE_ASSISTANT_DISABLE_AUTO_UPDATE turns updates off.

app_update crate (layer 0)

  • Manifest and asset choice: one archive per platform (macOS aarch64/x86_64 bundle, Linux and Windows executable). Under Rosetta the aarch64 build is chosen.
  • Download: into a .part file. Every new attempt resumes with Range (206 appends, 200 starts over, 416 discards the part). Network errors, 5xx, 408 and 429 are retried up to 5 times with backoff. Size and sha256 are verified at the end.
  • Install: extracts into a hidden staging folder next to the installation, then renames it into place with rollback.
    • macOS: ditto extraction, codesign --verify --deep --strict, and the same TeamIdentifier as the running app.
    • Linux: the new executable replaces the old file.
    • Windows: the running exe is renamed to *.old and removed on a later start.
  • Refuses: translocated apps and installations in folders the user can't write to.
  • Store: downloads live in the cache dir per version. An install record (from → target) lets the next start tell whether the update applied.

UpdateService (core)

  • An actor like VoiceService. It checks 15 s after start and then every 4 hours, and on request.
  • It publishes the app-scoped UiEvent::UpdateStateChanged.
  • Downloads start only on click.
  • On start it removes leftovers and downloads that are no longer newer, and it reuses a verified download.
  • A failed check keeps the last offer. A failed download offers the release again with the error.

GPUI

  • Title-bar chip states:
    • "Update X"
    • "Downloading" with a ring that fills up (like the context indicator), so the width stays put; the percentage is in the tooltip
    • "Restart to update"
    • "Installing…"
    • "Update failed" (retry on click)
  • If sessions are working, Restart asks first. The restart goes through set_restart_path on the installed path.
  • The About dialog shows the update status and has "Check for updates".

Verification

  • cargo fmt --check, cargo clippy --all-targets --all-features -D warnings and cargo deny check are clean, and the workspace tests pass.
  • Tests:
    • app_update runs against a local axum server: resume, ignored Range, 416, retries, fatal statuses, checksum.
    • macOS install tests use ad-hoc signed bundles.
    • The service is tested end to end against a fake release: find, download, install, stale downloads, "did not apply".
  • Tried live in the app on macOS against a local feed, with an isolated config:
    • the chip appeared;
    • the download ran, also throttled, where the ring filled and the chip kept its width;
    • Restart swapped the binary and started the new one;
    • the next start reported "did not apply" (same version, as expected) and reused the download;
    • the About dialog showed the status.
  • Not tried:
    • Windows and Linux live.
    • A real release workflow run.
    • Swapping a whole .app bundle in the running app (the live run updated a plain executable; the bundle swap is unit-tested).
    • Resume against GitHub's CDN.

Known gaps

  • The first release with this code has nothing to update to. Updating starts with the release after it.
  • A tooltip doesn't refresh while hovered, so the download percentage stays stale until the pointer moves.
  • Not done yet: a signed manifest (minisign), an auto-download setting, release notes in the chip, channels.

The release workflow compiles the manifest URL into its builds (only those
update themselves) and publishes update-manifest.json, listing URL, sha256
and size of every zip.
…, cleanup

Layer 0 mechanics of the self-update: an interrupted download resumes with
Range, network and server errors are retried with backoff, the result is
checked against the manifest's size and sha256. Installing extracts next to
the installation and renames into place: the whole bundle on macOS (after
codesign verification and a team check), the executable elsewhere (on
Windows the running exe is renamed away first).
…into it

code_assistant_core::updates::UpdateService checks 15 s after start and
every 4 hours, keeps the state (available, downloading, downloaded,
installing, plus the last error per step) and publishes it as an app-scoped
UiEvent. At start it removes downloads of versions not newer than the
running one and notices an install that did not apply.

The GPUI title bar shows a chip while a newer release is in sight: a click
downloads it, then restarts into it (asking first while sessions are
working). Failed checks stay out of the title bar; the About dialog shows
the state and checks by hand.
The label stays "Downloading", so the chip keeps its width; the ring is
the context indicator's, the percentage moved to the tooltip.
@stippi
stippi merged commit 8507bf9 into main Oct 30, 2026
6 checks passed
@stippi
stippi deleted the feature/self-update branch October 31, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant