Skip to content

fix(upgrade): respect prerelease channel & guard downgrades#1698

Merged
zhangmo8 merged 1 commit into
devfrom
fix-version-update
May 28, 2026
Merged

fix(upgrade): respect prerelease channel & guard downgrades#1698
zhangmo8 merged 1 commit into
devfrom
fix-version-update

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented May 28, 2026

Summary

close #1691。Canary 安装包启动后会被自动"更新"到旧的正式版本,且后续切回内测渠道会被误判为"自动更新失败"。

根因

  1. configPresenter 默认 updateChannel = 'stable',导致 Canary 安装包首启时也按 stable 拉 latest.yml
  2. electron-updater 在 channel 错配时拿到的可能是版本号低于当前 beta 的旧正式版,但 update-available 没有做版本号兜底,直接进入自动下载安装。
  3. 上一步若已写入 auto_update_marker.json,下次启动 checkPendingUpdate 会把渠道错配的 stale marker 当作"上次更新失败",把 _previousUpdateFailed 钉死,后续切到 beta 拿到正确版本也只能提示手动下载。

改动

  • configPresenter.getUpdateChannel():移除硬编码默认 stable,首次按当前安装包版本号是否含 -alpha/-beta/-rc/-canary 等后缀推断默认渠道。
  • upgradePresenter update-available:加 semver 兜底,远端版本 <= 当前版本直接拒绝(覆盖 issue 描述的 Canary 被推送旧正式版场景)。以"channel 是否同源"作为独立拒绝条件,避免误伤 "beta → 同版本号 stable 正式发布" 这类合法的渠道收敛升级。
  • upgradePresenter checkPendingUpdate:marker 中目标版本若与当前安装包不属于同一渠道(beta vs stable),直接丢弃 marker 而不是钉死 _previousUpdateFailed
  • 单元测试:新增 3 个用例覆盖跨渠道降级被拒、同渠道升级被放行、beta→同版本号 stable 收敛升级被放行。

Test plan

  • pnpm exec vitest run test/main/presenter/upgradePresenter.test.ts (7/7 passed)
  • pnpm run typecheck
  • pnpm run lint
  • pnpm run format

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Prevented the app from offering older versions as updates through improved version comparison checks
    • Fixed prerelease version validation to prevent incorrect channel downgrades
    • Enhanced update channel detection to properly identify beta and stable releases based on your current version

Review Change Stack

- 默认 updateChannel 不再硬编码为 stable,首次按当前安装包版本号
  推断(含 -beta/-alpha/-rc/-canary 等后缀的视为 beta 渠道)
- update-available 加 semver 兜底,远端版本 <= 当前版本时拒绝,
  避免 channel 错配把 Canary 用户"升级"到旧的正式版
- checkPendingUpdate 加渠道一致性校验:marker 中目标版本与当前
  安装包不属于同一渠道时直接丢弃,避免被钉死为 previousUpdateFailed
- 补充对应单元测试
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

The PR refactors how the Electron app determines its update channel and validates remote versions. Channel selection now infers 'beta' or 'stable' from prerelease version suffixes on first use rather than using a hardcoded default. Version comparison logic rejects updates that are outdated or equal to the current version, and prerelease tier consistency is validated during pending update recovery.

Changes

Update Channel and Version Validation

Layer / File(s) Summary
Prerelease detection and version comparison helpers
src/main/presenter/upgradePresenter/index.ts
compare-versions dependency is added and a prerelease regex with isPrereleaseVersion() helper classifies app versions by suffix matching (alpha, beta, rc, canary).
Update channel inference from app version
src/main/presenter/configPresenter/index.ts
Hardcoded 'stable' default is removed; getUpdateChannel() now validates stored values, infers channels from prerelease suffixes, and persists the result.
Update availability validation with version comparison
src/main/presenter/upgradePresenter/index.ts
update-available handler compares remote and current versions; updates are rejected if remote is missing or <= current, resetting state and returning early.
Pending update prerelease tier consistency check
src/main/presenter/upgradePresenter/index.ts
checkPendingUpdate() compares the saved marker's prerelease tier against the current app version; mismatched tiers trigger marker deletion to prevent false failure states.
Test infrastructure and cross-channel update scenarios
test/main/presenter/upgradePresenter.test.ts
Test suite mocks app.getVersion() with reset logic and adds cases for prerelease downgrades, in-channel beta upgrades, and beta-to-stable convergence.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A channel inferred with care,
Version checks everywhere,
Prerelease tiers align,
Updates now stay in line—
Hoppy hops all the way down!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main changes: respecting prerelease channels in upgrade logic and guarding against downgrades via semver checks.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-version-update

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/presenter/upgradePresenter/index.ts`:
- Around line 291-302: The current marker check only discards cross-channel
markers; extend it to also discard stale same-channel markers by comparing
semantic versions: when updateInfo.version (markerVersion) exists and is on the
same channel as currentVersion (isPrereleaseVersion(markerVersion) ===
isPrereleaseVersion(currentVersion)) and markerVersion is less than
currentVersion (use semver.lt / semver.compare), unlink this._updateMarkerPath
and return so the marker does not fall through to setting _previousUpdateFailed;
change the logic in the block that references updateInfo.version,
isPrereleaseVersion, currentVersion and this._updateMarkerPath accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf122de4-047c-497b-809e-cb8f6dbe7ed5

📥 Commits

Reviewing files that changed from the base of the PR and between d6ddb14 and 7292d4a.

📒 Files selected for processing (3)
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/upgradePresenter/index.ts
  • test/main/presenter/upgradePresenter.test.ts

Comment on lines +291 to +302
// 渠道一致性校验:marker 中的目标版本若与当前安装包不属于同一渠道(beta vs stable),
// 说明上次的"待完成更新"来自渠道错配,应直接丢弃而不是钉死为 previousUpdateFailed
const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
if (markerVersion) {
const markerIsPre = isPrereleaseVersion(markerVersion)
const currentIsPre = isPrereleaseVersion(currentVersion)
if (markerIsPre !== currentIsPre) {
console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Discard same-channel stale markers as well.

A marker for an older version on the same channel still falls through to _previousUpdateFailed. For example, 1.0.4-beta.4 left behind after a manual install of 1.0.5-beta.5 will now block later auto-updates even though the marker is stale.

Suggested fix
         const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
         if (markerVersion) {
+          try {
+            if (compare(markerVersion, currentVersion, '<=')) {
+              console.log('忽略过期的 update marker', { marker: markerVersion, currentVersion })
+              fs.unlinkSync(this._updateMarkerPath)
+              return
+            }
+          } catch (e) {
+            console.warn('marker 版本号无效,删除 update marker', markerVersion, e)
+            fs.unlinkSync(this._updateMarkerPath)
+            return
+          }
+
           const markerIsPre = isPrereleaseVersion(markerVersion)
           const currentIsPre = isPrereleaseVersion(currentVersion)
           if (markerIsPre !== currentIsPre) {
             console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
             fs.unlinkSync(this._updateMarkerPath)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 渠道一致性校验:marker 中的目标版本若与当前安装包不属于同一渠道(beta vs stable),
// 说明上次的"待完成更新"来自渠道错配,应直接丢弃而不是钉死为 previousUpdateFailed
const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
if (markerVersion) {
const markerIsPre = isPrereleaseVersion(markerVersion)
const currentIsPre = isPrereleaseVersion(currentVersion)
if (markerIsPre !== currentIsPre) {
console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
}
// 渠道一致性校验:marker 中的目标版本若与当前安装包不属于同一渠道(beta vs stable),
// 说明上次的"待完成更新"来自渠道错配,应直接丢弃而不是钉死为 previousUpdateFailed
const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
if (markerVersion) {
try {
if (compare(markerVersion, currentVersion, '<=')) {
console.log('忽略过期的 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
} catch (e) {
console.warn('marker 版本号无效,删除 update marker', markerVersion, e)
fs.unlinkSync(this._updateMarkerPath)
return
}
const markerIsPre = isPrereleaseVersion(markerVersion)
const currentIsPre = isPrereleaseVersion(currentVersion)
if (markerIsPre !== currentIsPre) {
console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/upgradePresenter/index.ts` around lines 291 - 302, The
current marker check only discards cross-channel markers; extend it to also
discard stale same-channel markers by comparing semantic versions: when
updateInfo.version (markerVersion) exists and is on the same channel as
currentVersion (isPrereleaseVersion(markerVersion) ===
isPrereleaseVersion(currentVersion)) and markerVersion is less than
currentVersion (use semver.lt / semver.compare), unlink this._updateMarkerPath
and return so the marker does not fall through to setting _previousUpdateFailed;
change the logic in the block that references updateInfo.version,
isPrereleaseVersion, currentVersion and this._updateMarkerPath accordingly.

@zhangmo8 zhangmo8 merged commit f76df77 into dev May 28, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the fix-version-update branch May 29, 2026 05:44
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.

[BUG] 下载 Canary 版本,打开应用触发更新会更新到正式版本

1 participant