* ♻️ refactor: implement config-based update system with version compatibility control Replace GitHub API-based update discovery with JSON config file system. Support version gating (users below v1.7 must upgrade to v1.7.0 before v2.0). Auto-select GitHub/GitCode config source based on IP location. Simplify fallback logic. Changes: - Add update-config.json with version compatibility rules - Implement _fetchUpdateConfig() and _findCompatibleChannel() - Remove legacy _getReleaseVersionFromGithub() and GitHub API dependency - Refactor _setFeedUrl() with simplified fallback to default feed URLs - Add design documentation in docs/UPDATE_CONFIG_DESIGN.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(i18n): Auto update translations for PR #11147 * format code * 🔧 chore: update config for v1.7.5 → v2.0.0 → v2.1.6 upgrade path Update version configuration to support multi-step upgrade path: - v1.6.x users → v1.7.5 (last v1.x release) - v1.7.x users → v2.0.0 (v2.x intermediate version) - v2.0.0+ users → v2.1.6 (current latest) Changes: - Update 1.7.0 → 1.7.5 with fixed feedUrl - Set 2.0.0 as intermediate version with fixed feedUrl - Add 2.1.6 as current latest pointing to releases/latest This ensures users upgrade through required intermediate versions before jumping to major releases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * 🔧 chore: refactor update config with constants and adjust versions Refactor update configuration system and adjust to actual versions: - Add UpdateConfigUrl enum in constant.ts for centralized config URLs - Point to test server (birdcat.top) for development testing - Update AppUpdater.ts to use UpdateConfigUrl constants - Adjust update-config.json to actual v1.6.7 with rc/beta channels - Remove v2.1.6 entry (not yet released) - Set package version to 1.6.5 for testing upgrade path - Add update-config.example.json for reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * update version * ✅ test: add comprehensive unit tests for AppUpdater config system Add extensive test coverage for new config-based update system including: - Config fetching with IP-based source selection (GitHub/GitCode) - Channel compatibility matching with version constraints - Smart fallback from rc/beta to latest when appropriate - Multi-step upgrade path validation (1.6.3 → 1.6.7 → 2.0.0) - Error handling for network and HTTP failures Test Coverage: - _fetchUpdateConfig: 4 tests (GitHub/GitCode selection, error handling) - _findCompatibleChannel: 9 tests (channel matching, version comparison) - Upgrade Path: 3 tests (version gating scenarios) - Total: 30 tests, 100% passing Also optimize _findCompatibleChannel logic with better variable naming and log messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * ✅ test: add complete multi-step upgrade path tests (1.6.3 → 1.7.5 → 2.0.0 → 2.1.6) Add comprehensive test suite for complete upgrade journey including: - Individual step validation (1.6.3→1.7.5, 1.7.5→2.0.0, 2.0.0→2.1.6) - Full multi-step upgrade simulation with version progression - Version gating enforcement (block skipping intermediate versions) - Verification that 1.6.3 cannot directly upgrade to 2.0.0 or 2.1.6 - Verification that 1.7.5 cannot skip 2.0.0 to reach 2.1.6 Test Coverage: - 6 new tests for complete upgrade path scenarios - Total: 36 tests, 100% passing This ensures the version compatibility system correctly enforces intermediate version upgrades for major releases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * 📝 docs: reorganize update config documentation with English translation Move update configuration design document to docs/technical/ directory and add English translation for international contributors. Changes: - Move docs/UPDATE_CONFIG_DESIGN.md → docs/technical/app-update-config-zh.md - Add docs/technical/app-update-config-en.md (English translation) - Organize technical documentation in dedicated directory Documentation covers: - Config-based update system design and rationale - JSON schema with version compatibility control - Multi-step upgrade path examples (1.6.3 → 1.7.5 → 2.0.0 → 2.1.6) - TypeScript type definitions and matching algorithms - GitHub/GitCode source selection for different regions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * format code * ✅ test: add tests for latest channel self-comparison prevention Add tests to verify the optimization that prevents comparing latest channel with itself when latest is requested, and ensures rc/beta channels are returned when they are newer than latest. New tests: - should not compare latest with itself when requesting latest channel - should return rc when rc version > latest version - should return beta when beta version > latest version These tests ensure the requestedChannel !== UpgradeChannel.LATEST check works correctly and users get the right channel based on version comparisons. Test Coverage: 39 tests, 100% passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * update github/gitcode * format code * update rc version * ♻️ refactor: merge update configs into single multi-mirror file - Merge app-upgrade-config-github.json and app-upgrade-config-gitcode.json into single app-upgrade-config.json - Add UpdateMirror enum for type-safe mirror selection - Optimize _fetchUpdateConfig to receive mirror parameter, eliminating duplicate IP country checks - Update ChannelConfig interface to use Record<UpdateMirror, string> for feedUrls - Rename documentation files from app-update-config-* to app-upgrade-config-* - Update docs with new multi-mirror configuration structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * ✅ test: update AppUpdater tests for multi-mirror configuration - Add UpdateMirror enum import - Update _fetchUpdateConfig tests to accept mirror parameter - Convert all feedUrl to feedUrls structure in test mocks - Update test expectations to match new ChannelConfig interface - All 39 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * format code * delete files * 📝 docs: add UpdateMirror enum to type definitions - Add UpdateMirror enum definition in both EN and ZH docs - Update ChannelConfig to use Record<UpdateMirror, string> - Add comments showing equivalent structure for clarity 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * 🐛 fix: return actual channel from _findCompatibleChannel Fix channel mismatch issue where requesting rc/beta but getting latest: - Change _findCompatibleChannel return type to include actual channel - Return { config, channel } instead of just config - Update _setFeedUrl to use actualChannel instead of requestedChannel - Update all test expectations to match new return structure - Add channel assertions to key tests This ensures autoUpdater.channel matches the actual feed URL being used. Fixes issue where: - User requests 'rc' channel - latest >= rc, so latest config is returned - But channel was set to 'rc' with latest URL ❌ - Now channel is correctly set to 'latest' ✅ All 39 tests passing ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * update version * udpate version * update config * add no cache header * update files * 🤖 chore: automate app upgrade config updates * format code * update workflow * update get method * docs: document upgrade workflow automation --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
18 KiB
Update Configuration System Design Document
Background
Currently, AppUpdater directly queries the GitHub API to retrieve beta and rc update information. To support users in China, we need to fetch a static JSON configuration file from GitHub/GitCode based on IP geolocation, which contains update URLs for all channels.
Design Goals
- Support different configuration sources based on IP geolocation (GitHub/GitCode)
- Support version compatibility control (e.g., users below v1.x must upgrade to v1.7.0 before upgrading to v2.0)
- Easy to extend, supporting future multi-major-version upgrade paths (v1.6 → v1.7 → v2.0 → v2.8 → v3.0)
- Maintain compatibility with existing electron-updater mechanism
Current Version Strategy
- v1.7.x is the last version of the 1.x series
- Users below v1.7.0 must first upgrade to v1.7.0 (or higher 1.7.x version)
- Users v1.7.0 and above can directly upgrade to v2.x.x
Automation Workflow
The cs-releases/app-upgrade-config.json file is synchronized by the Update App Upgrade Config workflow. The workflow runs the scripts/update-app-upgrade-config.ts helper so that every release tag automatically updates the JSON in cs-releases.
Trigger Conditions
- Release events (
release: released/prereleased)- Draft releases are ignored.
- When GitHub marks the release as prerelease, the tag must include
-beta/-rc(with optional numeric suffix). Otherwise the workflow exits early. - When GitHub marks the release as stable, the tag must match the latest release returned by the GitHub API. This prevents out-of-order updates when publishing historical tags.
- If the guard clauses pass, the version is tagged as
latestorbeta/rcbased on its semantic suffix and propagated to the script through theIS_PRERELEASEflag.
- Manual dispatch (
workflow_dispatch)- Required input:
tag(e.g.,v2.0.1). Optional input:is_prerelease(defaults tofalse). - When
is_prerelease=true, the tag must carry a beta/rc suffix, mirroring the automatic validation. - Manual runs still download the latest release metadata so that the workflow knows whether the tag represents the newest stable version (for documentation inside the PR body).
- Required input:
Workflow Steps
- Guard + metadata preparation – the
Check if should proceedandPrepare metadatasteps compute the target tag, prerelease flag, whether the tag is the newest release, and asafe_tagslug used for branch names. When any rule fails, the workflow stops without touching the config. - Checkout source branches – the default branch is checked out into
main/, while the long-livedcs-releasesbranch lives incs/. All modifications happen in the latter directory. - Install toolchain – Node.js 22, Corepack, and frozen Yarn dependencies are installed inside
main/. - Run the update script –
yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag>updates the JSON in-place.- The script normalizes the tag (e.g., strips
vprefix), detects the release channel (latest,rc,beta), and loads segment rules fromconfig/app-upgrade-segments.json. - It validates that prerelease flags and semantic suffixes agree, enforces locked segments, builds mirror feed URLs, and performs release-availability checks (GitHub HEAD request for every channel; GitCode GET for latest channels, falling back to
https://releases.cherry-ai.comwhen gitcode is delayed). - After updating the relevant channel entry, the script rewrites the config with semver-sort order and a new
lastUpdatedtimestamp.
- The script normalizes the tag (e.g., strips
- Detect changes + create PR – if
cs/app-upgrade-config.jsonchanged, the workflow opens a PRchore/update-app-upgrade-config/<safe_tag>againstcs-releaseswith a commit message🤖 chore: sync app-upgrade-config for <tag>. Otherwise it logs that no update is required.
Manual Trigger Guide
- Open the Cherry Studio repository on GitHub → Actions tab → select Update App Upgrade Config.
- Click Run workflow, choose the default branch (usually
main), and fill in thetaginput (e.g.,v2.1.0). - Toggle
is_prereleaseonly when the tag carries a prerelease suffix (-beta,-rc). Leave it unchecked for stable releases. - Start the run and wait for it to finish. Check the generated PR in the
cs-releasesbranch, verify the diff inapp-upgrade-config.json, and merge once validated.
JSON Configuration File Format
File Location
- GitHub:
https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/cs-releases/app-upgrade-config.json - GitCode:
https://gitcode.com/CherryHQ/cherry-studio/raw/cs-releases/app-upgrade-config.json
Note: Both mirrors provide the same configuration file hosted on the cs-releases branch. The client automatically selects the optimal mirror based on IP geolocation.
Configuration Structure (Current Implementation)
{
"lastUpdated": "2025-01-05T00:00:00Z",
"versions": {
"1.6.7": {
"minCompatibleVersion": "1.0.0",
"description": "Last stable v1.7.x release - required intermediate version for users below v1.7",
"channels": {
"latest": {
"version": "1.6.7",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7",
"gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v1.6.7"
}
},
"rc": {
"version": "1.6.0-rc.5",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5",
"gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5"
}
},
"beta": {
"version": "1.6.7-beta.3",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3",
"gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3"
}
}
}
},
"2.0.0": {
"minCompatibleVersion": "1.7.0",
"description": "Major release v2.0 - required intermediate version for v2.x upgrades",
"channels": {
"latest": null,
"rc": null,
"beta": null
}
}
}
}
Future Extension Example
When releasing v3.0, if users need to first upgrade to v2.8, you can add:
{
"2.8.0": {
"minCompatibleVersion": "2.0.0",
"description": "Stable v2.8 - required for v3 upgrade",
"channels": {
"latest": {
"version": "2.8.0",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/download/v2.8.0",
"gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v2.8.0"
}
},
"rc": null,
"beta": null
}
},
"3.0.0": {
"minCompatibleVersion": "2.8.0",
"description": "Major release v3.0",
"channels": {
"latest": {
"version": "3.0.0",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/latest",
"gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/latest"
}
},
"rc": {
"version": "3.0.0-rc.1",
"feedUrls": {
"github": "https://github.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1",
"gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1"
}
},
"beta": null
}
}
}
Field Descriptions
lastUpdated: Last update time of the configuration file (ISO 8601 format)versions: Version configuration object, key is the version number, sorted by semantic versioningminCompatibleVersion: Minimum compatible version that can upgrade to this versiondescription: Version descriptionchannels: Update channel configurationlatest: Stable release channelrc: Release Candidate channelbeta: Beta testing channel- Each channel contains:
version: Version number for this channelfeedUrls: Multi-mirror URL configurationgithub: electron-updater feed URL for GitHub mirrorgitcode: electron-updater feed URL for GitCode mirror
metadata: Stable mapping info for automationsegmentId: ID fromconfig/app-upgrade-segments.jsonsegmentType: Optional flag (legacy|breaking|latest) for documentation/debugging
TypeScript Type Definitions
// Mirror enum
enum UpdateMirror {
GITHUB = 'github',
GITCODE = 'gitcode'
}
interface UpdateConfig {
lastUpdated: string
versions: {
[versionKey: string]: VersionConfig
}
}
interface VersionConfig {
minCompatibleVersion: string
description: string
channels: {
latest: ChannelConfig | null
rc: ChannelConfig | null
beta: ChannelConfig | null
}
metadata?: {
segmentId: string
segmentType?: 'legacy' | 'breaking' | 'latest'
}
}
interface ChannelConfig {
version: string
feedUrls: Record<UpdateMirror, string>
// Equivalent to:
// feedUrls: {
// github: string
// gitcode: string
// }
}
Segment Metadata & Breaking Markers
- Segment definitions now live in
config/app-upgrade-segments.json. Each segment describes a semantic-version range (or exact matches) plus metadata such assegmentId,segmentType,minCompatibleVersion, and per-channel feed URL templates. - Each entry under
versionscarries ametadata.segmentId. This acts as the stable key that scripts use to decide which slot to update, even if the actual semantic version string changes. - Mark major upgrade gateways (e.g.,
2.0.0) by giving the related segment asegmentType: "breaking"and (optionally)lockedVersion. This prevents automation from accidentally moving that entry when other 2.x builds ship. - Adding another breaking hop (e.g.,
3.0.0) only requires defining a new segment in the JSON file; the automation will pick it up on the next run.
Automation Workflow
Starting from this change, .github/workflows/update-app-upgrade-config.yml listens to GitHub release events (published + prerelease). The workflow:
- Checks out the default branch (for scripts) and the
cs-releasesbranch (where the config is hosted). - Runs
yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.jsonto regenerate the config directly inside thecs-releasesworking tree. - If the file changed, it opens a PR against
cs-releasesviapeter-evans/create-pull-request, with the generated diff limited toapp-upgrade-config.json.
You can run the same script locally via yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json (add --dry-run to preview) to reproduce or debug whatever the workflow does. Passing --skip-release-checks along with --dry-run lets you bypass the release-page existence check (useful when the GitHub/GitCode pages aren’t published yet). Running without --config continues to update the copy in your current working directory (main branch) for documentation purposes.
Version Matching Logic
Algorithm Flow
- Get user's current version (
currentVersion) and requested channel (requestedChannel) - Get all version numbers from configuration file, sort in descending order by semantic versioning
- Iterate through the sorted version list:
- Check if
currentVersion >= minCompatibleVersion - Check if the requested
channelexists and is notnull - If conditions are met, return the channel configuration
- Check if
- If no matching version is found, return
null
Pseudocode Implementation
function findCompatibleVersion(
currentVersion: string,
requestedChannel: UpgradeChannel,
config: UpdateConfig
): ChannelConfig | null {
// Get all version numbers and sort in descending order
const versions = Object.keys(config.versions).sort(semver.rcompare)
for (const versionKey of versions) {
const versionConfig = config.versions[versionKey]
const channelConfig = versionConfig.channels[requestedChannel]
// Check version compatibility and channel availability
if (
semver.gte(currentVersion, versionConfig.minCompatibleVersion) &&
channelConfig !== null
) {
return channelConfig
}
}
return null // No compatible version found
}
Upgrade Path Examples
Scenario 1: v1.6.5 User Upgrade (Below 1.7)
- Current Version: 1.6.5
- Requested Channel: latest
- Match Result: 1.7.0
- Reason: 1.6.5 >= 0.0.0 (satisfies 1.7.0's minCompatibleVersion), but doesn't satisfy 2.0.0's minCompatibleVersion (1.7.0)
- Action: Prompt user to upgrade to 1.7.0, which is the required intermediate version for v2.x upgrade
Scenario 2: v1.6.5 User Requests rc/beta
- Current Version: 1.6.5
- Requested Channel: rc or beta
- Match Result: 1.7.0 (latest)
- Reason: 1.7.0 version doesn't provide rc/beta channels (values are null)
- Action: Upgrade to 1.7.0 stable version
Scenario 3: v1.7.0 User Upgrades to Latest
- Current Version: 1.7.0
- Requested Channel: latest
- Match Result: 2.0.0
- Reason: 1.7.0 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion)
- Action: Directly upgrade to 2.0.0 (current latest stable version)
Scenario 4: v1.7.2 User Upgrades to RC Version
- Current Version: 1.7.2
- Requested Channel: rc
- Match Result: 2.0.0-rc.1
- Reason: 1.7.2 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion), and rc channel exists
- Action: Upgrade to 2.0.0-rc.1
Scenario 5: v1.7.0 User Upgrades to Beta Version
- Current Version: 1.7.0
- Requested Channel: beta
- Match Result: 2.0.0-beta.1
- Reason: 1.7.0 >= 1.7.0, and beta channel exists
- Action: Upgrade to 2.0.0-beta.1
Scenario 6: v2.5.0 User Upgrade (Future)
Assuming v2.8.0 and v3.0.0 configurations have been added:
- Current Version: 2.5.0
- Requested Channel: latest
- Match Result: 2.8.0
- Reason: 2.5.0 >= 2.0.0 (satisfies 2.8.0's minCompatibleVersion), but doesn't satisfy 3.0.0's requirement
- Action: Prompt user to upgrade to 2.8.0, which is the required intermediate version for v3.x upgrade
Code Changes
Main Modifications
-
New Methods
_fetchUpdateConfig(ipCountry: string): Promise<UpdateConfig | null>- Fetch configuration file based on IP_findCompatibleChannel(currentVersion: string, channel: UpgradeChannel, config: UpdateConfig): ChannelConfig | null- Find compatible channel configuration
-
Modified Methods
_getReleaseVersionFromGithub()→ Remove or refactor to_getChannelFeedUrl()_setFeedUrl()- Use new configuration system to replace existing logic
-
New Type Definitions
UpdateConfigVersionConfigChannelConfig
Mirror Selection Logic
The client automatically selects the optimal mirror based on IP geolocation:
private async _setFeedUrl() {
const currentVersion = app.getVersion()
const testPlan = configManager.getTestPlan()
const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST
// Determine mirror based on IP country
const ipCountry = await getIpCountry()
const mirror = ipCountry.toLowerCase() === 'cn' ? 'gitcode' : 'github'
// Fetch update config
const config = await this._fetchUpdateConfig(mirror)
if (config) {
const channelConfig = this._findCompatibleChannel(currentVersion, requestedChannel, config)
if (channelConfig) {
// Select feed URL from the corresponding mirror
const feedUrl = channelConfig.feedUrls[mirror]
this._setChannel(requestedChannel, feedUrl)
return
}
}
// Fallback logic
const defaultFeedUrl = mirror === 'gitcode'
? FeedUrl.PRODUCTION
: FeedUrl.GITHUB_LATEST
this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl)
}
private async _fetchUpdateConfig(mirror: 'github' | 'gitcode'): Promise<UpdateConfig | null> {
const configUrl = mirror === 'gitcode'
? UpdateConfigUrl.GITCODE
: UpdateConfigUrl.GITHUB
try {
const response = await net.fetch(configUrl, {
headers: {
'User-Agent': generateUserAgent(),
'Accept': 'application/json',
'X-Client-Id': configManager.getClientId()
}
})
return await response.json() as UpdateConfig
} catch (error) {
logger.error('Failed to fetch update config:', error)
return null
}
}
Fallback and Error Handling Strategy
- Configuration file fetch failure: Log error, return current version, don't offer updates
- No matching version: Notify user that current version doesn't support automatic upgrade
- Network exception: Cache last successfully fetched configuration (optional)
GitHub Release Requirements
To support intermediate version upgrades, the following files need to be retained:
- v1.7.0 release and its latest*.yml files (as upgrade target for users below v1.7)
- Future intermediate versions (e.g., v2.8.0) need to retain corresponding release and latest*.yml files
- Complete installation packages for each version
Currently Required Releases
| Version | Purpose | Must Retain |
|---|---|---|
| v1.7.0 | Upgrade target for users below 1.7 | ✅ Yes |
| v2.0.0-rc.1 | RC testing channel | ❌ Optional |
| v2.0.0-beta.1 | Beta testing channel | ❌ Optional |
| latest | Latest stable version (automatic) | ✅ Yes |
Advantages
- Flexibility: Supports arbitrarily complex upgrade paths
- Extensibility: Adding new versions only requires adding new entries to the configuration file
- Maintainability: Configuration is separated from code, allowing upgrade strategy adjustments without releasing new versions
- Multi-source support: Automatically selects optimal configuration source based on geolocation
- Version control: Enforces intermediate version upgrades, ensuring data migration and compatibility
Future Extensions
- Support more granular version range control (e.g.,
>=1.5.0 <1.8.0) - Support multi-step upgrade path hints (e.g., notify user needs 1.5 → 1.8 → 2.0)
- Support A/B testing and gradual rollout
- Support local caching and expiration strategy for configuration files