Skip to content

Add RC / perf-RC update channels with modifier opt-in - #7336

Closed
nwparker wants to merge 1 commit into
mainfrom
nwparker/rc-update-logic
Closed

nwparker wants to merge 1 commit into
mainfrom
nwparker/rc-update-logic

Conversation

@nwparker

@nwparker nwparker commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

What

Reworks the updater into a three-channel model so power users can steer which release track they're offered, keyed off modifier keys on Check for Updates.

Release tracks (classified by the new getReleaseChannel):

Track Example tag
stable v1.4.122
rc v1.4.122-rc.3
perf-rc v1.4.122-rc.3.perf

Behavior

A running build is always offered stable + its own track. Modifier keys additively opt into higher tracks for that check:

  • Shift → also offer the rc track (a stable user reaches the latest RC).
  • Cmd (macOS) / Ctrl (Win/Linux) + Shift → also offer the perf-rc track (reach the latest perf-RC).

Which matches the requested rules:

  • On an RC build → stable + rcs, but not perf-rcs unless Cmd/Ctrl+Shift is held.
  • On a perf-RC build → stable + perf-rcs, but not rcs unless Shift is held.
  • Background / auto checks (no modifier) → only the running build's own track + stable.

"Update to latest" then pins the generic feed to the newest eligible tag (perf-rc outranks the same-numbered plain rc by semver precedence).

How

  • getReleaseChannel(version) in updater-fallback.ts classifies a version by track (perf identifier ⇒ perf-rc, other prerelease ⇒ rc, else stable).
  • resolveEligibleReleaseChannels() in updater.ts combines the running build's channel with sticky Shift / Cmd-Shift opt-ins into a canonical stable → rc → perf-rc set.
  • The feed resolver (fetchNewerReleaseTagsWithReadiness) now takes eligibleChannels and filters candidate tags by track instead of a binary includePrerelease.
  • New shared UpdateCheckOptions ({ includePrerelease?, includePerfPrerelease? }) is threaded through the native menu, both Settings "Check for Updates" surfaces, and the updater:check IPC. Modifier detection is platform-correct (metaKey on macOS, ctrlKey elsewhere).
  • Opt-ins stay sticky for the process, mirroring the prior RC flag, so a later background check keeps offering the same track until install.

Sidebar fix (from review)

The sidebar … Help-menu "Check for Updates" item was wired to Radix's onSelect, which delivers a synthetic CustomEvent with no modifier state — so Shift / Cmd-Shift never registered there (the RC half was a pre-existing bug on main). Switched it to onClick, which Radix composes with a real MouseEvent before its own select/close, and which keyboard activation also dispatches (as a modifier-less click → plain stable check). The native menu and Settings-section button already received real events and were unaffected.

Tests

  • New getReleaseChannel unit tests + channel-filtering feed tests (rc-only hides perf, perf-only hides rc, all-tracks picks semver-newest).
  • New updater.ts integration tests for all four modifier / current-version combinations.
  • New sidebar test that clicks the menu item with Shift and Cmd/Ctrl+Shift and asserts the emitted UpdateCheckOptions.
  • Menu test rewritten to be platform-robust and assert the new { includePrerelease, includePerfPrerelease } shape.
  • Migrated existing feed/updater assertions from includePrerelease to eligibleChannels.

All updater/menu/sidebar suites (188 tests), typecheck, oxlint, and oxfmt pass. Adversarially reviewed end-to-end against the four rules via a multi-lens workflow; it surfaced one real defect (the sidebar Radix onSelect issue above), which is fixed and covered by a test.

Notes

  • Packaged-only path (real update checks require a signed/packaged build), so not exercised in a dev Electron run.
  • Web/mobile updater.check remains a no-op stub — the widened options type is compatible.

Model updates as three tracks — stable, RC (-rc.N), and perf-RC (-rc.N.perf) —
classified by getReleaseChannel. A running build is always offered stable plus
its own track; Shift-clicking "Check for Updates" additively opts into the RC
track and Cmd (macOS) / Ctrl (Win/Linux)+Shift opts into the perf-RC track. So
an RC build sees stable+RC (perf-RC only with the key), and a perf-RC build sees
stable+perf-RC (RC only with the key). Auto/background checks use only the
running build's own track + stable.

- getReleaseChannel + ReleaseChannel classifier in updater-fallback
- feed filtering switched from an includePrerelease boolean to eligibleChannels
- resolveEligibleReleaseChannels combines the current channel with sticky opt-ins
- native menu + both Settings "Check for Updates" surfaces detect the modifiers
- sidebar item now reads a real onClick MouseEvent (Radix onSelect delivers a
  modifier-less CustomEvent, so Shift/Cmd-Shift never registered there)

Co-authored-by: Orca <help@stably.ai>
@nwparker
nwparker force-pushed the nwparker/rc-update-logic branch from 605f1f4 to 8e4cccb Compare July 4, 2026 08:30
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

This PR adds perf-RC release channel support to update checks. A new ReleaseChannel type and getReleaseChannel classifier categorize versions as stable, rc, or perf-rc. The prerelease feed filtering switches from a boolean includePrerelease flag to an eligibleChannels array. The updater core resolves eligible channels from the current version and sticky opt-in state, replacing the previous single prerelease flag with combined RC/perf-RC handling in feed pinning, logging, and fallback logic. A shared UpdateCheckOptions type is threaded through IPC, preload, and the app menu. Renderer click handlers and the native menu handler now detect Shift plus an additional modifier (Cmd on macOS, Ctrl elsewhere) to opt into perf-RC checks alongside standard prerelease checks. Tests are updated across updater, feed, menu, and renderer modules accordingly.

Sequence Diagram(s)

Included in the hidden review stack artifact above.

Related PRs: None specified.

Suggested labels: updater, enhancement

Suggested reviewers: None specified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it misses required template sections like Summary, Screenshots, AI Review Report, Security Audit, and the testing checklist. Rewrite the PR description to match the repository template, adding the missing sections and a clear testing checklist plus AI review and security audit notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding RC and perf-RC update channels with modifier-based opt-in.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/updater-fallback.ts (1)

130-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Logic is correct; comment exceeds guideline length.

getReleaseChannel correctly classifies stable/rc/perf-rc via exact 'perf' identifier matching (avoids substring false positives). The explanatory block above it is 5 lines; as per coding guidelines, comments explaining non-obvious behavior should be "one or two lines, capturing only the non-obvious reason."

Source: Coding guidelines

src/main/updater.ts (2)

1020-1033: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

IPC contract allows includePerfPrerelease without includePrerelease.

checkForUpdatesFromMenu derives rc/perfRc independently from options, so a caller passing only includePerfPrerelease: true yields eligibleChannels: ['stable', 'perf-rc'] (no 'rc'). The PR's UX contract assumes Cmd/Ctrl+Shift always carries Shift too (per the updater.test.ts comment "Cmd/Ctrl+Shift-click always carries Shift too, so both opt-ins fire"), but nothing in this file enforces that pairing — it relies entirely on the menu/renderer callers (not in this review batch) always sending both flags together.

Consider normalizing here for defense-in-depth, e.g. treating includePerfPrerelease as implying includePrerelease:

🛡️ Optional normalization
   const wantsPrereleaseOptIn = Boolean(options?.includePrerelease || options?.includePerfPrerelease)
   if (wantsPrereleaseOptIn) {
     clearPrereleaseFallbackContext()
     enablePrereleaseChannels({
-      rc: Boolean(options?.includePrerelease),
+      rc: Boolean(options?.includePrerelease || options?.includePerfPrerelease),
       perfRc: Boolean(options?.includePerfPrerelease)
     })
   }

782-785: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Several new "Why" comments exceed the one-to-two-line guideline.

Blocks at lines 782-785, 853-857, and 1005-1009 are 4-5 lines each. As per coding guidelines, **/*.{ts,tsx,js,jsx} comments explaining non-obvious behavior should be "short — one or two lines, capturing only the non-obvious reason." Not blocking, but worth trimming for consistency.

Also applies to: 853-857, 1005-1009

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5dcc7116-60f0-4ef7-87e1-149e34f96c39

📥 Commits

Reviewing files that changed from the base of the PR and between 8070e65 and 605f1f479bf25ba4cb997c5a4d76898bc6b87afa.

📒 Files selected for processing (14)
  • src/main/menu/register-app-menu.test.ts
  • src/main/menu/register-app-menu.ts
  • src/main/updater-fallback.ts
  • src/main/updater-prerelease-feed-readiness.test.ts
  • src/main/updater-prerelease-feed.test.ts
  • src/main/updater-prerelease-feed.ts
  • src/main/updater.fallback.test.ts
  • src/main/updater.test.ts
  • src/main/updater.ts
  • src/main/window/attach-main-window-services.ts
  • src/preload/api-types.ts
  • src/renderer/src/components/settings/GeneralUpdateSettingsSection.tsx
  • src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.tsx
  • src/shared/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx (1)

268-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test only exercises one platform branch for perf-RC modifier.

perfModifier is derived using navigator.userAgent.includes('Mac') — the exact same expression the SUT (SidebarSettingsHelpMenu.tsx line 151) uses to pick metaKey vs ctrlKey. Since both expected and actual values are computed from the same runtime navigator.userAgent, this test only ever exercises whichever branch matches the CI/test-runner's jsdom environment; the other platform's modifier-detection path is never independently verified. It also doesn't cover the case of holding the platform modifier without Shift (should not enable any prerelease flag), which would catch a regression in the shiftKey && (...) logic.

Consider stubbing navigator.userAgent (e.g. via vi.stubGlobal or Object.defineProperty) to explicitly assert both the Mac (metaKey) and non-Mac (ctrlKey) branches, plus a modifier-only-without-shift case.

♻️ Example of explicit branch coverage
-  it('routes Check for Updates modifiers to the RC and perf-RC channels', async () => {
-    const container = await renderMenu()
-    const item = findMenuItem(container, 'Check for Updates')
-    // Why: Cmd (macOS) / Ctrl (Win/Linux) with Shift is the perf-RC escalation.
-    const perfModifier = navigator.userAgent.includes('Mac') ? { metaKey: true } : { ctrlKey: true }
-
-    const clickWith = async (init: MouseEventInit): Promise<void> => {
-      await act(async () => {
-        item.dispatchEvent(new MouseEvent('click', { bubbles: true, ...init }))
-      })
-    }
-
-    await clickWith({})
-    await clickWith({ shiftKey: true })
-    await clickWith({ shiftKey: true, ...perfModifier })
-
-    expect(mocks.updaterCheck.mock.calls).toEqual([
-      [{ includePrerelease: false, includePerfPrerelease: false }],
-      [{ includePrerelease: true, includePerfPrerelease: false }],
-      [{ includePrerelease: true, includePerfPrerelease: true }]
-    ])
-  })
+  it.each([
+    ['Macintosh', { metaKey: true }],
+    ['Windows', { ctrlKey: true }]
+  ])('routes Check for Updates modifiers to the RC and perf-RC channels (%s)', async (ua, perfModifier) => {
+    vi.stubGlobal('navigator', { userAgent: ua })
+    const container = await renderMenu()
+    const item = findMenuItem(container, 'Check for Updates')
+
+    const clickWith = async (init: MouseEventInit): Promise<void> => {
+      await act(async () => {
+        item.dispatchEvent(new MouseEvent('click', { bubbles: true, ...init }))
+      })
+    }
+
+    await clickWith({})
+    await clickWith(perfModifier) // modifier alone, no shift
+    await clickWith({ shiftKey: true })
+    await clickWith({ shiftKey: true, ...perfModifier })
+
+    expect(mocks.updaterCheck.mock.calls).toEqual([
+      [{ includePrerelease: false, includePerfPrerelease: false }],
+      [{ includePrerelease: false, includePerfPrerelease: false }],
+      [{ includePrerelease: true, includePerfPrerelease: false }],
+      [{ includePrerelease: true, includePerfPrerelease: true }]
+    ])
+    vi.unstubAllGlobals()
+  })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 351daff2-eb5a-49ad-9f02-d9b2083f0e18

📥 Commits

Reviewing files that changed from the base of the PR and between 605f1f479bf25ba4cb997c5a4d76898bc6b87afa and 8e4cccb.

📒 Files selected for processing (15)
  • src/main/menu/register-app-menu.test.ts
  • src/main/menu/register-app-menu.ts
  • src/main/updater-fallback.ts
  • src/main/updater-prerelease-feed-readiness.test.ts
  • src/main/updater-prerelease-feed.test.ts
  • src/main/updater-prerelease-feed.ts
  • src/main/updater.fallback.test.ts
  • src/main/updater.test.ts
  • src/main/updater.ts
  • src/main/window/attach-main-window-services.ts
  • src/preload/api-types.ts
  • src/renderer/src/components/settings/GeneralUpdateSettingsSection.tsx
  • src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx
  • src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.tsx
  • src/shared/types.ts
✅ Files skipped from review due to trivial changes (1)
  • src/shared/types.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/main/updater.fallback.test.ts
  • src/main/menu/register-app-menu.ts
  • src/main/updater-fallback.ts
  • src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.tsx
  • src/main/updater-prerelease-feed.test.ts
  • src/preload/api-types.ts
  • src/main/updater-prerelease-feed-readiness.test.ts
  • src/main/menu/register-app-menu.test.ts
  • src/main/window/attach-main-window-services.ts
  • src/main/updater-prerelease-feed.ts
  • src/renderer/src/components/settings/GeneralUpdateSettingsSection.tsx
  • src/main/updater.test.ts
  • src/main/updater.ts

@nwparker

Copy link
Copy Markdown
Contributor Author

Closing as superseded after scope audit: although primarily release-channel feature work, it also fixed lost Shift/Cmd/Ctrl modifiers in the updater menu. Equivalent modifier capture landed via #7278, so there is no remaining bug-fix delta to rebase.

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