Skip to content

feat(ENG-849): surface shell (installer) updates as an in-app notice#453

Draft
pnewsam wants to merge 1 commit into
stagingfrom
paul/eng-849-surface-shell-installer-updates-in-app
Draft

feat(ENG-849): surface shell (installer) updates as an in-app notice#453
pnewsam wants to merge 1 commit into
stagingfrom
paul/eng-849-surface-shell-installer-updates-in-app

Conversation

@pnewsam

@pnewsam pnewsam commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

The Electron shell (src/main, preload, the runtime, native deps) isn't covered by OTA — it only updates when the user downloads and reinstalls a new installer, and today the app gives no signal that a newer shell exists. This matters now that UI OTA is prod-only (ENG-670): a user stranded on an old shell keeps missing renderer fixes with no explanation.

This adds a detect + notice flow (no auto-install — that's ENG-850):

  • The boot/periodic poll compares the installed shell CalVer (getAppDisplayVersion()) against a new shellVersion field in latest.json; if strictly newer, main pushes a shell-available status over the existing UI_UPDATE_STATUS channel.
  • Sidebar banner — dismissible "New version available (x.y.z) — Download"; opens the installer URL. Dismissal is per-version (localStorage), so it re-notifies when a newer shell ships (don't nag; do re-notify).
  • Settings → Updates — an "App update available" card (current → latest + Download), shown regardless of banner dismissal (Settings is a deliberate visit).
  • Download targets the per-platform CloudFront installer: prod …/mac/mindshub-cowork-latest.pkg / …/windows/mindshub-cowork-latest.exe (macOS installer is a .pkg).

Two deliberate design decisions

  1. Prod-only. The manifest is prod-only and a non-prod build must never be pointed at a prod installer (ENG-676), so checkForShellUpdate self-gates to buildKindStrict() === 'prod'; non-prod simply doesn't check. shellDownloadUrl still maps stable → -staging (tested) so it stays correct if non-prod is ever enabled.
  2. Explicit shellVersion, not the manifest's version. version is the UI-bundle version; shellVersion is the installer version. publish-ui.yml emits shellVersion only on the auto-release path (new shell_version input from release.yml), where a prod installer actually ships — so a UI-only re-publish can't fabricate a phantom "reinstall" notice for an installer that doesn't exist. Absent shellVersion → no notice. Fails closed everywhere (unknown kind, no manifest, non-CalVer versions).

Implementation

  • update-logic.tsUIManifest.shellVersion + parse (advisory-only: a malformed value is ignored, never rejects the manifest / breaks OTA); pure shellUpdateIsNewer and shellDownloadUrl. +11 unit tests.
  • updater.tscheckForShellUpdate() orchestrator + poll wiring (runs before the OTA early-return so the notice surfaces even when UI/server are current).
  • host.ts / global.d.ts — shell fields on the update-status payload.
  • App.jsx / Sidebar.jsx / SettingsView.jsx — state, per-version dismiss, banner, and the Settings card.
  • publish-ui.yml / release.yml — emit shellVersion on the release path.
  • docs/update-behavior.md — new shell-notice section.

Fixes ENG-849.

Screenshots

Pending — see verification note below.

Type of change

  • ⚡ New feature (non-breaking change which adds functionality)
  • 📄 This change requires a documentation update (done: docs/update-behavior.md)

Checklist:

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have checked my code and corrected any misspellings

Verification

  • npm run typecheck (main + renderer + tests)
  • npm test — 368 passed, incl. 11 new (parseUiManifest shellVersion, shellUpdateIsNewer, shellDownloadUrl)
  • check:cowork-purity · ✅ npm run build:renderer · ✅ both workflow YAMLs parse
  • Not yet done: interactive smoke. The live path only lights up in a packaged prod build whose manifest carries shellVersion (none published yet) — exercisable via the COWORK_OTA_MANIFEST_URL QA seam pointing at a fixture manifest with shellVersion. Left as draft until that smoke + screenshots are attached.

Notes for reviewers

🤖 Generated with Claude Code

The Electron shell (src/main, preload, runtime, native deps) isn't covered by
OTA — it only updates via a manual reinstall — so all we can do is notice a
newer published shell and point the user at the installer. Detection only; no
download/install (that's ENG-850).

Behavior
- The boot/periodic poll compares the installed shell CalVer against a new
  `shellVersion` field in latest.json; if strictly newer it pushes a
  `shell-available` status over the existing UI_UPDATE_STATUS channel.
- Sidebar: a dismissible "New version available — Download" banner opening the
  installer URL; dismissal is per-version (localStorage) so it re-notifies when
  a newer shell ships.
- Settings → Updates: an "App update available" card (current → latest +
  Download), shown regardless of banner dismissal.
- Download targets the per-platform CloudFront installer (prod
  mindshub-cowork-latest.{pkg,exe}).

Two deliberate decisions
- Prod-only: the manifest is prod-only and a non-prod build must never be sent
  to a prod installer (ENG-676), so checkForShellUpdate self-gates to
  buildKindStrict() === 'prod'. shellDownloadUrl still maps stable → -staging
  (tested) so it stays correct if non-prod is ever enabled.
- Explicit shellVersion (not the manifest's UI-bundle `version`): publish-ui.yml
  emits it ONLY on the auto-release path (via a new shell_version input from
  release.yml), where a prod installer actually ships — so a UI-only re-publish
  can't fabricate a phantom reinstall notice. Absent shellVersion → no notice.

- update-logic.ts: UIManifest.shellVersion + parse, shellUpdateIsNewer,
  shellDownloadUrl (+ 11 unit tests).
- updater.ts: checkForShellUpdate() orchestrator + poll wiring.
- host.ts / global.d.ts: shell fields on the update-status payload.
- App.jsx / Sidebar.jsx / SettingsView.jsx: state, banner, and Settings card.
- publish-ui.yml / release.yml: emit shellVersion on the release path.
- docs/update-behavior.md: shell-notice section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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