- Updated links in CONTRIBUTING.md and README.md to point to the correct Chinese documentation paths. - Removed outdated files including the English and Chinese versions of the branching strategy, contributing guide, and test plan documents. - Cleaned up references to non-existent documentation in the project structure to streamline the contributor experience.
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 x-files/app-upgrade-config/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 x-files/app-upgrade-config.
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-livedx-files/app-upgrade-configbranch 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>againstx-files/app-upgrade-configwith 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
x-files/app-upgrade-configbranch, 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/x-files/app-upgrade-config/app-upgrade-config.json - GitCode:
https://gitcode.com/CherryHQ/cherry-studio/raw/x-files/app-upgrade-config/app-upgrade-config.json
Note: Both mirrors provide the same configuration file hosted on the x-files/app-upgrade-config 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
x-files/app-upgrade-configbranch (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 thex-files/app-upgrade-configworking tree. - If the file changed, it opens a PR against
x-files/app-upgrade-configviapeter-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