docs/en/references/app-upgrade.md
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.
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.
release: released/prereleased)
-beta/-rc (with optional numeric suffix). Otherwise the workflow exits early.latest or beta/rc based on its semantic suffix and propagated to the script through the IS_PRERELEASE flag.workflow_dispatch)
tag (e.g., v2.0.1). Optional input: is_prerelease (defaults to false).is_prerelease=true, the tag must carry a beta/rc suffix, mirroring the automatic validation.Check if should proceed and Prepare metadata steps compute the target tag, prerelease flag, whether the tag is the newest release, and a safe_tag slug used for branch names. When any rule fails, the workflow stops without touching the config.main/, while the long-lived x-files/app-upgrade-config branch lives in cs/. All modifications happen in the latter directory.main/.pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag> updates the JSON in-place.
v prefix), detects the release channel (latest, rc, beta), and loads segment rules from config/app-upgrade-segments.json.https://releases.cherry-ai.com when gitcode is delayed).lastUpdated timestamp.cs/app-upgrade-config.json changed, the workflow opens a PR chore/update-app-upgrade-config/<safe_tag> against x-files/app-upgrade-config with a commit message 🤖 chore: sync app-upgrade-config for <tag>. Otherwise it logs that no update is required.main), and fill in the tag input (e.g., v2.1.0).is_prerelease only when the tag carries a prerelease suffix (-beta, -rc). Leave it unchecked for stable releases.x-files/app-upgrade-config branch, verify the diff in app-upgrade-config.json, and merge once validated.https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/x-files/app-upgrade-config/app-upgrade-config.jsonhttps://gitcode.com/CherryHQ/cherry-studio/raw/x-files/app-upgrade-config/app-upgrade-config.jsonNote: 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.
{
"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
}
}
}
}
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
}
}
}
lastUpdated: Last update time of the configuration file (ISO 8601 format)versions: Version configuration object, key is the version number, sorted by semantic versioning
minCompatibleVersion: Minimum compatible version that can upgrade to this versiondescription: Version descriptionchannels: Update channel configuration
latest: Stable release channelrc: Release Candidate channelbeta: Beta testing channelversion: Version number for this channelfeedUrls: Multi-mirror URL configuration
github: electron-updater feed URL for GitHub mirrorgitcode: electron-updater feed URL for GitCode mirrormetadata: Stable mapping info for automation
segmentId: ID from config/app-upgrade-segments.jsonsegmentType: Optional flag (legacy | breaking | latest) for documentation/debugging// 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
// }
}
config/app-upgrade-segments.json. Each segment describes a semantic-version range (or exact matches) plus metadata such as segmentId, segmentType, minCompatibleVersion, and per-channel feed URL templates.versions carries a metadata.segmentId. This acts as the stable key that scripts use to decide which slot to update, even if the actual semantic version string changes.2.0.0) by giving the related segment a segmentType: "breaking" and (optionally) lockedVersion. This prevents automation from accidentally moving that entry when other 2.x builds ship.3.0.0) only requires defining a new segment in the JSON file; the automation will pick it up on the next run.Starting from this change, .github/workflows/update-app-upgrade-config.yml listens to GitHub release events (published + prerelease). The workflow:
x-files/app-upgrade-config branch (where the config is hosted).pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json to regenerate the config directly inside the x-files/app-upgrade-config working tree.x-files/app-upgrade-config via peter-evans/create-pull-request, with the generated diff limited to app-upgrade-config.json.You can run the same script locally via pnpm 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.
currentVersion) and requested channel (requestedChannel)currentVersion >= minCompatibleVersionchannel exists and is not nullnullfunction 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
}
Assuming v2.8.0 and v3.0.0 configurations have been added:
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 configurationModified Methods
_getReleaseVersionFromGithub() → Remove or refactor to _getChannelFeedUrl()_setFeedUrl() - Use new configuration system to replace existing logicNew Type Definitions
UpdateConfigVersionConfigChannelConfigThe 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
}
}
To support intermediate version upgrades, the following files need to be retained:
| 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 |
>=1.5.0 <1.8.0)