fix(desktop): decouple update channels from version suffixes

This commit is contained in:
winewill
2026-11-04 11:06:11 +08:00
parent 6ad048912e
commit 152d0294c2
24 changed files with 226 additions and 39 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ on:
default: test
required: true
update_channel:
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定'
type: choice
options: [nightly, stable]
default: nightly
+1 -1
View File
@@ -15,7 +15,7 @@ on:
default: test
required: true
update_channel:
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定'
type: choice
options: [nightly, stable]
default: nightly
+1 -1
View File
@@ -15,7 +15,7 @@ on:
default: test
required: true
update_channel:
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定'
type: choice
options: [nightly, stable]
default: nightly
+2 -2
View File
@@ -18,8 +18,8 @@
en: 0b35c7471c09b27a
zh: cc4a382b99fc3210
/desktop-packaging/release-versions:
en: 8cc17b1e5ae3e422
zh: 7de3e4f1e76be3e3
en: 0ce6f56a2e94f687
zh: a878a5db6a405c68
/desktop-packaging/configuration-and-local-builds:
en: 5b4f0f80a117f202
zh: ff05be196c181087
+1 -1
View File
@@ -63,7 +63,7 @@ pnpm --dir apps/desktop run package:win:x64 --build-version 0.1.6-alpha.1.202609
A production release publishes the product version and takes no `--build-version`. Uploading one tags the packaged commit as `desktop-v<version>` once the artifacts are public; a build from a modified checkout is not tagged, and a tagging failure prints the command to run by hand rather than failing a completed upload. Test and local builds are deliberately left untagged, and every artifact records `dshBuildCommit` and `dshBuildDirty` in its manifest so a build handed over directly remains traceable.
The installer embeds `DSH_DESKTOP_UPDATE_CHANNEL` (`nightly` by default, or `stable`) independently of version and deployment. This value supplies the default channel; the saved selection in [Desktop settings](../README.md#local-desktop-settings) takes precedence, including after installing another package. Stable-channel packages reject prerelease versions. A stable product can produce a nightly `-test.<date>.<sequence>` build, but that version ranks below the stable base and cannot upgrade an already installed higher version. Keep automatic downgrade disabled; stable corrective releases require a product version bump. CI version selection and publication are documented in [CI publication](../publishing/ci/README.md#package-and-publish).
The installer embeds `DSH_DESKTOP_UPDATE_CHANNEL` (`nightly` by default, or `stable`) independently of version and deployment. This value supplies the default channel; the saved selection in [Desktop settings](../README.md#local-desktop-settings) takes precedence, including after installing another package. Operators select the channel independently of version suffixes; both channels accept release and prerelease versions. A stable product can produce a `-test.<date>.<sequence>` build, but that version ranks below the stable base and cannot upgrade an already installed higher version. Keep automatic downgrade disabled; stable corrective releases require a product version bump. CI version selection and publication are documented in [CI publication](../publishing/ci/README.md#package-and-publish).
<a id="configuration-and-local-builds"></a>
+1 -1
View File
@@ -63,7 +63,7 @@ pnpm --dir apps/desktop run package:win:x64 --build-version 0.1.6-alpha.1.202609
production 发布使用产品版本本身,不传 `--build-version`。其上传成功后会把打包所用 commit 打成 `desktop-v<版本>` 标签;来自有改动工作区的构建不打标签,打标签失败也只打印手工命令,不会让已完成的上传变成失败。test 与本地构建有意不留标签,而所有产物的清单都记录 `dshBuildCommit` 与 `dshBuildDirty`,直接分发的构建同样可溯源。
安装包独立于版本和发布环境内置 `DSH_DESKTOP_UPDATE_CHANNEL`(默认 `nightly`,也可选择 `stable`)。该值提供默认渠道;[桌面设置](../README.zh.md#local-desktop-settings)中保存的选择优先,包括安装另一个包之后。stable 包拒绝预发布版本。稳定产品可生成 nightly 的 `-test.<date>.<sequence>` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。
安装包独立于版本和发布环境内置 `DSH_DESKTOP_UPDATE_CHANNEL`(默认 `nightly`,也可选择 `stable`)。该值提供默认渠道;[桌面设置](../README.zh.md#local-desktop-settings)中保存的选择优先,包括安装另一个包之后。操作人员独立于版本后缀选择渠道;两个渠道均接受正式版本和预发布版本。稳定产品可生成 `-test.<date>.<sequence>` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。
<a id="configuration-and-local-builds"></a>
+2 -2
View File
@@ -24,8 +24,8 @@
en: 0f43f2e193e95043
zh: 1c1efb062c06f7b8
/desktop-ci-publication/package-and-publish:
en: 3f58698708671202
zh: be0ba215a67168d6
en: 041fde801a4dc1ae
zh: 447e3966241cd266
/desktop-ci-publication/recovery-and-records:
en: 0d87b7a9c073993c
zh: 9f135e8c1a4b7d8e
+1 -1
View File
@@ -70,7 +70,7 @@ Environment approval gates this workflow, not arbitrary code running under the s
For publication to either test or production, choose Windows `signing=signed` or leave macOS **Skip notarization** unchecked. Unsigned and unnotarized packages are only for internal debugging; they cannot publish or provide automatic updates. Before installing dependencies, the hosted routing job rejects debugging artifacts identified by the selected run and attempt. An archive-only rerun retains the original build attempt in artifact names, so this early check can miss it; local manifest qualification still rejects debugging builds before publication. Missing artifact metadata does not establish eligibility: the original machine still validates its retained manifest and signatures. A debugging-package rejection requires a new signed/notarized package and its run ID / attempt; retrying publication cannot qualify the existing bytes.
Select the platform's `Package Desktop` workflow, preset, source, channel and version mode. `source_ref` accepts repository branch names (for example `feature/desktop-update`) or `dsh-v*` tags for test; production accepts only `master` or `dsh-v*` tags. Names matching `dsh-v` followed by a digit and letters, digits, dots or hyphens are treated as tags, even if a branch has that name. Tags must match the selected product version. We recommend dispatching the packaging workflow from `master` and enter the source separately; raw commit SHAs, PR refs and fork repositories are not supported. The selected source must contain the CI adapters, so older sources fail before dependency installation. Only select trusted branches: their build code runs on the signing machine. This is an operating convention, not enforced workflow-origin or credential isolation. Publication uses the recorded commit and requires it to remain in the selected branch history; normal branch advances are allowed, deletion or a diverging force-push rejects. Tag publication requires the tag to still resolve to the recorded commit. Dispatch the publisher from `master`; its hosted routing job rejects other refs before scheduling the original machine. The packaged source commit is recorded separately. `version_mode=default` selects product for production and auto for test; product preserves the selected checkout version, auto appends date/run/attempt, and manual requires `build_version`. A stable-channel package requires a stable version, so test/stable needs product or a stable manual version. The optional `test_release_id` is the test URL prefix: blank retains the machine default, successive upgrade tests on that platform need the same value, and defaults should be rotated periodically. It is not a GitHub run ID. See [packaging configuration](../../packaging/README.md#configuration-and-local-builds) for its validation and rotation limits.
Select the platform's `Package Desktop` workflow, preset, source, channel and version mode. `source_ref` accepts repository branch names (for example `feature/desktop-update`) or `dsh-v*` tags for test; production accepts only `master` or `dsh-v*` tags. Names matching `dsh-v` followed by a digit and letters, digits, dots or hyphens are treated as tags, even if a branch has that name. Tags must match the selected product version. We recommend dispatching the packaging workflow from `master` and enter the source separately; raw commit SHAs, PR refs and fork repositories are not supported. The selected source must contain the CI adapters, so older sources fail before dependency installation. Only select trusted branches: their build code runs on the signing machine. This is an operating convention, not enforced workflow-origin or credential isolation. Publication uses the recorded commit and requires it to remain in the selected branch history; normal branch advances are allowed, deletion or a diverging force-push rejects. Tag publication requires the tag to still resolve to the recorded commit. Dispatch the publisher from `master`; its hosted routing job rejects other refs before scheduling the original machine. The packaged source commit is recorded separately. `version_mode=default` selects product for production and auto for test; product preserves the selected checkout version, auto appends date/run/attempt, and manual requires `build_version`. The operator-selected channel accepts any supported build version, including alpha and RC versions; it does not change version precedence or permit client downgrades. The optional `test_release_id` is the test URL prefix: blank retains the machine default, successive upgrade tests on that platform need the same value, and defaults should be rotated periodically. It is not a GitHub run ID. See [packaging configuration](../../packaging/README.md#configuration-and-local-builds) for its validation and rotation limits.
Packaging only retains files. After success, open **Publish Desktop · COS → Run workflow** on `master`, enter `package_run_id` from the packaging run URL, and optionally enter `package_run_attempt`. Blank selects the latest attempt and its successful package job. An archival-only retry retains the original build attempt; a failed newer package job never falls back to an older success. An explicit attempt selects that build even when its separate GitHub archival failed. Windows must be signed; macOS must finish notarization and stapling. GitHub job evidence selects the original runner; its local manifest supplies the preset, version, source and channel before approval. No installer or request artifact is downloaded. Retained packages with the old `publishRequested=false` flag remain eligible; missing or changed local files stop publication.
+1 -1
View File
@@ -70,7 +70,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
发布到 test 或 production 都必须在 Windows 选择 `signing=signed`,或在 macOS 不勾选**跳过公证**。无签名、无公证的包仅限内部调试,不能发布或用于自动更新。托管路由 job 会在安装依赖前,依据所选 run 和 attempt 对应的 artifact 名称拒绝已标明的调试包。仅重跑归档时,artifact 名称保留原构建 attempt,早期检查可能未识别该包;本地清单资格校验仍会在发布前拒绝调试包。缺少 artifact 元数据不代表允许发布:原机器仍会验证留存清单和签名。遇到调试包拒绝提示时,需重新完成签名或公证,再使用新包的 run ID / attempt 发布;重跑发布不能使现有产物获得发布资格。
选择对应平台的 `Package Desktop` 工作流、preset、源码、通道和版本模式。test 的 `source_ref` 接受仓库内分支名(如 `feature/desktop-update`)或 `dsh-v*` tag;production 仅接受 `master` 或 `dsh-v*` tag。符合 `dsh-v` 后接数字、其余为字母、数字、点或连字符的名称按 tag 处理,即使存在同名分支。tag 必须与所选产品版本一致。建议从 `master` 启动打包工作流,再单独填写打包源码;不支持直接填写 commit SHA、PR ref 或 fork 仓库。源码必须包含 CI 适配脚本,旧源码会在安装依赖前失败。仅选择可信分支:其构建代码将在签名机器上执行。这是操作约定,不提供工作流来源强制限制或凭据隔离。发布使用记录的 commit,并要求该提交仍在所选分支历史中;正常追加提交不影响发布,删除分支或导致历史分叉的强推会拒绝。tag 发布要求 tag 仍指向记录的提交。从 `master` 发起发布;托管路由 job 会在派发到原机器前拒绝其他 ref。实际打包源码提交单独记录。`version_mode=default` 对 production 选择 product,对 test 选择 auto;product 保留所选源码版本,auto 追加日期/run/attempt,manual 要求填写 `build_version`。stable 包要求稳定版本,因此 test/stable 需选择 product 或稳定的手填版本。可选 `test_release_id` 是测试 URL 前缀:留空沿用机器默认值,同一平台连续升级测试需保持相同值,并应定期轮换默认值。它不是 GitHub run ID。格式校验和轮换限制见[打包配置](../../packaging/README.zh.md#configuration-and-local-builds)。
选择对应平台的 `Package Desktop` 工作流、preset、源码、通道和版本模式。test 的 `source_ref` 接受仓库内分支名(如 `feature/desktop-update`)或 `dsh-v*` tag;production 仅接受 `master` 或 `dsh-v*` tag。符合 `dsh-v` 后接数字、其余为字母、数字、点或连字符的名称按 tag 处理,即使存在同名分支。tag 必须与所选产品版本一致。建议从 `master` 启动打包工作流,再单独填写打包源码;不支持直接填写 commit SHA、PR ref 或 fork 仓库。源码必须包含 CI 适配脚本,旧源码会在安装依赖前失败。仅选择可信分支:其构建代码将在签名机器上执行。这是操作约定,不提供工作流来源强制限制或凭据隔离。发布使用记录的 commit,并要求该提交仍在所选分支历史中;正常追加提交不影响发布,删除分支或导致历史分叉的强推会拒绝。tag 发布要求 tag 仍指向记录的提交。从 `master` 发起发布;托管路由 job 会在派发到原机器前拒绝其他 ref。实际打包源码提交单独记录。`version_mode=default` 对 production 选择 product,对 test 选择 auto;product 保留所选源码版本,auto 追加日期/run/attempt,manual 要求填写 `build_version`。操作人员选择的渠道接受任意受支持的构建版本,包括 alpha 和 RC 版本;渠道不改变版本优先级,也不允许客户端降级。可选 `test_release_id` 是测试 URL 前缀:留空沿用机器默认值,同一平台连续升级测试需保持相同值,并应定期轮换默认值。它不是 GitHub run ID。格式校验和轮换限制见[打包配置](../../packaging/README.zh.md#configuration-and-local-builds)。
打包只保留文件。成功后,在 `master` 上打开 **Publish Desktop · COS → Run workflow**,填写打包运行 URL 中的 `package_run_id`,并可填写 `package_run_attempt`。留空选择最新尝试及其成功的 package job;归档重跑继续使用原始构建 attempt,新的构建失败时不回退到较早成功构建。显式填写则选择对应构建,独立的 GitHub 归档失败不取消其资格。Windows 必须完成签名,macOS 必须完成公证和 stapling。GitHub job 证据确定原 runner,本机清单在审批前提供 preset、版本、源码和通道。不会下载安装包或请求 artifact。旧清单的 `publishRequested=false` 不影响发布资格;本机文件缺失或变化会停止发布。
@@ -24,7 +24,6 @@ export function desktopCiVersion(environment, productVersion, now = new Date())
}
candidate = `${desktopBuildVersionPrefix(productVersion)}${date}.${environment.GITHUB_RUN_NUMBER}.${environment.GITHUB_RUN_ATTEMPT}`
}
if (environment.DSH_DESKTOP_UPDATE_CHANNEL === 'stable' && candidate.includes('-')) throw new Error('Stable channel requires a stable version; select product or manual stable version')
try {
const resolved = validateDesktopBuildVersion(candidate, productVersion)
return resolved
@@ -221,7 +221,6 @@ export async function createDesktopUploadPlan(
throw new Error(`desktop upload: ${targetName} package completion record for ${buildVersion} does not match the ${update.environment} update destination`)
}
if (update.channel === 'stable' && buildVersion.includes('-')) throw new Error('desktop upload: stable requires a stable version')
const metadataFilename = desktopUpdateMetadataFilename(buildVersion, target.platform, update.channel)
const metadataPath = join(artifactsRoot, metadataFilename)
let metadataValue: unknown
@@ -105,7 +105,6 @@ export function createElectronBuilderConfig(
// the artifact names, the update feed, and the installed app.getVersion() the updater compares against.
const productVersion = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version
const buildVersion = resolveDesktopBuildVersion(env, productVersion)
if (update?.channel === 'stable' && buildVersion.includes('-')) throw new Error('desktop package: stable requires a stable version')
const packaged = resolveDesktopBuildCommit(env)
return {
appId,
-1
View File
@@ -276,7 +276,6 @@ export class DesktopUpdateCoordinator {
this.cancellation = result.cancellationToken
const version = result.updateInfo.version
if (valid(version) === null) throw new Error('desktop update: feed version is invalid')
if (this.channel === 'stable' && version.includes('-')) throw new Error('desktop update: stable feed contains a prerelease')
this.release = result.isUpdateAvailable && gt(version, this.currentVersion())
? { kind: 'checked', version } : { kind: 'none' }
return this.setState(this.candidate === undefined ? { phase: 'idle' } : { phase: 'available', version })
@@ -2,7 +2,7 @@
import type { AppUpdater } from 'electron-updater'
/**
* Select a feed without permitting downgrades or prereleases on stable.
* Select the operator-managed feed, accepting prereleases while preventing downgrades.
* @param updater - Updater whose prior operations have settled.
* @param channel - Selected distribution channel.
*/
@@ -10,5 +10,5 @@ export function selectUpdateChannel(updater: AppUpdater, channel: 'nightly' | 's
updater.channel = channel
// The library channel setter enables downgrades.
updater.allowDowngrade = false
updater.allowPrerelease = channel === 'nightly'
updater.allowPrerelease = true
}
@@ -0,0 +1,36 @@
import { readFileSync } from 'node:fs'
import { expect, it } from 'vitest'
import { desktopCiVersion } from '../scripts/desktop-ci-version.mjs'
import { desktopBuildVersionPrefix } from '../scripts/desktop-build-version.mjs'
import { createElectronBuilderConfig } from '../scripts/electron-builder-config.mjs'
it.each(['nightly', 'stable'] as const)('resolves every CI version mode independently of %s', (channel) => {
const now = new Date('2026-11-03T00:00:00Z')
for (const version of ['1.2.3', '1.2.3-alpha.1', '1.2.3-rc.1']) {
const env = { DSH_DESKTOP_UPDATE_CHANNEL: channel, GITHUB_RUN_NUMBER: '7', GITHUB_RUN_ATTEMPT: '2' }
const generated = `${desktopBuildVersionPrefix(version)}20261103.7.2`
expect(desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'product' }, version, now)).toBe(version)
expect(desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'auto' }, version, now)).toBe(generated)
expect(desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'manual', PACKAGE_BUILD_VERSION: generated }, version, now)).toBe(generated)
expect(desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'default', DSH_PACKAGING_PRESET: 'production' }, version, now)).toBe(version)
expect(desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'default', DSH_PACKAGING_PRESET: 'test' }, version, now)).toBe(generated)
expect(() => desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: 'manual', PACKAGE_BUILD_VERSION: 'invalid' }, version, now)).toThrow('Invalid build version')
}
})
it.each(['arm64', 'x64'] as const)('embeds stable in a prerelease macOS %s package and provider', (arch) => {
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }
const version = `${desktopBuildVersionPrefix(manifest.version)}20261103.1`
const config = createElectronBuilderConfig({
DSH_DESKTOP_APP_ID: 'com.example.channel-test',
DSH_DESKTOP_AUTO_UPDATE_ENV: 'production',
DSH_DESKTOP_UPDATE_CHANNEL: 'stable',
DSH_DESKTOP_BUILD_VERSION: version,
DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN: 'https://policy.example.com',
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)',
DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234',
APPLE_KEYCHAIN_PROFILE: 'channel-test',
}, 'darwin', arch)
expect(config.extraMetadata).toMatchObject({ version, dshDesktopUpdateChannel: 'stable' })
expect(config.publish).toEqual([{ provider: 'generic', url: `https://download.deepseek.com/dsh-desk/feeds/mac-${arch}/`, channel: 'stable' }])
})
+19 -2
View File
@@ -183,8 +183,25 @@ describe('desktop upload plan', () => {
expect(stable.artifacts.filter(file => file.channelMetadata).map(file => file.filename)).toEqual(['stable.yml'])
expect(nightly.artifacts.filter(file => file.channelMetadata).map(file => file.filename)).toEqual(['nightly.yml'])
expect(stable.artifacts.every(file => !nightly.artifacts.some(other => other.key === file.key))).toBe(true)
const prerelease = await fixture('win-x64', '1.2.3-alpha.1', environment, 'stable')
await expect(createDesktopUploadPlan('win-x64', prerelease)).rejects.toThrow(/stable requires a stable version/u)
})
it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('publishes manually selected prereleases on stable for %s', async (target) => {
const summaries: { environment: string; version: string; keys: string[] }[] = []
for (const environment of ['test', 'production'] as const) {
for (const version of ['1.2.3-alpha.1', '1.2.3-rc.1']) {
const plan = await createDesktopUploadPlan(target, await fixture(target, version, environment, 'stable'))
expect(plan.version).toBe(version)
const feed = plan.artifacts.find(file => file.channelMetadata)!
expect(feed.filename).toBe(target === 'win-x64' ? 'stable.yml' : 'stable-mac.yml')
const payload = plan.artifacts.find(file => file.filename.endsWith(target === 'win-x64' ? '.exe' : '.zip'))!
expect(payload.key).toContain(`/bin/${target}/stable/`)
expect(load(feed.contents!)).toMatchObject({ version, files: [{
url: `${environment === 'test' ? TEST_ORIGIN : 'https://download.deepseek.com'}/${payload.key}`,
}] })
summaries.push({ environment, version, keys: plan.artifacts.map(file => file.key) })
}
}
await expect(`${JSON.stringify(summaries, null, 2)}\n`).toMatchFileSnapshot(`./expected/stable-prerelease-${target}.json`)
})
it('publishes fixed feeds referencing versioned binaries without overriding CDN cache policy', async () => {
@@ -0,0 +1,42 @@
[
{
"environment": "test",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.dmg",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.zip",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.zip.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/mac-arm64/stable-mac.yml"
]
},
{
"environment": "test",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.dmg",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.zip",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.zip.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/mac-arm64/stable-mac.yml"
]
},
{
"environment": "production",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.dmg",
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.zip",
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-alpha.1-mac-arm64.zip.blockmap",
"dsh-desk/feeds/mac-arm64/stable-mac.yml"
]
},
{
"environment": "production",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.dmg",
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.zip",
"dsh-desk/bin/mac-arm64/stable/deepseek-harness-1.2.3-rc.1-mac-arm64.zip.blockmap",
"dsh-desk/feeds/mac-arm64/stable-mac.yml"
]
}
]
@@ -0,0 +1,42 @@
[
{
"environment": "test",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.dmg",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.zip",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.zip.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/mac-x64/stable-mac.yml"
]
},
{
"environment": "test",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.dmg",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.zip",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.zip.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/mac-x64/stable-mac.yml"
]
},
{
"environment": "production",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.dmg",
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.zip",
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-alpha.1-mac-x64.zip.blockmap",
"dsh-desk/feeds/mac-x64/stable-mac.yml"
]
},
{
"environment": "production",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.dmg",
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.zip",
"dsh-desk/bin/mac-x64/stable/deepseek-harness-1.2.3-rc.1-mac-x64.zip.blockmap",
"dsh-desk/feeds/mac-x64/stable-mac.yml"
]
}
]
@@ -0,0 +1,38 @@
[
{
"environment": "test",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/win-x64/stable/deepseek-harness-1.2.3-alpha.1-win-x64.exe",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/win-x64/stable/deepseek-harness-1.2.3-alpha.1-win-x64.exe.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/win-x64/stable.yml"
]
},
{
"environment": "test",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/0123456789abcdef0123456789abcdef/bin/win-x64/stable/deepseek-harness-1.2.3-rc.1-win-x64.exe",
"dsh-desk/0123456789abcdef0123456789abcdef/bin/win-x64/stable/deepseek-harness-1.2.3-rc.1-win-x64.exe.blockmap",
"dsh-desk/0123456789abcdef0123456789abcdef/feeds/win-x64/stable.yml"
]
},
{
"environment": "production",
"version": "1.2.3-alpha.1",
"keys": [
"dsh-desk/bin/win-x64/stable/deepseek-harness-1.2.3-alpha.1-win-x64.exe",
"dsh-desk/bin/win-x64/stable/deepseek-harness-1.2.3-alpha.1-win-x64.exe.blockmap",
"dsh-desk/feeds/win-x64/stable.yml"
]
},
{
"environment": "production",
"version": "1.2.3-rc.1",
"keys": [
"dsh-desk/bin/win-x64/stable/deepseek-harness-1.2.3-rc.1-win-x64.exe",
"dsh-desk/bin/win-x64/stable/deepseek-harness-1.2.3-rc.1-win-x64.exe.blockmap",
"dsh-desk/feeds/win-x64/stable.yml"
]
}
]
+22 -12
View File
@@ -244,18 +244,28 @@ it.each([new DesktopUpdatePreparationError('stop-failed', 'private diagnostic'),
})
it('keeps a stable-version nightly installation on nightly and rejects prereleases on stable', async () => {
const nightly = fixture('nightly', '1.0.0')
expect(nightly.updater.channel).toBe('nightly')
expect(nightly.updater.allowPrerelease).toBe(true)
expect((await nightly.coordinator.check()).phase).toBe('available')
expect(nightly.updater.channel).toBe('nightly')
const stable = fixture('stable', '1.0.0')
expect(stable.updater.channel).toBe('stable')
expect(stable.updater.allowPrerelease).toBe(false)
expect(stable.updater.allowDowngrade).toBe(false)
expect((await stable.coordinator.check()).phase).toBe('error')
expect(stable.downloadUpdate).not.toHaveBeenCalled()
it.each(['nightly', 'stable'] as const)('accepts newer prereleases on the selected %s channel', async (channel) => {
const f = fixture(channel, '1.0.0')
expect(f.updater.channel).toBe(channel)
expect(f.updater.allowPrerelease).toBe(true)
expect(f.updater.allowDowngrade).toBe(false)
for (const version of ['1.1.0-alpha.1', '1.1.0-rc.2', '1.1.0']) {
f.checkForUpdates.mockResolvedValueOnce({ isUpdateAvailable: true, updateInfo: { version } })
expect(await f.coordinator.check()).toMatchObject({ phase: 'available', version })
expect(f.updater.channel).toBe(channel)
}
})
it.each(['nightly', 'stable'] as const)('rejects older, equal and malformed versions on %s', async (channel) => {
for (const version of ['0.9.0', '1.0.0-rc.1', '1.0.0', 'invalid']) {
const f = fixture(channel, '1.0.0')
f.checkForUpdates.mockResolvedValueOnce({ isUpdateAvailable: true, updateInfo: { version } })
expect((await f.coordinator.check()).phase).toBe(version === 'invalid' ? 'error' : 'idle')
await expect(f.coordinator.download(version)).rejects.toThrow()
await expect(f.coordinator.install(version)).rejects.toThrow()
expect(f.downloadUpdate).not.toHaveBeenCalled()
expect(f.quitAndInstall).not.toHaveBeenCalled()
}
})
@@ -60,8 +60,14 @@ async function main() {
await target.checkForUpdates()
assert.equal(requests.at(-1), `/stable${process.platform === 'darwin' ? '-mac' : ''}.yml`)
assert.equal(target.allowDowngrade, false)
assert.equal(target.allowPrerelease, false)
version = '0.9.0'
assert.equal(target.allowPrerelease, true)
for (const candidate of ['2.0.0-alpha.1', '2.0.0-rc.1', '2.0.0']) {
version = candidate
await discardDesktopUpdate(target)
assert.equal((await target.checkForUpdates()).isUpdateAvailable, true)
assert.equal(target.updateInfoAndProvider.info.version, candidate)
}
version = '0.9.0-rc.1'
await discardDesktopUpdate(target)
await target.checkForUpdates()
assert.equal(target.updateInfoAndProvider, null)
@@ -98,7 +104,7 @@ async function main() {
// A later process can encounter files that a previous process could not remove.
await writeFile(file, oldPayload)
await writeFile(join(pending, 'update-info.json'), cachedInfo)
version = '2.1.0'
version = '2.1.0-rc.1'
payload = Buffer.from('different release after obsolete cache survived')
sha512 = createHash('sha512').update(payload).digest('base64')
const recovered = new Target(undefined, adapter)
@@ -115,7 +121,7 @@ async function main() {
await recovered.checkForUpdates()
await recovered.downloadUpdate()
assert.deepEqual(await readFile(recovered.downloadedUpdateHelper.file), payload)
assert.equal(recovered.downloadedUpdateHelper.versionInfo.version, '2.1.0')
assert.equal(recovered.downloadedUpdateHelper.versionInfo.version, '2.1.0-rc.1')
console.log('PASS: obsolete cache rejected by a new updater instance; current payload verified')
console.log('PASS: real provider channel switching, rollout eligibility, real downloads, native platform cache and proxy retirement')
} finally {
@@ -12,5 +12,5 @@
en: bca4377ec9032e3b
zh: 6da9f3be04f1cd39
/desktop-stable-channel-name/migration:
en: 8442b24f420f6be2
zh: b2517178eccbf8d5
en: a7f4cf090fde3729
zh: 3dd080660f49e7c6
@@ -13,6 +13,6 @@ Desktop packages, update feeds, and channel configuration use `stable` instead o
## Migration
1. Select `stable` in packaging workflows or set `DSH_DESKTOP_UPDATE_CHANNEL=stable` when building a stable-channel package. Build a stable version, without a prerelease suffix.
1. Select `stable` in packaging workflows or set `DSH_DESKTOP_UPDATE_CHANNEL=stable` when building a stable-channel package. Select the channel independently of the version suffix; stable also accepts alpha and RC versions.
2. If a local Desktop settings file contains `updates.channel: "latest"`, change the value to `"stable"` and restart Desktop. Omitting the field uses the package default.
3. Publish with the updated tooling: it writes `stable.yml` on Windows and `stable-mac.yml` on macOS within each architecture's feed directory, with stable-channel binaries under `stable/`. Verify the selected channel and requested feed in the test deployment before production publication. Old packages still requesting `latest` feeds do not migrate automatically.
@@ -13,6 +13,6 @@ description: "桌面正式更新渠道由 latest 更名为 stable。"
## 迁移
1. 在打包工作流中选择 `stable`,或在构建正式渠道包时设置 `DSH_DESKTOP_UPDATE_CHANNEL=stable`。使用不含预发布后缀的稳定版本号。
1. 在打包工作流中选择 `stable`,或在构建正式渠道包时设置 `DSH_DESKTOP_UPDATE_CHANNEL=stable`。渠道选择独立于版本后缀;stable 也接受 alpha 和 RC 版本。
2. 如果本地桌面配置文件包含 `updates.channel: "latest"`,将该值改为 `"stable"` 并重启桌面应用。省略该字段时使用安装包默认值。
3. 使用更新后的工具发布:在各架构的清单目录内,Windows 写入 `stable.yml`,macOS 写入 `stable-mac.yml`;正式渠道二进制位于 `stable/` 下。在正式发布前,通过测试部署核对选定渠道和请求的清单。仍请求 `latest` 清单的旧包不会自动迁移。