Skip to content

feat: real in-app auto-update for the desktop app (electron-updater) - #272

Open
NovakPAai wants to merge 2 commits into
mainfrom
feat/desktop-in-app-update
Open

feat: real in-app auto-update for the desktop app (electron-updater)#272
NovakPAai wants to merge 2 commits into
mainfrom
feat/desktop-in-app-update

Conversation

@NovakPAai

Copy link
Copy Markdown
Collaborator

Problem

The desktop app (macOS DMG) offered an Update Now banner that hit POST /api/updatenpm i -g codbash-app@latest + restart. In the packaged Electron app this never worked: it updated an unrelated npm-global copy while the app kept running its bundled server, so after the "restart" the app opened on the old version again. This is the exact symptom users reported ("download, click Update, it restarts, still old version").

What this does

Replaces the broken web self-update with a real in-place update via electron-updater, keeping the npm-CLI path intact.

Update flow (desktop): check GitHub Releases → banner shows vX availableDownload (click) → progress % → Restart to update → app relaunches onto the new version. autoDownload=false so the user controls the download; allowDowngrade/allowPrerelease pinned false.

Build/publish:

  • Added mac zip target (Squirrel.Mac installs from the zip, not the DMG) and a Windows nsis target.
  • RELEASE.md documents publishing the zips + blockmaps + latest-mac.yml (and the Windows latest.yml).

Guard: desktop/main.js sets CODBASH_DESKTOP=1; the server refuses POST /api/update with 400 in that mode, so nothing half-updates. The npm-CLI (codbash run) self-update path is unchanged.

Security hardening (from security review)

  • will-navigate guard pins the window to http://127.0.0.1:<port>/, so the powerful updater IPC bridge can't be reached by an off-origin page (C1).
  • Main process validates lifecycle state before download/install (renderer buttons are UX, not the security boundary) and validates the IPC sender frame (H2/M1).
  • Version strings rendered via textContent only (no HTML injection).

Correctness (from code review)

  • Event listeners attach at autoUpdater creation → no check result lost to a boot race; single initial check (renderer-triggered, reload-safe).
  • Periodic 6h re-check skips while downloading/downloaded → can't wipe the "ready to restart" banner.
  • Error state offers Check again + Open download page; download is idempotent against a fast double-click.

Tests

  • test/desktop-update-guard.test.js — spawns the server with CODBASH_DESKTOP=1, asserts POST /api/update → 400.
  • Full suite: 207 pass, 0 fail. electron-builder --dir confirms electron-updater is bundled into app.asar; desktop smoke (SMOKE OK) passes.

Deferred (documented)

  • Windows code-signing — the NSIS target is wired, but without an OV/EV cert electron-updater's signature check is a no-op (hash-only) and SmartScreen warns. RELEASE.md says: keep Windows on the notify-only fallback until signed.
  • Electron 33 bump — separate chore (needs full rebuild + re-notarization).
  • Unit test for renderDesktopUpdateState — the function is DOM-coupled and not module-exported; repo has no jsdom. The critical server guard is covered.

Rollout note

⚠️ In-app update only "activates" from the next release onward — the currently-installed 7.15.0 bundle doesn't ship electron-updater, so 7.15.0 → next still can't auto-update; next → the one after will.

🤖 Generated with Claude Code

The desktop app showed an "Update Now" banner that called POST /api/update
(npm i -g codbash-app@latest + restart). In the packaged Electron app that
never worked: it updated an unrelated npm-global copy while the app kept running
its bundled server, so the restart landed back on the old version.

Replace it with a real in-place update via electron-updater:
- Desktop: check GitHub Releases (latest-mac.yml/latest.yml), Download on click,
  progress, then Restart to relaunch onto the new version. autoDownload=false so
  the user controls the download; allowDowngrade/allowPrerelease pinned false.
- Add mac `zip` target (Squirrel.Mac can't apply a DMG) and a Windows `nsis`
  target; document the zip/blockmap publish flow in RELEASE.md.
- The npm-CLI self-update path is unchanged; the server refuses /api/update with
  400 when CODBASH_DESKTOP=1 (set by desktop/main.js) so it can't half-update.

Hardening (from security review):
- will-navigate guard pins the window to the local server origin, so the
  powerful updater IPC bridge can't be reached by an off-origin page.
- Main-process validates state before download/install (renderer buttons are UX,
  not the security boundary) and validates the IPC sender frame.

Correctness (from code review):
- Event listeners attach at autoUpdater creation so no check result is lost to a
  boot race; a single initial check (renderer-triggered, reload-safe).
- Periodic 6h re-check skips while downloading/downloaded so it can't wipe the
  "ready to restart" banner.
- Error state offers Check-again + Open-download-page; download is idempotent
  against a fast double-click.

Test: test/desktop-update-guard.test.js asserts /api/update → 400 in desktop mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NovakPAai

NovakPAai commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@vakovalskii — assigned you as reviewer/maintainer. Two things to flag.

1. Needs testing on a Windows machine 🪟

I don't have Windows, so the NSIS target and the Windows auto-update path are wired up but not verified on a real Windows box. Please validate on your side (or with the other participant who has Windows):

  • build it: cd desktop && npm run dist:win → should produce codbash Setup <ver>.exe + latest.yml;
  • test the update cycle between two versions: available → Download → Restart → new version.

⚠️ The Windows build is currently unsigned — until an OV/EV code-signing cert is in place, electron-updater's signature check degrades to hash-only and SmartScreen warns. Recommendation in RELEASE.md: do not ship Windows auto-update to real users until it's signed — keep Windows on the "open the releases page" fallback. macOS is signed/notarized and ready.

2. This must ship in EVERY future build 🔁

The mechanism now lives in the codebase (desktop/main.js + electron-updater), so it's included in the code automatically. But for the update to actually work for users, every release must publish to the GitHub Release:

  • macOS: *-mac.zip + *.zip.blockmap + latest-mac.yml (not just the DMG — otherwise the updater 404s on the zip);
  • Windows (once signed): *.exe + .blockmap + latest.yml.

This is the one step that's easy to forget in a manual release — step-by-step commands are in desktop/RELEASE.md §2b/§4. Suggest adding it to the release checklist.

Rollout note

Auto-update only "activates" from the next release onward: the currently-installed 7.15.0 bundle doesn't ship electron-updater yet, so 7.15.0 → next still takes the old path, and next → the one after will update in place.

@NovakPAai

Copy link
Copy Markdown
Collaborator Author

Cross-link: this overlaps with #271 (@indapublic's desktop OTA PoC), which targets the same problem. This PR is broader — it adds the Windows NSIS target, the macOS zip target (required for Squirrel.Mac to actually apply the update; #271 is DMG-only), security hardening (navigation guard + IPC sender validation + state-gating), and a regression test.

Worth borrowing from #271: its regenerate-latest-mac.js script automates the post-stapling latest-mac.yml regeneration, which this PR currently documents as manual RELEASE.md steps. Happy to fold that in. @vakovalskii to decide how to reconcile the two.

Adds desktop/scripts/regenerate-latest-mac.js (+ `npm run refresh-update-feed`)
so the release runbook no longer hand-edits latest-mac.yml. Generalises the idea
from #271 (thanks @indapublic) to the zip+dmg feed: it parses electron-builder's
own latest-mac.yml and refreshes sha512/size/blockMapSize only for entries whose
bytes changed on disk (sha512 mismatch), then re-syncs the top-level sha512 to
the `path` file. Schema-preserving and idempotent — the untouched .zip entries
(the actual mac update artifact) are left as-is; only stapled DMG entries are
recomputed.

RELEASE.md step 2b now calls the script instead of the manual yaml edit. Pure
transforms covered by test/desktop-update-feed.test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NovakPAai

Copy link
Copy Markdown
Collaborator Author

Folded in the feed-automation idea from #271: added desktop/scripts/regenerate-latest-mac.js (npm run refresh-update-feed), generalised to the zip+dmg feed. It parses electron-builder's own latest-mac.yml and refreshes checksums only for artifacts whose bytes changed on disk (schema-preserving, idempotent), so the release runbook no longer hand-edits the yaml. Pure transforms are unit-tested in test/desktop-update-feed.test.js. Credit to @indapublic for the original script. (commit a93e185)

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.

2 participants