From 152d0294c2e491c3ba8e3cf2fe410ef5ea63f85e Mon Sep 17 00:00:00 2001 From: winewill Date: Tue, 3 Nov 2026 19:10:11 +0800 Subject: [PATCH 01/10] fix(desktop): decouple update channels from version suffixes --- .github/workflows/macos-intel-package.yml | 2 +- .github/workflows/macos-package.yml | 2 +- .github/workflows/windows-package.yml | 2 +- apps/desktop/packaging/README.i18n.yaml | 4 +- apps/desktop/packaging/README.md | 2 +- apps/desktop/packaging/README.zh.md | 2 +- apps/desktop/publishing/ci/README.i18n.yaml | 4 +- apps/desktop/publishing/ci/README.md | 2 +- apps/desktop/publishing/ci/README.zh.md | 2 +- apps/desktop/scripts/desktop-ci-version.mjs | 1 - apps/desktop/scripts/desktop-upload-plan.ts | 1 - .../scripts/electron-builder-config.mjs | 1 - apps/desktop/src/update-coordinator.ts | 1 - .../src/updates/electron-updater/channel.ts | 4 +- .../tests/desktop-channel-version.spec.ts | 36 ++++++++++++++++ .../desktop/tests/desktop-upload-plan.spec.ts | 21 +++++++++- .../expected/stable-prerelease-mac-arm64.json | 42 +++++++++++++++++++ .../expected/stable-prerelease-mac-x64.json | 42 +++++++++++++++++++ .../expected/stable-prerelease-win-x64.json | 38 +++++++++++++++++ apps/desktop/tests/update-coordinator.spec.ts | 34 +++++++++------ .../electron-updater/qualification.mjs | 14 +++++-- .../desktop-stable-channel/guide.i18n.yaml | 4 +- .../desktop-stable-channel/guide.md | 2 +- .../desktop-stable-channel/guide.zh.md | 2 +- 24 files changed, 226 insertions(+), 39 deletions(-) create mode 100644 apps/desktop/tests/desktop-channel-version.spec.ts create mode 100644 apps/desktop/tests/expected/stable-prerelease-mac-arm64.json create mode 100644 apps/desktop/tests/expected/stable-prerelease-mac-x64.json create mode 100644 apps/desktop/tests/expected/stable-prerelease-win-x64.json diff --git a/.github/workflows/macos-intel-package.yml b/.github/workflows/macos-intel-package.yml index 01308037fe..77672e2c51 100644 --- a/.github/workflows/macos-intel-package.yml +++ b/.github/workflows/macos-intel-package.yml @@ -15,7 +15,7 @@ on: default: test required: true update_channel: - description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定' + description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定' type: choice options: [nightly, stable] default: nightly diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 4b8fd840b2..73401cf2c7 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -15,7 +15,7 @@ on: default: test required: true update_channel: - description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定' + description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定' type: choice options: [nightly, stable] default: nightly diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index 7a6ec241d5..2f01d18706 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -15,7 +15,7 @@ on: default: test required: true update_channel: - description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定' + description: '升级通道:人工选择 nightly 或 stable,与版本后缀无关;默认通道由此打包输入确定' type: choice options: [nightly, stable] default: nightly diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 0589a36c5e..292a1fcb2e 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -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 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 93214bd994..86d7447938 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -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` 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..` 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..` 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). diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 383e342637..3ec108ac0c 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -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..` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。 +安装包独立于版本和发布环境内置 `DSH_DESKTOP_UPDATE_CHANNEL`(默认 `nightly`,也可选择 `stable`)。该值提供默认渠道;[桌面设置](../README.zh.md#local-desktop-settings)中保存的选择优先,包括安装另一个包之后。操作人员独立于版本后缀选择渠道;两个渠道均接受正式版本和预发布版本。稳定产品可生成 `-test..` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。 diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index 70c5d0e62b..f17d53f1d6 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -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 diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index eb21407702..5503d71628 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -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. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 91e2ac717d..041bb73c4b 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -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` 不影响发布资格;本机文件缺失或变化会停止发布。 diff --git a/apps/desktop/scripts/desktop-ci-version.mjs b/apps/desktop/scripts/desktop-ci-version.mjs index 3822b8329f..49a21baee2 100644 --- a/apps/desktop/scripts/desktop-ci-version.mjs +++ b/apps/desktop/scripts/desktop-ci-version.mjs @@ -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 diff --git a/apps/desktop/scripts/desktop-upload-plan.ts b/apps/desktop/scripts/desktop-upload-plan.ts index e6a8d4c7f0..e94c24d18a 100644 --- a/apps/desktop/scripts/desktop-upload-plan.ts +++ b/apps/desktop/scripts/desktop-upload-plan.ts @@ -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 diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index c2563a0edf..d5522e3da9 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -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, diff --git a/apps/desktop/src/update-coordinator.ts b/apps/desktop/src/update-coordinator.ts index 29cf2d88f3..475a560667 100644 --- a/apps/desktop/src/update-coordinator.ts +++ b/apps/desktop/src/update-coordinator.ts @@ -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 }) diff --git a/apps/desktop/src/updates/electron-updater/channel.ts b/apps/desktop/src/updates/electron-updater/channel.ts index 06f07627b2..a67dfdacde 100644 --- a/apps/desktop/src/updates/electron-updater/channel.ts +++ b/apps/desktop/src/updates/electron-updater/channel.ts @@ -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 } diff --git a/apps/desktop/tests/desktop-channel-version.spec.ts b/apps/desktop/tests/desktop-channel-version.spec.ts new file mode 100644 index 0000000000..f720a58ac6 --- /dev/null +++ b/apps/desktop/tests/desktop-channel-version.spec.ts @@ -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' }]) +}) diff --git a/apps/desktop/tests/desktop-upload-plan.spec.ts b/apps/desktop/tests/desktop-upload-plan.spec.ts index 18f3fa18fa..33b7f989ef 100644 --- a/apps/desktop/tests/desktop-upload-plan.spec.ts +++ b/apps/desktop/tests/desktop-upload-plan.spec.ts @@ -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 () => { diff --git a/apps/desktop/tests/expected/stable-prerelease-mac-arm64.json b/apps/desktop/tests/expected/stable-prerelease-mac-arm64.json new file mode 100644 index 0000000000..97afcbe70e --- /dev/null +++ b/apps/desktop/tests/expected/stable-prerelease-mac-arm64.json @@ -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" + ] + } +] diff --git a/apps/desktop/tests/expected/stable-prerelease-mac-x64.json b/apps/desktop/tests/expected/stable-prerelease-mac-x64.json new file mode 100644 index 0000000000..45e103bb57 --- /dev/null +++ b/apps/desktop/tests/expected/stable-prerelease-mac-x64.json @@ -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" + ] + } +] diff --git a/apps/desktop/tests/expected/stable-prerelease-win-x64.json b/apps/desktop/tests/expected/stable-prerelease-win-x64.json new file mode 100644 index 0000000000..f1174feee9 --- /dev/null +++ b/apps/desktop/tests/expected/stable-prerelease-win-x64.json @@ -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" + ] + } +] diff --git a/apps/desktop/tests/update-coordinator.spec.ts b/apps/desktop/tests/update-coordinator.spec.ts index 75521799f4..621a00fb6c 100644 --- a/apps/desktop/tests/update-coordinator.spec.ts +++ b/apps/desktop/tests/update-coordinator.spec.ts @@ -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() + } }) diff --git a/apps/desktop/tests/updates/electron-updater/qualification.mjs b/apps/desktop/tests/updates/electron-updater/qualification.mjs index c8243d5480..e5bb26ab47 100644 --- a/apps/desktop/tests/updates/electron-updater/qualification.mjs +++ b/apps/desktop/tests/updates/electron-updater/qualification.mjs @@ -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 { diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml index ce81afec05..49ca2a4270 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml @@ -12,5 +12,5 @@ en: bca4377ec9032e3b zh: 6da9f3be04f1cd39 /desktop-stable-channel-name/migration: - en: 8442b24f420f6be2 - zh: b2517178eccbf8d5 + en: a7f4cf090fde3729 + zh: 3dd080660f49e7c6 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md index 9261aa2960..2514bf3020 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md @@ -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. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md index 441693811b..9eb899526a 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md @@ -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` 清单的旧包不会自动迁移。 From 950b63b37b9a26126aa02b050e368cd89ddd8577 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:06:08 +0800 Subject: [PATCH 02/10] test(desktop): verify prerelease downgrade rejection --- apps/desktop/tests/desktop-publish.spec.ts | 15 +++++++++++++++ apps/desktop/tests/update-coordinator.spec.ts | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/desktop/tests/desktop-publish.spec.ts b/apps/desktop/tests/desktop-publish.spec.ts index bc5cba2166..cf88751230 100644 --- a/apps/desktop/tests/desktop-publish.spec.ts +++ b/apps/desktop/tests/desktop-publish.spec.ts @@ -81,6 +81,21 @@ it.each(['binary', 'same-version-feed', 'newer-feed'] as const)('stops on %s con expect(f.writes).toHaveLength(0) }) +it.each(['1.0.0-rc.1', '1.0.0-test.20261104.1.1'])('rejects %s before writing when stable already serves its release version', async (version) => { + const f = fixture('production') + const plan: DesktopUploadPlan = { + ...f.plan, + version, + artifacts: f.plan.artifacts.map(artifact => artifact.channelMetadata + ? { ...artifact, filename: 'stable.yml', key: 'feed/stable.yml', contents: `version: ${version}\n` } + : artifact), + } + f.objects.set('feed/stable.yml', observed('version: 1.0.0\n')) + await expect(publishDesktopRelease(plan, f.operations)).rejects.toMatchObject({ code: 'superseded' }) + expect(f.writes).toEqual([]) + expect(f.tags()).toBe(0) +}) + it('rejects a files-only plan and a production build without clean commit evidence', async () => { const f = fixture('production') await expect(publishDesktopRelease({ ...f.plan, artifacts: f.plan.artifacts.slice(0, 2) }, f.operations)).rejects.toMatchObject({ code: 'invalid-plan' }) diff --git a/apps/desktop/tests/update-coordinator.spec.ts b/apps/desktop/tests/update-coordinator.spec.ts index 621a00fb6c..a70b4a11c4 100644 --- a/apps/desktop/tests/update-coordinator.spec.ts +++ b/apps/desktop/tests/update-coordinator.spec.ts @@ -261,8 +261,8 @@ it.each(['nightly', 'stable'] as const)('rejects older, equal and malformed vers 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() + await expect(f.coordinator.download(version)).rejects.toThrow(/no checked update/u) + await expect(f.coordinator.install(version)).rejects.toThrow(/not ready/u) expect(f.downloadUpdate).not.toHaveBeenCalled() expect(f.quitAndInstall).not.toHaveBeenCalled() } From dedf7e65c547969b1c40d2217786ddb7334f22a5 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:15:17 +0800 Subject: [PATCH 03/10] test(desktop): cover native prerelease downgrade and Windows config --- .../desktop/tests/desktop-channel-version.spec.ts | 15 +++++++++++++++ apps/desktop/tests/update-coordinator.spec.ts | 2 +- .../updates/electron-updater/qualification.mjs | 10 ++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/desktop/tests/desktop-channel-version.spec.ts b/apps/desktop/tests/desktop-channel-version.spec.ts index f720a58ac6..9a5b77ee53 100644 --- a/apps/desktop/tests/desktop-channel-version.spec.ts +++ b/apps/desktop/tests/desktop-channel-version.spec.ts @@ -34,3 +34,18 @@ it.each(['arm64', 'x64'] as const)('embeds stable in a prerelease macOS %s packa 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' }]) }) + +it('accepts a stable prerelease in an unsigned Windows x64 configuration', () => { + 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_UNSIGNED: '1', + }, 'win32', 'x64') + expect(config.extraMetadata).toMatchObject({ version }) + expect(config.publish).toBeNull() +}) diff --git a/apps/desktop/tests/update-coordinator.spec.ts b/apps/desktop/tests/update-coordinator.spec.ts index a70b4a11c4..b65f825bbc 100644 --- a/apps/desktop/tests/update-coordinator.spec.ts +++ b/apps/desktop/tests/update-coordinator.spec.ts @@ -262,7 +262,7 @@ it.each(['nightly', 'stable'] as const)('rejects older, equal and malformed vers 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(/no checked update/u) - await expect(f.coordinator.install(version)).rejects.toThrow(/not ready/u) + await expect(f.coordinator.install(version)).rejects.toThrow(/confirmed target is not ready/u) expect(f.downloadUpdate).not.toHaveBeenCalled() expect(f.quitAndInstall).not.toHaveBeenCalled() } diff --git a/apps/desktop/tests/updates/electron-updater/qualification.mjs b/apps/desktop/tests/updates/electron-updater/qualification.mjs index e5bb26ab47..83eeb6a3f9 100644 --- a/apps/desktop/tests/updates/electron-updater/qualification.mjs +++ b/apps/desktop/tests/updates/electron-updater/qualification.mjs @@ -67,10 +67,12 @@ async function main() { 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) + for (const candidate of ['0.9.0-rc.1', '1.0.0-rc.1']) { + version = candidate + await discardDesktopUpdate(target) + assert.equal((await target.checkForUpdates()).isUpdateAvailable, false) + assert.equal(target.updateInfoAndProvider, null) + } version = '2.0.0' percentage = 0 await discardDesktopUpdate(target) From 0fdd91f3e6431150c5a1283656075f1625415198 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:40:11 +0800 Subject: [PATCH 04/10] docs(desktop): align channel selection guidance --- ...5-electron-desktop-packaging-and-updates.i18n.yaml | 4 ++-- ...26-08-25-electron-desktop-packaging-and-updates.md | 2 +- ...08-25-electron-desktop-packaging-and-updates.zh.md | 2 +- .../2026-09-08-desktop-update-extensions.i18n.yaml | 4 ++-- .../feature/2026-09-08-desktop-update-extensions.md | 11 +---------- .../2026-09-08-desktop-update-extensions.zh.md | 11 +---------- apps/desktop/README.i18n.yaml | 4 ++-- apps/desktop/README.md | 2 +- apps/desktop/README.zh.md | 2 +- 9 files changed, 12 insertions(+), 30 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml index ad94eb0bd5..3112e2938d 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml @@ -21,8 +21,8 @@ en: 29def175ab33b7d6 zh: 87e55ec28c20c7c1 /agent-note-package-and-update-the-electron-desktop-application/updates-and-recovery: - en: 4cb7014acfe71762 - zh: 2178e0337cba0324 + en: 61a02870beb953ec + zh: 205ecce330959e9a /agent-note-package-and-update-the-electron-desktop-application/security-and-release-policy: en: 5faaa53b58121311 zh: e1c3fc8d21a27d1a diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md index 0241afef65..5821faf5a6 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md @@ -73,7 +73,7 @@ Electron update uses one `electron-updater` release stream and signed `electron- The [immediate-window reference](../../../../apps/desktop/README.md) owns the local loading page, direct Host startup, and recovery in the main window. Profile reconciliation follows the [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). -`DSH_DESKTOP_AUTO_UPDATE_ENV` selects the test deployment by default or the production deployment for both the target-specific generic-provider URL and COS destination. Release automation supplies the test HTTPS origin through `DOWNLOAD_TEST_ORIGIN` and each deployment's bucket through `DOWNLOAD_TEST_COS_BUCKET` or `DOWNLOAD_PROD_COS_BUCKET`; keeping mutable test routing and COS storage identities out of source lets deployment infrastructure change without a code release, while the public production origin remains fixed. Packaging resolves only the public updater URL, disables electron-builder publishing, removes every COS credential field from its subprocess environment, and writes a completion record only after electron-builder and every signing or notarization hook succeeds. Target upload additionally requires the selected bucket, then requires the completion record, root dsh version, Desktop version, version-derived channel metadata, artifact names, sizes, and SHA-512 values to agree before it reads the selected credentials or sends data. It uploads immutable versioned updater payloads and any separate blockmaps before replacing the channel metadata emitted by electron-builder, and it never deletes historical objects. Stable versions use the `latest` metadata name; prereleases use the first semantic-version prerelease identifier. NSIS embeds its blockmap in the signed executable; the macOS ZIP carries a separate blockmap. Both let electron-updater download changed blocks when supported, while application replacement and the local pnpm package operation remain separate operations. +`DSH_DESKTOP_AUTO_UPDATE_ENV` selects the test deployment by default or the production deployment for both the target-specific generic-provider URL and COS destination. Release automation supplies the test HTTPS origin through `DOWNLOAD_TEST_ORIGIN` and each deployment's bucket through `DOWNLOAD_TEST_COS_BUCKET` or `DOWNLOAD_PROD_COS_BUCKET`; keeping mutable test routing and COS storage identities out of source lets deployment infrastructure change without a code release, while the public production origin remains fixed. Packaging resolves only the public updater URL, disables electron-builder publishing, removes every COS credential field from its subprocess environment, and writes a completion record only after electron-builder and every signing or notarization hook succeeds. Target upload additionally requires the selected bucket, then requires the completion record, root dsh version, Desktop version, channel-selected metadata, artifact names, sizes, and SHA-512 values to agree before it reads the selected credentials or sends data. It uploads immutable versioned updater payloads and any separate blockmaps before replacing the channel metadata emitted by electron-builder, and it never deletes historical objects. The selected channel determines the metadata name: `stable.yml` or `nightly.yml`, with `-mac` before the extension for macOS; version suffixes do not select the channel. NSIS embeds its blockmap in the signed executable; the macOS ZIP carries a separate blockmap. Both let electron-updater download changed blocks when supported, while application replacement and the local pnpm package operation remain separate operations. Test releases require `DOWNLOAD_TEST_RELEASE_ID` from the platform dotenv file. Their feeds and binaries share `dsh-desk//`; production retains its fixed directories. The ID is part of the packaged update URL, so upload rejects a different ID. Internal distribution can supply a new installer after rotation; installed clients retain their existing feed. Random directories reduce guessing but provide no authorization for link holders. The [release configuration](../../../../apps/desktop/publishing/README.md#upload-updates) defines generation and reuse. diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md index f3075a175d..50cf872de0 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md @@ -73,7 +73,7 @@ Electron 更新只使用一个 `electron-updater` 发布流和签名 `electron-b [立即显示窗口参考](../../../../apps/desktop/README.zh.md)负责本地加载页、直接启动 Host 和主窗口恢复。profile 协调遵循[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)。 -`DSH_DESKTOP_AUTO_UPDATE_ENV` 默认为测试部署,也可以选择生产部署,并同时决定目标专用的 generic-provider URL 与 COS 目标。发布自动化通过 `DOWNLOAD_TEST_ORIGIN` 提供测试 HTTPS origin,并通过 `DOWNLOAD_TEST_COS_BUCKET` 或 `DOWNLOAD_PROD_COS_BUCKET` 提供各部署的 bucket;可变的测试路由与 COS 存储身份不写入源码,部署基础设施变更时无需发布新代码,而公开的生产 origin 仍固定。打包只解析公开更新 URL、禁止 electron-builder 发布、从子进程环境中删除每个 COS 凭据字段,并且只有在 electron-builder 以及每个签名或公证 hook 成功后才写入完成记录。目标上传还必须提供所选 bucket,随后会先要求完成记录、根 dsh 版本、Desktop 版本、根据版本得出的频道元数据、产物名称、大小与 SHA-512 全部一致,再读取所选凭据或发送数据。它先上传不可变且带版本的更新载荷与所有独立 blockmap,最后替换 electron-builder 生成的频道元数据,并且不会删除历史对象。稳定版本使用 `latest` 元数据名称,预发布版本则使用语义化版本的第一个预发布标识符。NSIS 把 blockmap 嵌入已签名的可执行文件,macOS ZIP 则使用独立 blockmap;两者都让 electron-updater 在平台支持时只下载变化的数据块,而应用替换与本地 pnpm 包操作仍是两个独立操作。 +`DSH_DESKTOP_AUTO_UPDATE_ENV` 默认为测试部署,也可以选择生产部署,并同时决定目标专用的 generic-provider URL 与 COS 目标。发布自动化通过 `DOWNLOAD_TEST_ORIGIN` 提供测试 HTTPS origin,并通过 `DOWNLOAD_TEST_COS_BUCKET` 或 `DOWNLOAD_PROD_COS_BUCKET` 提供各部署的 bucket;可变的测试路由与 COS 存储身份不写入源码,部署基础设施变更时无需发布新代码,而公开的生产 origin 仍固定。打包只解析公开更新 URL、禁止 electron-builder 发布、从子进程环境中删除每个 COS 凭据字段,并且只有在 electron-builder 以及每个签名或公证 hook 成功后才写入完成记录。目标上传还必须提供所选 bucket,随后会先要求完成记录、根 dsh 版本、Desktop 版本、根据所选渠道确定的元数据、产物名称、大小与 SHA-512 全部一致,再读取所选凭据或发送数据。它先上传不可变且带版本的更新载荷与所有独立 blockmap,最后替换 electron-builder 生成的频道元数据,并且不会删除历史对象。所选渠道决定元数据名称:`stable.yml` 或 `nightly.yml`,macOS 在扩展名前加 `-mac`;版本后缀不决定渠道。NSIS 把 blockmap 嵌入已签名的可执行文件,macOS ZIP 则使用独立 blockmap;两者都让 electron-updater 在平台支持时只下载变化的数据块,而应用替换与本地 pnpm 包操作仍是两个独立操作。 测试发布必须从平台 dotenv 文件提供 `DOWNLOAD_TEST_RELEASE_ID`。清单和安装包共用 `dsh-desk//`,生产环境保留固定目录。ID 是打包更新 URL 的一部分,因此上传会拒绝不同的 ID。轮换后可通过内部分发提供新安装包,已安装客户端保留原清单地址。随机目录降低被猜中的概率,但不对持有链接者进行鉴权。[发布配置](../../../../apps/desktop/publishing/README.zh.md#upload-updates)定义生成与复用方式。 diff --git a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.i18n.yaml b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.i18n.yaml index ce51330f2a..7f183184ef 100644 --- a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.i18n.yaml @@ -15,8 +15,8 @@ en: 403bd786a8411349 zh: c44584e8a7e5b74a /agent-note-deferred-desktop-update-extensions/proposal/stable-subscription-and-channel-switching: - en: f39b1ed0d941d372 - zh: 05b9a006c9ec0d3c + en: 33663c160d5e280b + zh: 96689283c31ed820 /agent-note-deferred-desktop-update-extensions/proposal/automatic-installation-setting: en: 758d49f0a7dc6707 zh: ddb0bfe616eb93bc diff --git a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.md b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.md index 6c8a2ebe4a..a5fad6dbd5 100644 --- a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.md +++ b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.md @@ -29,16 +29,7 @@ Do not use `0.1.3-dsk.1` to repair stable `0.1.3`: SemVer orders it below the st ### Stable subscription and channel switching -| User option | Feed | Release contents | -|---|---|---| -| Stable | `latest.yml` / `latest-mac.yml` | Stable only | -| Nightly | `nightly.yml` / `nightly-mac.yml` | Alpha, rc, and stable | - -The [initial publication rule](2026-09-08-desktop-update-policy-and-installation.md) adds the stable feed when shared stable releases exist, without switching fixed-Nightly clients. A user-facing stable subscription and selector remain deferred. If enabled, persist the selected channel; never infer it from the installed version. Preserve `allowDowngrade = false` when assigning the updater channel. Moving from Nightly to stable keeps a higher installed Nightly until a higher stable version exists, without downgrading Desktop or bundled dsh. - -Channel switching may immediately check; download behavior must follow the approved setting. Ignore old-channel responses and reconfirm downloaded artifacts against the new channel before installation. A channel change does not clear mandatory blocking without a valid policy response for the new conditions. Display a persistent Nightly explanation under the selector, with candidate Chinese copy “提前体验新功能,版本可能不稳定。”; hide it after selecting stable, while separately explaining any wait for a higher stable version. - -Design compatibility only for Desktop packages actually distributed. If a future migration has real clients reading `alpha.yml` or `rc.yml`, publish a higher bridge version to the old feed and Nightly until the old entry can be retired. No such bridge is required initially. +This part of the proposal is superseded by [Desktop channel settings](../../../../apps/desktop/README.md#local-desktop-settings) and the [packaging channel policy](../../../../apps/desktop/packaging/README.md). The implemented selector persists explicit user choices; publishers select versions independently of channel names, and channel changes do not permit downgrades. ### Automatic-installation setting diff --git a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.zh.md b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.zh.md index 06096c8b28..2b7ca7ea52 100644 --- a/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.zh.md +++ b/.agents/notes/proposed/feature/2026-09-08-desktop-update-extensions.zh.md @@ -29,16 +29,7 @@ Status: proposed ### 稳定版订阅与通道切换 -| 用户选项 | Feed | 发布内容 | -|---|---|---| -| 稳定版 | `latest.yml` / `latest-mac.yml` | 仅稳定版本 | -| Nightly | `nightly.yml` / `nightly-mac.yml` | alpha、rc 和稳定版本 | - -[一期发布规则](2026-09-08-desktop-update-policy-and-installation.zh.md)在共用稳定版本出现时增加稳定 feed,不切换固定 Nightly 客户端。面向用户的稳定版订阅与选择器仍延后。启用后持久化所选通道,不从已安装版本反推。设置 updater 通道时保持 `allowDowngrade = false`。从 Nightly 切到稳定版时,较高的已安装 Nightly 持续运行,直到出现更高稳定版;不得降级 Desktop 或内置 dsh。 - -切换通道可以立即检查,下载行为必须遵循获准设置。忽略旧通道响应,安装前按新通道重新确认已下载产物。通道改变不能在缺少新条件有效策略响应时解除强制阻断。选择器下方常驻 Nightly 说明,候选中文为“提前体验新功能,版本可能不稳定。”;选择稳定版后隐藏,必要时另行说明需等待更高稳定版。 - -仅为实际分发过的 Desktop 安装包设计兼容。后续迁移若存在读取 `alpha.yml` 或 `rc.yml` 的真实客户端,应向旧 feed 与 Nightly 发布更高桥接版本,直到可淘汰旧入口。一期不需要这类桥接。 +本提案的这一部分已由[桌面渠道设置](../../../../apps/desktop/README.zh.md#local-desktop-settings)和[打包渠道策略](../../../../apps/desktop/packaging/README.zh.md)取代。已实现的选择器持久化用户的显式选择;发布者独立于渠道名称选择版本,切换渠道不允许降级。 ### 自动安装设置 diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 081d44f9f8..b9e1607113 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -60,8 +60,8 @@ en: 67bf0a18faa7aa76 zh: 782e14ce62bb939f /deepseek-harness-desktop/updates/local-desktop-settings: - en: 01461be5b81e8609 - zh: 58e818df821a3aa2 + en: dbe57394894b7368 + zh: a3bb788c2ce631a4 /deepseek-harness-desktop/updates/mandatory-update-policy: en: 182a9c6705213e13 zh: 681762f83490f4c6 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 335991f722..0dfafe98b1 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -304,7 +304,7 @@ On first launch, including after an upgrade, Desktop creates this document if it Startup loading never overwrites existing documents; an omitted `updates` object or field resolves to `false`. Set the field to `true` to permit test authentication dialogs, then fully quit and restart the application. Set it back to `false` and restart to suppress them again. Creation, read, or validation failures use the default `false` without blocking startup. Desktop attempts to warn through the Electron console with the file path; unavailable logging does not block startup. Correct the file and restart to apply an explicit setting. Unknown fields are preserved. Installation updates retain this user-data file; Windows uninstallation removes it under the [uninstall rules](#windows-uninstall-rules). -In Desktop **Settings → General → Update channel**, **Stable** selects `stable` and **Development** selects `nightly`. The main process saves `updates.channel` in this same file and applies the choice immediately through IPC; no Host settings service is involved. An omitted channel uses the build-injected package default. External edits and deletion are read only on the next launch; no file watcher runs. The authentication-popup setting remains startup-only. UI writes retain unrelated fields from the latest valid document; external editors must not write concurrently with a UI save. +In Desktop **Settings → General → Update channel**, **Stable** selects `stable` and **Development** selects `nightly`. Publishers choose versions for each channel independently of version suffixes; Stable can also deliver alpha or RC versions. The main process saves `updates.channel` in this same file and applies the choice immediately through IPC; no Host settings service is involved. An omitted channel uses the build-injected package default. External edits and deletion are read only on the next launch; no file watcher runs. The authentication-popup setting remains startup-only. UI writes retain unrelated fields from the latest valid document; external editors must not write concurrently with a UI save. During a UI switch, the selector shows the pending choice with a loading indicator. Failure restores the actual runtime selection and shows a short toast; ordinary save failures keep the current channel and update target accessible. Invalid startup settings or failure to retire the previous update suspends update operations; without a mandatory policy block, the application remains usable. Startup settings errors show a non-blocking ten-second toast with a details action. The update indicator retains the suspension reason independently of download progress; open it for recovery guidance and a button to reveal the settings file. Fix the file, then choose **Reload settings** in the error details or restart; this explicit reload applies only update settings and never rewrites the file. The reload button shows a busy indicator for at least half a second and rejects repeated clicks. Invalid files keep updates paused. Selecting a valid channel in Settings can also recover an invalid channel; malformed JSON must be repaired first. A known mandatory-update block remains enforced; its configuration-error action opens the same file-location and reload dialog. Successful channel changes trigger a fresh mandatory-policy check, waiting for an older channel’s in-flight check if necessary. Successful switching retires pending checks, downloads and installation eligibility before checking the new feed, including when both feeds advertise the same version. Switching never downgrades or installs automatically. Installation preparation rejects UI switches; once `quitAndInstall()` hands off, file edits cannot cancel that installation. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 82ccf994a4..5fa852fc9e 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -304,7 +304,7 @@ Electron 主进程在启动时读取一次 `app.getPath('userData')/desktop/sett 启动读取不会覆盖已有文件;缺少 `updates` 对象或该字段时均按 `false` 处理。将字段设为 `true` 后完全退出并重启应用,即可允许测试鉴权弹窗。改回 `false` 并重启即可再次禁止。创建、读取或校验失败时使用默认值 `false`,不阻止启动。Desktop 尝试在 Electron 控制台记录包含文件路径的警告;日志不可用也不阻止启动。修正文件并重启后才会应用显式设置。未知字段会保留。安装更新保留此用户数据文件;Windows 卸载按[卸载规则](#windows-uninstall-rules)删除它。 -在桌面版**设置 → 通用设置 → 更新渠道**中,**正式版**选择 `stable`,**开发版**选择 `nightly`。主进程在同一文件中保存 `updates.channel`,并通过 IPC 立即应用选择,不依赖 Host 设置服务。省略 channel 时使用打包注入的包内默认值。外部编辑和删除仅在下次启动读取,不运行文件监听器。鉴权弹窗设置仍须重启生效。UI 写入保留最新有效文件中的其他字段;外部编辑器不能与 UI 保存同时写入。 +在桌面版**设置 → 通用设置 → 更新渠道**中,**正式版**选择 `stable`,**开发版**选择 `nightly`。发布者独立于版本后缀为各渠道选择版本;正式版渠道也可以提供 alpha 或 RC 版本。主进程在同一文件中保存 `updates.channel`,并通过 IPC 立即应用选择,不依赖 Host 设置服务。省略 channel 时使用打包注入的包内默认值。外部编辑和删除仅在下次启动读取,不运行文件监听器。鉴权弹窗设置仍须重启生效。UI 写入保留最新有效文件中的其他字段;外部编辑器不能与 UI 保存同时写入。 UI 切换期间,选择器显示待应用选项和加载指示;失败时恢复实际运行渠道,并显示简短 toast。普通保存失败不会阻止使用当前渠道及旧更新目标。启动配置无效或无法撤销旧更新安装资格会暂停更新操作;没有强更策略阻塞时,主应用仍可使用。启动配置错误会显示持续十秒、可查看详情的非阻塞提示。更新入口独立于下载进度保留暂停原因;打开后可查看恢复指引、配置路径并在系统文件管理器中定位文件。修正文件后,可在错误详情中选择**重新读取配置**或重启;显式重读只应用更新配置,不改写文件。重读按钮至少显示半秒加载指示,期间不接受重复点击;文件仍无效时继续暂停更新。在设置中选择有效渠道也可恢复无效渠道;JSON 损坏须先修复文件。已知强更阻塞继续生效;强更页面的配置异常入口可打开同一文件定位和重读对话框。渠道切换成功后立即重新检查强更策略;如旧渠道的检查仍在进行,则等待其完成后检查新渠道。切换成功时先收尾检查、下载并撤销安装资格,再检查新渠道;两个渠道版本号相同也遵循该规则。切换不降级,也不自动安装。安装准备期间拒绝 UI 切换;调用 `quitAndInstall()` 交接后,编辑文件不能取消本次安装。 From d65fd521b536489e2618e38c35a4a72f951c1568 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:51:39 +0800 Subject: [PATCH 05/10] fix(desktop): clarify workflow version mode names --- .github/workflows/macos-intel-package.yml | 4 ++-- .github/workflows/macos-package.yml | 4 ++-- .github/workflows/windows-package.yml | 4 ++-- apps/desktop/packaging/README.i18n.yaml | 8 ++++---- apps/desktop/packaging/README.md | 6 +++--- apps/desktop/packaging/README.zh.md | 6 +++--- apps/desktop/publishing/ci/README.i18n.yaml | 4 ++-- apps/desktop/publishing/ci/README.md | 2 +- apps/desktop/publishing/ci/README.zh.md | 2 +- apps/desktop/scripts/desktop-ci-version.mjs | 10 +++++----- apps/desktop/tests/desktop-channel-version.spec.ts | 9 +++++++-- apps/desktop/tests/prepare-windows-ci.spec.ts | 4 ++-- scripts/macos-package-workflow.spec.ts | 1 + scripts/windows-package-workflow.spec.ts | 1 + 14 files changed, 36 insertions(+), 29 deletions(-) diff --git a/.github/workflows/macos-intel-package.yml b/.github/workflows/macos-intel-package.yml index 77672e2c51..3f2d1d2cb2 100644 --- a/.github/workflows/macos-intel-package.yml +++ b/.github/workflows/macos-intel-package.yml @@ -21,9 +21,9 @@ on: default: nightly required: true version_mode: - description: '版本模式:default 为 test 自动编号、production 跟随产品;product 保留原版本,auto 自动编号,manual 手填' + description: '版本模式:default 按环境自动选择(test 取 build-number,production 取 follow-dsh);follow-dsh 跟随 dsh 包版本号;build-number 自动追加构建编号;manual 手填' type: choice - options: [default, product, auto, manual] + options: [default, follow-dsh, build-number, manual] default: default required: true build_version: diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 73401cf2c7..418a3b628d 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -21,9 +21,9 @@ on: default: nightly required: true version_mode: - description: '版本模式:default 为 test 自动编号、production 跟随产品;product 保留原版本,auto 自动编号,manual 手填' + description: '版本模式:default 按环境自动选择(test 取 build-number,production 取 follow-dsh);follow-dsh 跟随 dsh 包版本号;build-number 自动追加构建编号;manual 手填' type: choice - options: [default, product, auto, manual] + options: [default, follow-dsh, build-number, manual] default: default required: true build_version: diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index 2f01d18706..b59ea15c06 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -21,9 +21,9 @@ on: default: nightly required: true version_mode: - description: '版本模式:default 为 test 自动编号、production 跟随产品;product 保留原版本,auto 自动编号,manual 手填' + description: '版本模式:default 按环境自动选择(test 取 build-number,production 取 follow-dsh);follow-dsh 跟随 dsh 包版本号;build-number 自动追加构建编号;manual 手填' type: choice - options: [default, product, auto, manual] + options: [default, follow-dsh, build-number, manual] default: default required: true build_version: diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 8866f69447..db12d8d6e2 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -18,14 +18,14 @@ en: 0b35c7471c09b27a zh: cc4a382b99fc3210 /desktop-packaging/release-versions: - en: 0ce6f56a2e94f687 - zh: a878a5db6a405c68 + en: 2c42e40141c24dbd + zh: f057e32326681051 /desktop-packaging/configuration-and-local-builds: en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 7bd0f4aa899a7295 - zh: a27c75934844123d + en: 83706649764cd891 + zh: 05a7d0e853747cd6 /desktop-packaging/macos-self-hosted-packaging: en: 3bd14c640893b192 zh: 7c236cbe46be4a47 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index aa34bcd9e7..c6e00d5418 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -51,7 +51,7 @@ Record the current dsh version as the base. A production Desktop release uses th Use the actual creation date in Asia/Shanghai. For each base and date, start the index at 1 and increment after checking retained release records and published objects; never reuse a published version. Test distribution does not publish the corresponding unsuffixed base. -Manual Desktop CI uses a separate unpublished build identifier: `auto` appends the UTC date, workflow run number and attempt (`.YYYYMMDD.run.attempt`, with `-test` for a stable base). For example, `0.2.1-alpha.1.20261008.42.2`. These numbers are not reserved release sequence numbers. +Manual Desktop CI uses a separate unpublished build identifier: `build-number` appends the UTC date, workflow run number and attempt (`.YYYYMMDD.run.attempt`, with `-test` for a stable base). For example, `0.2.1-alpha.1.20261008.42.2`. These numbers are not reserved release sequence numbers. Pass the confirmed version to the packaging command as `--build-version`, which reaches the artifact names, the update feed, and the upload validation as one value. The manifests keep the product version, so a test build no longer rewrites the release family and leaves nothing to revert: @@ -109,9 +109,9 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t For supervised runner startup and recovery, follow [Windows runner machine setup](../windows-signing/README.md#machine-setup). -The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `build_version` defaults to `auto`, using the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. +The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (product version for production, build numbering for test); `build-number` uses the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. -Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f build_version=auto`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. +Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f version_mode=build-number`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. All Windows packaging modes, including test and unsigned, require the repository variable `DESKTOP_PUBLISH_RUNNERS`; configure the [original-machine route](../publishing/ci/README.md#machine-setup) before dispatch. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 062a408a8e..36908cba5c 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -51,7 +51,7 @@ description: "在本地或 Windows/macOS Actions 中生成 Desktop 安装包 日期使用实际创建时的 Asia/Shanghai 日期。每个基础版本、每天的序号从 1 开始,检查保留的发布记录与已发布对象后递增;绝不复用已发布版本。test 分发不发布对应的无后缀基础版本。 -Desktop 手动 CI 使用独立的未发布构建标识:`auto` 追加 UTC 日期、工作流运行编号和尝试次数(`.YYYYMMDD.run.attempt`,稳定基础版本使用 `-test`)。例如 `0.2.1-alpha.1.20261008.42.2`。这些编号不是预留的发布序号。 +Desktop 手动 CI 使用独立的未发布构建标识:`build-number` 追加 UTC 日期、工作流运行编号和尝试次数(`.YYYYMMDD.run.attempt`,稳定基础版本使用 `-test`)。例如 `0.2.1-alpha.1.20261008.42.2`。这些编号不是预留的发布序号。 把确认后的版本通过 `--build-version` 传给打包命令,该值同时决定产物文件名、更新 feed 与上传校验。清单保留产品版本,因此 test 打包不再改写发布家族,也不留下需要还原的改动: @@ -109,9 +109,9 @@ macOS 正式发布和本地未签名 DMG 都通过 [electron-builder 配置](../ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-signing/README.zh.md#machine-setup)。 -`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`build_version` 默认为 `auto`,使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 +`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用产品版本,test 追加构建编号);`build-number` 使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 -在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f build_version=auto` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 +在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f version_mode=build-number` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 所有 Windows 打包模式(含 test 和 unsigned)都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`;触发前配置[原机器路由](../publishing/ci/README.zh.md#machine-setup)。 diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index 03703afcc5..f0d00ca9c2 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -24,8 +24,8 @@ en: 0f43f2e193e95043 zh: 1c1efb062c06f7b8 /desktop-ci-publication/package-and-publish: - en: 041fde801a4dc1ae - zh: 447e3966241cd266 + en: dd0a0020cb4e911d + zh: 9521d4ba2e3ef31b /desktop-ci-publication/recovery-and-records: en: 266afba2adeeadef zh: 063114103585dae8 diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index 99b0ec8aee..7654af2064 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -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`. 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. +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 follow-dsh for production and build-number for test; follow-dsh preserves the selected checkout’s dsh version, build-number appends date/run/attempt in either environment, and manual requires `build_version`. Existing dispatch commands using `-f version_mode=auto` must use `-f version_mode=build-number`, and `-f version_mode=product` becomes `-f version_mode=follow-dsh`; the local CLI option `--build-version auto` is unchanged. 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. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 1ef63ecb18..fb5b46590e 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -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`。操作人员选择的渠道接受任意受支持的构建版本,包括 alpha 和 RC 版本;渠道不改变版本优先级,也不允许客户端降级。可选 `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 选择 follow-dsh,对 test 选择 build-number;follow-dsh 跟随所选源码的 dsh 包版本号,build-number 在任一环境均追加日期/run/attempt,manual 要求填写 `build_version`。已有派发命令中的 `-f version_mode=auto` 须改为 `-f version_mode=build-number`,`-f version_mode=product` 改为 `-f version_mode=follow-dsh`;本地 CLI 的 `--build-version auto` 不变。操作人员选择的渠道接受任意受支持的构建版本,包括 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` 不影响发布资格;本机文件缺失或变化会停止发布。 diff --git a/apps/desktop/scripts/desktop-ci-version.mjs b/apps/desktop/scripts/desktop-ci-version.mjs index 49a21baee2..bc57adfb5e 100644 --- a/apps/desktop/scripts/desktop-ci-version.mjs +++ b/apps/desktop/scripts/desktop-ci-version.mjs @@ -10,13 +10,13 @@ import { desktopBuildVersionPrefix, validateDesktopBuildVersion } from './deskto */ export function desktopCiVersion(environment, productVersion, now = new Date()) { const mode = environment.PACKAGE_VERSION_MODE - const selected = mode === 'default' ? (environment.DSH_PACKAGING_PRESET === 'production' ? 'product' : 'auto') : mode - if (selected !== undefined && !['product', 'auto', 'manual'].includes(selected)) throw new Error('Invalid version mode') - const requested = selected === 'product' ? productVersion : selected === 'auto' ? 'auto' : environment.PACKAGE_BUILD_VERSION?.trim() + const selected = mode === 'default' ? (environment.DSH_PACKAGING_PRESET === 'production' ? 'follow-dsh' : 'build-number') : mode + if (selected !== undefined && !['follow-dsh', 'build-number', 'manual'].includes(selected)) throw new Error('Invalid version mode') + const requested = selected === 'follow-dsh' ? productVersion : selected === 'build-number' ? 'auto' : environment.PACKAGE_BUILD_VERSION?.trim() if (selected === 'manual' && (!requested || requested === 'auto')) throw new Error('Manual version mode requires an explicit version') const date = now.toISOString().slice(0, 10).replaceAll('-', '') const example = `${desktopBuildVersionPrefix(productVersion)}${date}.1` - if (!requested) throw new Error(`Build version is required: use auto, ${productVersion}, or e.g. ${example}`) + if (!requested) throw new Error(`Build version is required: select version_mode=build-number, or use ${productVersion}, or e.g. ${example}`) let candidate = requested if (requested === 'auto') { for (const name of ['GITHUB_RUN_NUMBER', 'GITHUB_RUN_ATTEMPT']) { @@ -28,6 +28,6 @@ export function desktopCiVersion(environment, productVersion, now = new Date()) const resolved = validateDesktopBuildVersion(candidate, productVersion) return resolved } catch { - throw new Error(`Invalid build version for this checkout: use auto, ${productVersion}, or e.g. ${example}`) + throw new Error(`Invalid build version for this checkout: select version_mode=build-number, or use ${productVersion}, or e.g. ${example}`) } } diff --git a/apps/desktop/tests/desktop-channel-version.spec.ts b/apps/desktop/tests/desktop-channel-version.spec.ts index 9a5b77ee53..fde0dae8a6 100644 --- a/apps/desktop/tests/desktop-channel-version.spec.ts +++ b/apps/desktop/tests/desktop-channel-version.spec.ts @@ -9,8 +9,13 @@ it.each(['nightly', 'stable'] as const)('resolves every CI version mode independ 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: 'follow-dsh' }, version, now)).toBe(version) + for (const preset of ['test', 'production']) { + expect(desktopCiVersion({ ...env, DSH_PACKAGING_PRESET: preset, PACKAGE_VERSION_MODE: 'build-number' }, version, now)).toBe(generated) + } + for (const oldMode of ['auto', 'product']) { + expect(() => desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: oldMode }, version, now)).toThrow('Invalid version mode') + } 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) diff --git a/apps/desktop/tests/prepare-windows-ci.spec.ts b/apps/desktop/tests/prepare-windows-ci.spec.ts index f445682467..7f8cc1a8fc 100644 --- a/apps/desktop/tests/prepare-windows-ci.spec.ts +++ b/apps/desktop/tests/prepare-windows-ci.spec.ts @@ -14,7 +14,7 @@ describe('manual Windows CI version', () => { }) it.each(['', 'bad', '0.3.0'])('reports checkout-specific guidance for %s', (version) => { expect(() => windowsCiVersion({ PACKAGE_BUILD_VERSION: version }, '0.2.1-alpha.1', now)) - .toThrow(/auto, 0\.2\.1-alpha\.1, or e\.g\. 0\.2\.1-alpha\.1\.20261009\.1/u) + .toThrow(/version_mode=build-number, or use 0\.2\.1-alpha\.1, or e\.g\. 0\.2\.1-alpha\.1\.20261009\.1/u) }) it.each([{ GITHUB_RUN_NUMBER: '0' }, { GITHUB_RUN_ATTEMPT: '1\nINJECTED=value' }])('rejects invalid run identity %j', (override) => { expect(() => windowsCiVersion({ ...inputs, ...override }, '0.2.1', now)).toThrow(/positive integer/u) @@ -25,7 +25,7 @@ describe('manual Windows CI version', () => { it('resolves the selected version policy independently of the update channel', () => { expect(windowsCiVersion({ ...inputs, PACKAGE_VERSION_MODE: 'default', DSH_PACKAGING_PRESET: 'production' }, '1.2.3', now)).toBe('1.2.3') expect(windowsCiVersion({ ...inputs, PACKAGE_VERSION_MODE: 'default', DSH_PACKAGING_PRESET: 'test' }, '1.2.3', now)).toBe('1.2.3-test.20261009.42.2') - expect(windowsCiVersion({ PACKAGE_VERSION_MODE: 'product' }, '1.2.3-alpha.1', now)).toBe('1.2.3-alpha.1') + expect(windowsCiVersion({ PACKAGE_VERSION_MODE: 'follow-dsh' }, '1.2.3-alpha.1', now)).toBe('1.2.3-alpha.1') expect(() => windowsCiVersion({ PACKAGE_VERSION_MODE: 'manual', PACKAGE_BUILD_VERSION: 'auto' }, '1.2.3', now)).toThrow(/explicit version/u) expect(() => windowsCiVersion({ PACKAGE_VERSION_MODE: 'invalid' }, '1.2.3', now)).toThrow(/version mode/u) }) diff --git a/scripts/macos-package-workflow.spec.ts b/scripts/macos-package-workflow.spec.ts index 6125239177..8af161c720 100644 --- a/scripts/macos-package-workflow.spec.ts +++ b/scripts/macos-package-workflow.spec.ts @@ -33,6 +33,7 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silicon runner', () => { expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch']) + expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { version_mode: { default: 'default', options: ['default', 'follow-dsh', 'build-number', 'manual'] } } }) expect(workflow.jobs.package).not.toHaveProperty('name') expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { test_release_id: { type: 'string', default: '', required: false } } }) expect(workflow.on.workflow_dispatch).not.toHaveProperty('inputs.publish') diff --git a/scripts/windows-package-workflow.spec.ts b/scripts/windows-package-workflow.spec.ts index df5e0d7947..d5b2542673 100644 --- a/scripts/windows-package-workflow.spec.ts +++ b/scripts/windows-package-workflow.spec.ts @@ -62,6 +62,7 @@ it.skipIf(process.platform !== 'win32')('resets only the allocated checkout with it('routes manual packaging to the dedicated runner with explicit preset and signing selection', () => { expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch']) + expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { version_mode: { default: 'default', options: ['default', 'follow-dsh', 'build-number', 'manual'] } } }) expect(workflow.jobs.package).not.toHaveProperty('name') expect(job['runs-on']).toBe('dsh-win-package-trial') expect(JSON.stringify(job.env)).not.toContain('runner.') From e0e2c63a9cce64b66e7ddc441711d0fd621bc07b Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:57:19 +0800 Subject: [PATCH 06/10] fix(desktop): bridge workflow modes for older sources --- .github/workflows/macos-intel-package.yml | 3 ++- .github/workflows/macos-package.yml | 3 ++- .github/workflows/windows-package.yml | 3 ++- apps/desktop/packaging/README.i18n.yaml | 4 ++-- apps/desktop/packaging/README.md | 2 +- apps/desktop/packaging/README.zh.md | 2 +- apps/desktop/publishing/ci/README.i18n.yaml | 4 ++-- apps/desktop/publishing/ci/README.md | 2 +- apps/desktop/publishing/ci/README.zh.md | 2 +- apps/desktop/scripts/desktop-ci-version.mjs | 4 +++- apps/desktop/tests/desktop-channel-version.spec.ts | 5 ++--- scripts/macos-package-workflow.spec.ts | 1 + scripts/windows-package-workflow.spec.ts | 2 +- 13 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.github/workflows/macos-intel-package.yml b/.github/workflows/macos-intel-package.yml index 3f2d1d2cb2..5bf1b2f484 100644 --- a/.github/workflows/macos-intel-package.yml +++ b/.github/workflows/macos-intel-package.yml @@ -75,7 +75,8 @@ jobs: PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL: ${{ inputs.compression_level }} PACKAGE_SKIP_NOTARIZATION: ${{ inputs.skip_notarization }} PACKAGE_SOURCE_REF: ${{ inputs.source_ref }} - PACKAGE_VERSION_MODE: ${{ inputs.version_mode }} + # TODO: Remove this legacy source_ref mode mapping on 2027-01-04 with the parser aliases. + PACKAGE_VERSION_MODE: ${{ inputs.version_mode == 'follow-dsh' && 'product' || inputs.version_mode == 'build-number' && 'auto' || inputs.version_mode }} DSH_DESKTOP_UPDATE_CHANNEL: ${{ inputs.update_channel }} DSH_TELEMETRY_DISABLED: '1' DSH_PACKAGING_PRESET: ${{ inputs.preset }} diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 418a3b628d..7b9022bbfb 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -75,7 +75,8 @@ jobs: PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL: ${{ inputs.compression_level }} PACKAGE_SKIP_NOTARIZATION: ${{ inputs.skip_notarization }} PACKAGE_SOURCE_REF: ${{ inputs.source_ref }} - PACKAGE_VERSION_MODE: ${{ inputs.version_mode }} + # TODO: Remove this legacy source_ref mode mapping on 2027-01-04 with the parser aliases. + PACKAGE_VERSION_MODE: ${{ inputs.version_mode == 'follow-dsh' && 'product' || inputs.version_mode == 'build-number' && 'auto' || inputs.version_mode }} DSH_DESKTOP_UPDATE_CHANNEL: ${{ inputs.update_channel }} DSH_TELEMETRY_DISABLED: '1' DSH_PACKAGING_PRESET: ${{ inputs.preset }} diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index b59ea15c06..405ee6bd29 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -77,7 +77,8 @@ jobs: shell: pwsh env: PACKAGE_SOURCE_REF: ${{ inputs.source_ref }} - PACKAGE_VERSION_MODE: ${{ inputs.version_mode }} + # TODO: Remove this legacy source_ref mode mapping on 2027-01-04 with the parser aliases. + PACKAGE_VERSION_MODE: ${{ inputs.version_mode == 'follow-dsh' && 'product' || inputs.version_mode == 'build-number' && 'auto' || inputs.version_mode }} DSH_DESKTOP_UPDATE_CHANNEL: ${{ inputs.update_channel }} DSH_TELEMETRY_DISABLED: '1' DSH_PACKAGING_PRESET: ${{ inputs.preset }} diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index db12d8d6e2..4f1b79d367 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,8 +24,8 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 83706649764cd891 - zh: 05a7d0e853747cd6 + en: 1ddc3a9c9f2d3b5c + zh: 032c0f2c740fdfdd /desktop-packaging/macos-self-hosted-packaging: en: 3bd14c640893b192 zh: 7c236cbe46be4a47 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index c6e00d5418..638859feb2 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -111,7 +111,7 @@ For supervised runner startup and recovery, follow [Windows runner machine setup The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (product version for production, build numbering for test); `build-number` uses the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. -Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f version_mode=build-number`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. +Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. All Windows packaging modes, including test and unsigned, require the repository variable `DESKTOP_PUBLISH_RUNNERS`; configure the [original-machine route](../publishing/ci/README.md#machine-setup) before dispatch. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 36908cba5c..6a239d5188 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -111,7 +111,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s `Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用产品版本,test 追加构建编号);`build-number` 使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 -在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f preset=test -f signing=unsigned -f version_mode=build-number` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 +在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 所有 Windows 打包模式(含 test 和 unsigned)都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`;触发前配置[原机器路由](../publishing/ci/README.zh.md#machine-setup)。 diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index f0d00ca9c2..7ad46ef26a 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -24,8 +24,8 @@ en: 0f43f2e193e95043 zh: 1c1efb062c06f7b8 /desktop-ci-publication/package-and-publish: - en: dd0a0020cb4e911d - zh: 9521d4ba2e3ef31b + en: f0f7817a03b9feb4 + zh: e8756c06bc688162 /desktop-ci-publication/recovery-and-records: en: 266afba2adeeadef zh: 063114103585dae8 diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index 7654af2064..1261da7e5a 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -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 follow-dsh for production and build-number for test; follow-dsh preserves the selected checkout’s dsh version, build-number appends date/run/attempt in either environment, and manual requires `build_version`. Existing dispatch commands using `-f version_mode=auto` must use `-f version_mode=build-number`, and `-f version_mode=product` becomes `-f version_mode=follow-dsh`; the local CLI option `--build-version auto` is unchanged. 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. +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 follow-dsh for production and build-number for test; follow-dsh preserves the selected checkout’s dsh version, build-number appends date/run/attempt in either environment, and manual requires `build_version`. Existing dispatch commands using `-f version_mode=auto` must use `-f version_mode=build-number`, and `-f version_mode=product` becomes `-f version_mode=follow-dsh`; the local CLI option `--build-version auto` is unchanged. Until 2027-01-04, workflows pass legacy mode names to support older selected sources, and current parsers accept both names. 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. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index fb5b46590e..3133c00801 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -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 选择 follow-dsh,对 test 选择 build-number;follow-dsh 跟随所选源码的 dsh 包版本号,build-number 在任一环境均追加日期/run/attempt,manual 要求填写 `build_version`。已有派发命令中的 `-f version_mode=auto` 须改为 `-f version_mode=build-number`,`-f version_mode=product` 改为 `-f version_mode=follow-dsh`;本地 CLI 的 `--build-version auto` 不变。操作人员选择的渠道接受任意受支持的构建版本,包括 alpha 和 RC 版本;渠道不改变版本优先级,也不允许客户端降级。可选 `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 选择 follow-dsh,对 test 选择 build-number;follow-dsh 跟随所选源码的 dsh 包版本号,build-number 在任一环境均追加日期/run/attempt,manual 要求填写 `build_version`。已有派发命令中的 `-f version_mode=auto` 须改为 `-f version_mode=build-number`,`-f version_mode=product` 改为 `-f version_mode=follow-dsh`;本地 CLI 的 `--build-version auto` 不变。截至 2027-01-04,工作流向所选源码传递旧模式名以兼容旧源码,当前解析器同时接受新旧名称。操作人员选择的渠道接受任意受支持的构建版本,包括 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` 不影响发布资格;本机文件缺失或变化会停止发布。 diff --git a/apps/desktop/scripts/desktop-ci-version.mjs b/apps/desktop/scripts/desktop-ci-version.mjs index bc57adfb5e..da3e71d43a 100644 --- a/apps/desktop/scripts/desktop-ci-version.mjs +++ b/apps/desktop/scripts/desktop-ci-version.mjs @@ -9,7 +9,9 @@ import { desktopBuildVersionPrefix, validateDesktopBuildVersion } from './deskto * @returns {string} Validated build version. */ export function desktopCiVersion(environment, productVersion, now = new Date()) { - const mode = environment.PACKAGE_VERSION_MODE + const input = environment.PACKAGE_VERSION_MODE + // TODO: Remove legacy mode aliases on 2027-01-04 with the workflow source_ref mapping. + const mode = input === 'product' ? 'follow-dsh' : input === 'auto' ? 'build-number' : input const selected = mode === 'default' ? (environment.DSH_PACKAGING_PRESET === 'production' ? 'follow-dsh' : 'build-number') : mode if (selected !== undefined && !['follow-dsh', 'build-number', 'manual'].includes(selected)) throw new Error('Invalid version mode') const requested = selected === 'follow-dsh' ? productVersion : selected === 'build-number' ? 'auto' : environment.PACKAGE_BUILD_VERSION?.trim() diff --git a/apps/desktop/tests/desktop-channel-version.spec.ts b/apps/desktop/tests/desktop-channel-version.spec.ts index fde0dae8a6..99293c650c 100644 --- a/apps/desktop/tests/desktop-channel-version.spec.ts +++ b/apps/desktop/tests/desktop-channel-version.spec.ts @@ -13,9 +13,8 @@ it.each(['nightly', 'stable'] as const)('resolves every CI version mode independ for (const preset of ['test', 'production']) { expect(desktopCiVersion({ ...env, DSH_PACKAGING_PRESET: preset, PACKAGE_VERSION_MODE: 'build-number' }, version, now)).toBe(generated) } - for (const oldMode of ['auto', 'product']) { - expect(() => desktopCiVersion({ ...env, PACKAGE_VERSION_MODE: oldMode }, version, now)).toThrow('Invalid version mode') - } + 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) diff --git a/scripts/macos-package-workflow.spec.ts b/scripts/macos-package-workflow.spec.ts index 8af161c720..4f707cb99c 100644 --- a/scripts/macos-package-workflow.spec.ts +++ b/scripts/macos-package-workflow.spec.ts @@ -33,6 +33,7 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silicon runner', () => { expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch']) + expect(workflow.jobs.package!.env.PACKAGE_VERSION_MODE).toBe("${{ inputs.version_mode == 'follow-dsh' && 'product' || inputs.version_mode == 'build-number' && 'auto' || inputs.version_mode }}") expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { version_mode: { default: 'default', options: ['default', 'follow-dsh', 'build-number', 'manual'] } } }) expect(workflow.jobs.package).not.toHaveProperty('name') expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { test_release_id: { type: 'string', default: '', required: false } } }) diff --git a/scripts/windows-package-workflow.spec.ts b/scripts/windows-package-workflow.spec.ts index d5b2542673..18720ad948 100644 --- a/scripts/windows-package-workflow.spec.ts +++ b/scripts/windows-package-workflow.spec.ts @@ -80,7 +80,7 @@ it('routes manual packaging to the dedicated runner with explicit preset and sig expect(workflow.on.workflow_dispatch!.inputs.version_mode!.default).toBe('default') expect(workflow.on.workflow_dispatch!.inputs.source_ref!.default).toBe('master') expect(workflow.on.workflow_dispatch!.inputs.update_channel!.default).toBe('nightly') - expect(job.env.PACKAGE_VERSION_MODE).toBe('${{ inputs.version_mode }}') + expect(job.env.PACKAGE_VERSION_MODE).toBe("${{ inputs.version_mode == 'follow-dsh' && 'product' || inputs.version_mode == 'build-number' && 'auto' || inputs.version_mode }}") expect(job.env.PACKAGE_SOURCE_REF).toBe('${{ inputs.source_ref }}') expect(job.env.DSH_DESKTOP_UPDATE_CHANNEL).toBe('${{ inputs.update_channel }}') expect(job.env.DSH_PACKAGING_PRESET).toBe('${{ inputs.preset }}') From 812f846489da031de8eace1e5306f220c6d7141f Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 14:19:21 +0800 Subject: [PATCH 07/10] docs(desktop): clarify version mode compatibility lifetime --- apps/desktop/publishing/ci/README.i18n.yaml | 4 ++-- apps/desktop/publishing/ci/README.md | 2 +- apps/desktop/publishing/ci/README.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index 7ad46ef26a..b7bb9a5f58 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -24,8 +24,8 @@ en: 0f43f2e193e95043 zh: 1c1efb062c06f7b8 /desktop-ci-publication/package-and-publish: - en: f0f7817a03b9feb4 - zh: e8756c06bc688162 + en: f420ecc16d8b82e2 + zh: 9bd6ce8308daba55 /desktop-ci-publication/recovery-and-records: en: 266afba2adeeadef zh: 063114103585dae8 diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index 1261da7e5a..3f81552c57 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -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 follow-dsh for production and build-number for test; follow-dsh preserves the selected checkout’s dsh version, build-number appends date/run/attempt in either environment, and manual requires `build_version`. Existing dispatch commands using `-f version_mode=auto` must use `-f version_mode=build-number`, and `-f version_mode=product` becomes `-f version_mode=follow-dsh`; the local CLI option `--build-version auto` is unchanged. Until 2027-01-04, workflows pass legacy mode names to support older selected sources, and current parsers accept both names. 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. +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. Sources without the CI adapters 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 follow-dsh for production and build-number for test; follow-dsh preserves the selected checkout’s dsh version, build-number appends date/run/attempt in either environment, and manual requires `build_version`. Existing dispatch commands using `-f version_mode=auto` must use `-f version_mode=build-number`, and `-f version_mode=product` becomes `-f version_mode=follow-dsh`; the local CLI option `--build-version auto` is unchanged. Workflows temporarily pass legacy mode names to sources that contain the CI adapters but predate the mode rename; current parsers accept both names. Removal is planned for 2027-01-04, not automatic: remove workflow mappings and parser aliases together, then require selected sources to support the new mode names. 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. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 3133c00801..d8ca70e282 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -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 选择 follow-dsh,对 test 选择 build-number;follow-dsh 跟随所选源码的 dsh 包版本号,build-number 在任一环境均追加日期/run/attempt,manual 要求填写 `build_version`。已有派发命令中的 `-f version_mode=auto` 须改为 `-f version_mode=build-number`,`-f version_mode=product` 改为 `-f version_mode=follow-dsh`;本地 CLI 的 `--build-version auto` 不变。截至 2027-01-04,工作流向所选源码传递旧模式名以兼容旧源码,当前解析器同时接受新旧名称。操作人员选择的渠道接受任意受支持的构建版本,包括 alpha 和 RC 版本;渠道不改变版本优先级,也不允许客户端降级。可选 `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 选择 follow-dsh,对 test 选择 build-number;follow-dsh 跟随所选源码的 dsh 包版本号,build-number 在任一环境均追加日期/run/attempt,manual 要求填写 `build_version`。已有派发命令中的 `-f version_mode=auto` 须改为 `-f version_mode=build-number`,`-f version_mode=product` 改为 `-f version_mode=follow-dsh`;本地 CLI 的 `--build-version auto` 不变。工作流暂时传递旧模式名,以兼容已含 CI 适配脚本但早于模式改名的源码;当前解析器同时接受新旧名称。计划于 2027-01-04 同时删除 workflow 映射和解析器别名,并要求所选源码支持新模式名;该日期不会触发自动失效。操作人员选择的渠道接受任意受支持的构建版本,包括 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` 不影响发布资格;本机文件缺失或变化会停止发布。 From 69db579d7aea734d9df00980e36ac87a87d90cac Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 15:04:24 +0800 Subject: [PATCH 08/10] docs(desktop): distinguish version mode and manual version --- apps/desktop/packaging/README.i18n.yaml | 4 ++-- apps/desktop/packaging/README.md | 2 +- apps/desktop/packaging/README.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 5c9605f08b..b290abb47e 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,8 +24,8 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 78c571cbf77e80f8 - zh: c06d905876452921 + en: bb097c6ffa1cf8ac + zh: 0c960be105e44a93 /desktop-packaging/macos-self-hosted-packaging: en: 08619f0e26782636 zh: 7c236cbe46be4a47 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 91e138eee0..4a289bbb01 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -109,7 +109,7 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t For supervised runner startup and recovery, follow [Windows runner machine setup](../windows-signing/README.md#machine-setup). -The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (product version for production, build numbering for test); `build-number` uses the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. +The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (`follow-dsh` for production, `build-number` for test); `build-number` uses the CI identifier in [Release versions](#release-versions). Set `build_version` only when selecting `manual`; its value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index db21505bb1..624ce054e8 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -109,7 +109,7 @@ macOS 正式发布和本地未签名 DMG 都通过 [electron-builder 配置](../ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-signing/README.zh.md#machine-setup)。 -`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用产品版本,test 追加构建编号);`build-number` 使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 +`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用 `follow-dsh`,test 使用 `build-number`);`build-number` 使用[发布版本](#release-versions)中的 CI 标识。仅在选择 `manual` 时填写 `build_version`,其值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 From 08afd883d7d792d8a4c98fa1ec6c6148d1478319 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 15:14:58 +0800 Subject: [PATCH 09/10] docs(desktop): scope build numbering to its version mode --- apps/desktop/packaging/README.i18n.yaml | 8 ++++---- apps/desktop/packaging/README.md | 4 ++-- apps/desktop/packaging/README.zh.md | 4 ++-- apps/desktop/scripts/desktop-build-version.mjs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index b290abb47e..7b5d2eef36 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -18,14 +18,14 @@ en: 0b35c7471c09b27a zh: cc4a382b99fc3210 /desktop-packaging/release-versions: - en: 2c42e40141c24dbd - zh: f057e32326681051 + en: bdee4adb7916e6cc + zh: 2880c3594ebab7ab /desktop-packaging/configuration-and-local-builds: en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: bb097c6ffa1cf8ac - zh: 0c960be105e44a93 + en: b39449c2f945cab9 + zh: a742ba9efa282be8 /desktop-packaging/macos-self-hosted-packaging: en: 08619f0e26782636 zh: 7c236cbe46be4a47 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 4a289bbb01..d994f5e8f4 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -51,7 +51,7 @@ Record the current dsh version as the base. A production Desktop release uses th Use the actual creation date in Asia/Shanghai. For each base and date, start the index at 1 and increment after checking retained release records and published objects; never reuse a published version. Test distribution does not publish the corresponding unsuffixed base. -Manual Desktop CI uses a separate unpublished build identifier: `build-number` appends the UTC date, workflow run number and attempt (`.YYYYMMDD.run.attempt`, with `-test` for a stable base). For example, `0.2.1-alpha.1.20261008.42.2`. These numbers are not reserved release sequence numbers. +Version mode `build-number` appends the UTC date, workflow run number and attempt (`.YYYYMMDD.run.attempt`, with `-test` for a stable base). For example, `0.2.1-alpha.1.20261008.42.2`. These numbers are not reserved release sequence numbers. Pass the confirmed version to the packaging command as `--build-version`, which reaches the artifact names, the update feed, and the upload validation as one value. The manifests keep the product version, so a test build no longer rewrites the release family and leaves nothing to revert: @@ -109,7 +109,7 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t For supervised runner startup and recovery, follow [Windows runner machine setup](../windows-signing/README.md#machine-setup). -The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (`follow-dsh` for production, `build-number` for test); `build-number` uses the CI identifier in [Release versions](#release-versions). Set `build_version` only when selecting `manual`; its value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. +The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `version_mode` defaults to `default` (`follow-dsh` for production, `build-number` for test); `build-number` uses the CI identifier in [Release versions](#release-versions). `build_version` is required for `manual` and ignored by the other modes; its value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged. Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 624ce054e8..a79b9753ca 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -51,7 +51,7 @@ description: "在本地或 Windows/macOS Actions 中生成 Desktop 安装包 日期使用实际创建时的 Asia/Shanghai 日期。每个基础版本、每天的序号从 1 开始,检查保留的发布记录与已发布对象后递增;绝不复用已发布版本。test 分发不发布对应的无后缀基础版本。 -Desktop 手动 CI 使用独立的未发布构建标识:`build-number` 追加 UTC 日期、工作流运行编号和尝试次数(`.YYYYMMDD.run.attempt`,稳定基础版本使用 `-test`)。例如 `0.2.1-alpha.1.20261008.42.2`。这些编号不是预留的发布序号。 +版本模式 `build-number` 追加 UTC 日期、工作流运行编号和尝试次数(`.YYYYMMDD.run.attempt`,稳定基础版本使用 `-test`)。例如 `0.2.1-alpha.1.20261008.42.2`。这些编号不是预留的发布序号。 把确认后的版本通过 `--build-version` 传给打包命令,该值同时决定产物文件名、更新 feed 与上传校验。清单保留产品版本,因此 test 打包不再改写发布家族,也不留下需要还原的改动: @@ -109,7 +109,7 @@ macOS 正式发布和本地未签名 DMG 都通过 [electron-builder 配置](../ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-signing/README.zh.md#machine-setup)。 -`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用 `follow-dsh`,test 使用 `build-number`);`build-number` 使用[发布版本](#release-versions)中的 CI 标识。仅在选择 `manual` 时填写 `build_version`,其值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 +`Package Desktop · Windows · x64` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`version_mode` 默认为 `default`(production 使用 `follow-dsh`,test 使用 `build-number`);`build-number` 使用[发布版本](#release-versions)中的 CI 标识。`manual` 模式必须填写 `build_version`,其他模式忽略该字段;其值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。 在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref -f source_ref= -f preset=test -f signing=unsigned -f version_mode=build-number` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。 diff --git a/apps/desktop/scripts/desktop-build-version.mjs b/apps/desktop/scripts/desktop-build-version.mjs index bddf4ca492..5a75253f5c 100644 --- a/apps/desktop/scripts/desktop-build-version.mjs +++ b/apps/desktop/scripts/desktop-build-version.mjs @@ -2,7 +2,7 @@ * Resolve the version one build publishes, which is not always the version the * repository declares. * - * Manual Desktop CI uses UTC date, workflow run number and attempt as + * Desktop CI version mode `build-number` uses UTC date, workflow run number and attempt as * `.YYYYMMDD.run.attempt`; these identify unpublished builds, not release sequences. * * A production release publishes the version in the manifests, aligned with the From c649fdae69b0a6321c6cd0141b87d3e80f53bb90 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 15:44:33 +0800 Subject: [PATCH 10/10] docs(desktop): preserve frozen stable channel migration guide --- .../v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml | 4 ++-- .../v0.2.2-alpha.2/desktop-stable-channel/guide.md | 2 +- .../v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml index 49ca2a4270..ce81afec05 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.i18n.yaml @@ -12,5 +12,5 @@ en: bca4377ec9032e3b zh: 6da9f3be04f1cd39 /desktop-stable-channel-name/migration: - en: a7f4cf090fde3729 - zh: 3dd080660f49e7c6 + en: 8442b24f420f6be2 + zh: b2517178eccbf8d5 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md index 2514bf3020..9261aa2960 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.md @@ -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. Select the channel independently of the version suffix; stable also accepts alpha and RC versions. +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. 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. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md index 9eb899526a..441693811b 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/desktop-stable-channel/guide.zh.md @@ -13,6 +13,6 @@ description: "桌面正式更新渠道由 latest 更名为 stable。" ## 迁移 -1. 在打包工作流中选择 `stable`,或在构建正式渠道包时设置 `DSH_DESKTOP_UPDATE_CHANNEL=stable`。渠道选择独立于版本后缀;stable 也接受 alpha 和 RC 版本。 +1. 在打包工作流中选择 `stable`,或在构建正式渠道包时设置 `DSH_DESKTOP_UPDATE_CHANNEL=stable`。使用不含预发布后缀的稳定版本号。 2. 如果本地桌面配置文件包含 `updates.channel: "latest"`,将该值改为 `"stable"` 并重启桌面应用。省略该字段时使用安装包默认值。 3. 使用更新后的工具发布:在各架构的清单目录内,Windows 写入 `stable.yml`,macOS 写入 `stable-mac.yml`;正式渠道二进制位于 `stable/` 下。在正式发布前,通过测试部署核对选定渠道和请求的清单。仍请求 `latest` 清单的旧包不会自动迁移。