diff --git a/.github/workflows/desktop-publish.yml b/.github/workflows/desktop-publish.yml index ff756d1b95..32efedf65c 100644 --- a/.github/workflows/desktop-publish.yml +++ b/.github/workflows/desktop-publish.yml @@ -9,6 +9,11 @@ on: description: '打包运行 ID:填写成功的 Package Desktop run ID;发布环境和 release ID 沿用原包' type: string required: true + staging_percentage: + description: '更新灰度比例:0–100 整数,默认 100;留空沿用包内值,作用于原包所属渠道' + type: string + required: false + default: '100' upload_website: description: '同时上传官网安装包并刷新缓存(仅 production);取消后仅执行版本与自动更新发布' type: boolean @@ -66,6 +71,7 @@ jobs: website-destination: ${{ steps.check.outputs.website-destination }} label: ${{ steps.check.outputs.label }} env: + DESKTOP_STAGING_PERCENTAGE: ${{ inputs.staging_percentage }} DESKTOP_PACKAGE_SELECTION: ${{ needs.route.outputs.selection }} DESKTOP_UPLOAD_WEBSITE: ${{ inputs.upload_website }} DOWNLOAD_PROD_EO_ZONE_ID: ${{ vars.DOWNLOAD_PROD_EO_ZONE_ID }} @@ -111,6 +117,7 @@ jobs: cancel-in-progress: false timeout-minutes: 90 env: + DESKTOP_STAGING_PERCENTAGE: ${{ inputs.staging_percentage }} DESKTOP_APPROVED_PACKAGE: ${{ needs.prepare.outputs.approval }} DESKTOP_PUBLISH_ROUTE: ${{ needs.prepare.outputs.route }} DSH_TELEMETRY_DISABLED: '1' diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index 3a0b48db93..328bdb9397 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: e62e9d93a50e6489 + zh: 2491c37e40f4c496 /desktop-ci-publication/recovery-and-records: en: d6eb9cbac5b819a0 zh: 0de1ec24055d1007 diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index d1e5f18fa1..143814ea79 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -68,6 +68,8 @@ Environment approval gates this workflow, not arbitrary code running under the s ## Package and publish +The publication form accepts an optional `staging_percentage` integer from 0 to 100. It applies to the retained package’s channel (nightly or stable), appears in the approval summary, and is included in the published YAML and audit record without modifying retained artifacts. Leave it blank for the packaged default (normally full rollout). Completed publications cannot change this percentage through a retry; same-version feed conflicts remain errors. + 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. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 0ff96d3ba3..e4e290f581 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -68,6 +68,8 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任 ## 打包与发布 +发布表单支持可选的 `staging_percentage`,取值为 0 到 100 的整数。该值作用于保留包所属的 nightly 或 stable 渠道,显示在审批摘要中,并写入发布的 YAML 和审计记录,不修改保留产物。留空沿用包内默认值(通常为全量)。已完成的发布不能通过重试修改比例;同版本清单内容冲突仍会报错。 + 发布到 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)。 diff --git a/apps/desktop/scripts/desktop-publication-summary.ts b/apps/desktop/scripts/desktop-publication-summary.ts index 49d438aaf2..a9e86575fd 100644 --- a/apps/desktop/scripts/desktop-publication-summary.ts +++ b/apps/desktop/scripts/desktop-publication-summary.ts @@ -1,4 +1,6 @@ /** Render the verified publication plan for GitHub's native deployment reviewers. */ +import { load } from 'js-yaml' +import { z } from 'zod' import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { desktopWebsiteInstallerUrl } from './desktop-auto-update-environment.mjs' @@ -48,6 +50,10 @@ export async function desktopPublicationSummary( row('发布环境', code(plan.environment)), row('版本号', code(plan.version)), row('升级通道', code(source.channel)), + ...feeds.map((feed) => { + const metadata = z.object({ stagingPercentage: z.number().optional() }).parse(load(feed.contents!)) + return row('更新灰度比例', metadata.stagingPercentage === undefined ? '全量(未设置)' : `${metadata.stagingPercentage}%`) + }), row('平台', code(plan.target)), ...(plan.environment === 'test' ? [row('测试 release ID', `${code(source.testReleaseId!)}
测试下载路径的随机前缀,用于降低地址被猜到的可能性;连续升级测试需使用相同值。`)] : []), row('下载地址预览', `${installers.map(file => link(url(file.key))).join('
')}
发布后的目标地址,审批前不保证可下载。`), diff --git a/apps/desktop/scripts/desktop-publish-record.ts b/apps/desktop/scripts/desktop-publish-record.ts index 77ac3ca698..7ef28cac0c 100644 --- a/apps/desktop/scripts/desktop-publish-record.ts +++ b/apps/desktop/scripts/desktop-publish-record.ts @@ -40,7 +40,7 @@ export interface DesktopPublicationRecord { */ export async function createDesktopPublicationRecord( root: string, - identity: { repository: string; runId: string; attempt: string; actor: string; destination: string; operation: 'publish' | 'rollback'; reason: string }, + identity: { repository: string; runId: string; attempt: string; actor: string; destination: string; operation: 'publish' | 'rollback'; reason: string; stagingPercentage?: number | undefined }, ): Promise { await mkdir(root, { recursive: true, mode: 0o700 }) const staging = await mkdtemp(join(root, '.initializing-')) @@ -78,6 +78,7 @@ export async function wasDesktopAttemptPublished(root: string, expected: { runId: string attempt: string destination: string + stagingPercentage?: number | undefined }): Promise { let entries try { entries = await readdir(root, { withFileTypes: true }) } catch (error) { @@ -93,7 +94,7 @@ export async function wasDesktopAttemptPublished(root: string, expected: { throw error } const parsed = z.object({ operation: z.string(), repository: z.string(), - runId: z.string(), attempt: z.string(), destination: z.string() }).parse(operation) + runId: z.string(), attempt: z.string(), destination: z.string(), stagingPercentage: z.number().optional() }).parse(operation) if (parsed.operation !== 'publish' || parsed.repository !== expected.repository || parsed.runId !== expected.runId || parsed.attempt !== expected.attempt || parsed.destination !== expected.destination) continue let events @@ -103,7 +104,12 @@ export async function wasDesktopAttemptPublished(root: string, expected: { } // Only newline-terminated entries were completely appended before an interruption. for (const line of events.split('\n').slice(0, -1)) { - if (z.object({ type: z.string() }).parse(JSON.parse(line)).type === 'feeds-complete') return true + if (z.object({ type: z.string() }).parse(JSON.parse(line)).type === 'feeds-complete') { + if (parsed.stagingPercentage !== expected.stagingPercentage) { + throw new Error('desktop publish: staging percentage differs from the completed publication') + } + return true + } } } return false diff --git a/apps/desktop/scripts/desktop-upload-plan.ts b/apps/desktop/scripts/desktop-upload-plan.ts index e6a8d4c7f0..6a9cfd1183 100644 --- a/apps/desktop/scripts/desktop-upload-plan.ts +++ b/apps/desktop/scripts/desktop-upload-plan.ts @@ -61,6 +61,8 @@ export interface DesktopUploadPlan { export interface DesktopUploadPlanOptions { /** Publish only the installer at its fixed download URL, replacing the previous object. */ readonly latest?: boolean + /** Percentage of installations eligible for this publication; absent preserves packaged metadata. */ + readonly stagingPercentage?: number | undefined readonly environment?: NodeJS.ProcessEnv readonly repositoryRoot?: string readonly appRoot?: string @@ -272,6 +274,7 @@ export async function createDesktopUploadPlan( const payloadUrl = `${update.origin}/${binaryPrefix}/${updaterInfo.filename}` const published = { ...metadata, + ...(options.stagingPercentage === undefined ? {} : { stagingPercentage: options.stagingPercentage }), files: [{ ...object(metadata.files[0], `${metadataFilename}.files[0]`), url: payloadUrl }], ...(metadata.path === undefined ? {} : { path: payloadUrl }), } diff --git a/apps/desktop/scripts/publish-desktop-ci.ts b/apps/desktop/scripts/publish-desktop-ci.ts index f881e0d48a..e3d14abbff 100644 --- a/apps/desktop/scripts/publish-desktop-ci.ts +++ b/apps/desktop/scripts/publish-desktop-ci.ts @@ -28,6 +28,11 @@ const routeSchema = z.object({ runId: z.string(), attempt: z.string(), commit: z * @returns Validated local publication context; no network operation is performed. */ export async function prepareDesktopPublication(env: NodeJS.ProcessEnv, verifySignatures = verifyRetainedDesktopSignatures) { + const percentage = env.DESKTOP_STAGING_PERCENTAGE?.trim() || '' + if (percentage !== '' && (!/^\d+$/u.test(percentage) || Number(percentage) > 100)) { + throw new Error('desktop publish: staging percentage must be an integer from 0 to 100') + } + const stagingPercentage = percentage === '' ? undefined : Number(percentage) const route = routeSchema.parse(JSON.parse(env.DESKTOP_PUBLISH_ROUTE || '{}')) if (route.runnerName !== env.RUNNER_NAME) throw new Error('desktop publish: task was dispatched to a different machine') const root = desktopReleasesRoot(env) @@ -48,12 +53,13 @@ export async function prepareDesktopPublication(env: NodeJS.ProcessEnv, verifySi DOWNLOAD_TEST_RELEASE_ID: source.testReleaseId ?? '', DOWNLOAD_TEST_COS_BUCKET: config.bucket } : { DSH_DESKTOP_AUTO_UPDATE_ENV: 'production', DOWNLOAD_PROD_COS_BUCKET: config.bucket } const plan = await createDesktopUploadPlan(source.target, { artifactsRoot: directory, + stagingPercentage, packagedProductVersion: source.productVersion, environment: { ...environment, DSH_DESKTOP_UPDATE_CHANNEL: source.channel } }) if (plan.version !== source.version || plan.commit !== source.commit || plan.dirty !== false) throw new Error('desktop publish: completion record differs from retained source or has uncommitted changes') await verifySignatures(directory, manifest, config) const approval = createHash('sha256').update(JSON.stringify({ manifest, plan })).digest('hex') if (env.DESKTOP_APPROVED_PACKAGE && env.DESKTOP_APPROVED_PACKAGE !== approval) throw new Error('desktop publish: package or destination changed after approval') - return { route, manifest, config, plan, root, approval } + return { route, manifest, config, plan, root, approval, stagingPercentage } } if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) { @@ -62,7 +68,7 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi const mode = process.argv[2] if (process.argv.length !== 3 || (mode !== '--check' && mode !== '--publish')) throw new Error('Usage: publish-desktop-ci.ts --check|--publish') const env = process.env - const { route, manifest, config, plan, root, approval } = await prepareDesktopPublication(env) + const { route, manifest, config, plan, root, approval, stagingPercentage } = await prepareDesktopPublication(env) if (mode === '--publish' && !env.DESKTOP_APPROVED_PACKAGE) throw new Error('desktop publish: approved package digest is required') if (env.DESKTOP_UPLOAD_WEBSITE === 'true' && route.preset === 'production' && !/^zone-[a-z0-9]+$/u.test(env.DOWNLOAD_PROD_EO_ZONE_ID || '')) { @@ -84,11 +90,11 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi phase = 'channel lock and publication journal acquisition' await withDesktopChannelLock(join(dirname(root), 'publish-locks'), destination, async () => { const published = await wasDesktopAttemptPublished(recordsRoot, - { repository: manifest.source.repository, runId: route.runId, attempt: route.attempt, destination }) + { repository: manifest.source.repository, runId: route.runId, attempt: route.attempt, destination, stagingPercentage }) const journal = await createDesktopPublicationRecord(recordsRoot, { repository: manifest.source.repository, runId: route.runId, attempt: route.attempt, actor: env.GITHUB_TRIGGERING_ACTOR || env.GITHUB_ACTOR || 'unknown', - destination, operation: 'publish', reason: 'Actions publication', + destination, stagingPercentage, operation: 'publish', reason: 'Actions publication', }) if (env.GITHUB_OUTPUT) await appendFile(env.GITHUB_OUTPUT, `record=${journal.directory}\n`) let status: 'complete' | 'failed' | 'published-tag-pending' = 'failed' diff --git a/apps/desktop/tests/desktop-publication-preflight.spec.ts b/apps/desktop/tests/desktop-publication-preflight.spec.ts index 04402925a5..dd6e2e7f41 100644 --- a/apps/desktop/tests/desktop-publication-preflight.spec.ts +++ b/apps/desktop/tests/desktop-publication-preflight.spec.ts @@ -1,3 +1,4 @@ +import { load } from 'js-yaml' import { createHash } from 'node:crypto' import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' @@ -61,10 +62,25 @@ it.each(['mac-arm64', 'win-x64'] as const)('preflights a complete retained %s bu expect(summary).not.toContain('example.com//') expect(summary).not.toContain(root) expect(summary).not.toContain('absent-credentials') + for (const percentage of ['0', '10', '100']) { + const staged = await prepareDesktopPublication({ ...env, DESKTOP_STAGING_PERCENTAGE: percentage }, signature) + const stagedFeed = staged.plan.artifacts.find(file => file.channelMetadata)! + expect(load(stagedFeed.contents!)).toMatchObject({ stagingPercentage: Number(percentage) }) + expect(staged.approval).not.toBe(prepared.approval) + expect(await desktopPublicationSummary(staged.manifest.source, staged.plan, staged.approval, false)).toContain(`${percentage}%`) + await expect(prepareDesktopPublication({ ...env, DESKTOP_STAGING_PERCENTAGE: percentage, + DESKTOP_APPROVED_PACKAGE: prepared.approval }, signature)).rejects.toThrow(/changed after approval/u) + } + for (const percentage of ['-1', '101', '1.5', 'NaN', 'Infinity', '1e1']) { + await expect(prepareDesktopPublication({ ...env, DESKTOP_STAGING_PERCENTAGE: percentage }, signature)) + .rejects.toThrow(/integer from 0 to 100/u) + } + const blank = await prepareDesktopPublication({ ...env, DESKTOP_STAGING_PERCENTAGE: ' ' }, signature) + expect(blank.approval).toBe(prepared.approval) await expect(prepareDesktopPublication({ ...env, DESKTOP_APPROVED_PACKAGE: 'wrong' }, signature)).rejects.toThrow(/changed after approval/u) await expect(prepareDesktopPublication({ ...env, RUNNER_NAME: 'other-machine' }, signature)).rejects.toThrow(/different machine/u) await writeFile(join(retained, `${base}.${extension}`), 'changed') await expect(prepareDesktopPublication(env, signature)).rejects.toThrow(/changed file/u) - expect(signatureChecks).toBe(2) + } finally { await rm(root, { recursive: true, force: true }) } }) diff --git a/apps/desktop/tests/desktop-publish-record.spec.ts b/apps/desktop/tests/desktop-publish-record.spec.ts index 87646edbe1..287af3e4a2 100644 --- a/apps/desktop/tests/desktop-publish-record.spec.ts +++ b/apps/desktop/tests/desktop-publish-record.spec.ts @@ -52,10 +52,10 @@ it('persists intent, before/after bytes and terminal state outside the checkout' } finally { await rm(root, { recursive: true, force: true }) } }) -it('recognizes feed completion even when the later tag failed', async () => { +it.each([undefined, 10])('recognizes feed completion with percentage %s even when the later tag failed', async (stagingPercentage) => { const root = await mkdtemp(join(tmpdir(), 'desktop-recovery-')) try { - const identity = { repository: 'owner/repo', runId: '12', attempt: '2', destination: 'bucket/mac' } + const identity = { repository: 'owner/repo', runId: '12', attempt: '2', destination: 'bucket/mac', stagingPercentage } const journal = await createDesktopPublicationRecord(root, { ...identity, actor: 'operator', operation: 'publish', reason: 'publish' }) expect(await wasDesktopAttemptPublished(root, identity)).toBe(false) await journal.record({ type: 'feeds-complete' }) @@ -63,6 +63,8 @@ it('recognizes feed completion even when the later tag failed', async () => { await journal.finish('published-tag-pending') expect(await wasDesktopAttemptPublished(root, identity)).toBe(true) expect(await wasDesktopAttemptPublished(root, { ...identity, attempt: '1' })).toBe(false) + await expect(wasDesktopAttemptPublished(root, { ...identity, stagingPercentage: 20 })) + .rejects.toThrow(/differs from the completed publication/u) } finally { await rm(root, { recursive: true, force: true }) } }) diff --git a/apps/desktop/tests/desktop-upload-plan.spec.ts b/apps/desktop/tests/desktop-upload-plan.spec.ts index 18f3fa18fa..b6f81d69df 100644 --- a/apps/desktop/tests/desktop-upload-plan.spec.ts +++ b/apps/desktop/tests/desktop-upload-plan.spec.ts @@ -178,11 +178,14 @@ describe('desktop upload plan', () => { }) it.each(['test', 'production'] as const)('isolates stable-channel packages from nightly in %s', async (environment) => { - const nightly = await createDesktopUploadPlan('win-x64', await fixture('win-x64', '1.2.3', environment, 'nightly')) - const stable = await createDesktopUploadPlan('win-x64', await fixture('win-x64', '1.2.3', environment, 'stable')) + const nightly = await createDesktopUploadPlan('win-x64', { ...await fixture('win-x64', '1.2.3', environment, 'nightly'), stagingPercentage: 10 }) + const stable = await createDesktopUploadPlan('win-x64', { ...await fixture('win-x64', '1.2.3', environment, 'stable'), stagingPercentage: 10 }) 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) + for (const plan of [nightly, stable]) { + expect(plan.artifacts.find(file => file.channelMetadata)!.contents).toContain('stagingPercentage: 10') + } 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) }) diff --git a/apps/desktop/tests/expected/publication-mac-arm64.md b/apps/desktop/tests/expected/publication-mac-arm64.md index 7b376d9d65..f975154887 100644 --- a/apps/desktop/tests/expected/publication-mac-arm64.md +++ b/apps/desktop/tests/expected/publication-mac-arm64.md @@ -8,6 +8,7 @@ | 发布环境 | test | | 版本号 | 1.0.0 | | 升级通道 | nightly | +| 更新灰度比例 | 全量(未设置) | | 平台 | mac-arm64 | | 测试 release ID | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
测试下载路径的随机前缀,用于降低地址被猜到的可能性;连续升级测试需使用相同值。 | | 下载地址预览 | https://example.com/dsh-desk/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bin/mac-arm64/deepseek-harness-1.0.0-mac-arm64.dmg
发布后的目标地址,审批前不保证可下载。 | diff --git a/apps/desktop/tests/expected/publication-production-website.md b/apps/desktop/tests/expected/publication-production-website.md index 464650c653..4ba276895c 100644 --- a/apps/desktop/tests/expected/publication-production-website.md +++ b/apps/desktop/tests/expected/publication-production-website.md @@ -8,6 +8,7 @@ | 发布环境 | production | | 版本号 | 1.0.0 | | 升级通道 | stable | +| 更新灰度比例 | 全量(未设置) | | 平台 | win-x64 | | 下载地址预览 | https://download.example.com/dsh-desk/bin/win-x64/stable/deepseek-harness-1.0.0-win-x64.exe
发布后的目标地址,审批前不保证可下载。 | | 升级 YAML | https://download.example.com/dsh-desk/feeds/win-x64/stable.yml
本次发布将更新此文件;审批前可能不存在或仍指向旧版本。 | diff --git a/apps/desktop/tests/expected/publication-production.md b/apps/desktop/tests/expected/publication-production.md index a3b93ccb87..716e499d5e 100644 --- a/apps/desktop/tests/expected/publication-production.md +++ b/apps/desktop/tests/expected/publication-production.md @@ -8,6 +8,7 @@ | 发布环境 | production | | 版本号 | 1.0.0 | | 升级通道 | stable | +| 更新灰度比例 | 全量(未设置) | | 平台 | win-x64 | | 下载地址预览 | https://download.example.com/dsh-desk/bin/win-x64/stable/deepseek-harness-1.0.0-win-x64.exe
发布后的目标地址,审批前不保证可下载。 | | 升级 YAML | https://download.example.com/dsh-desk/feeds/win-x64/stable.yml
本次发布将更新此文件;审批前可能不存在或仍指向旧版本。 | diff --git a/apps/desktop/tests/expected/publication-win-x64.md b/apps/desktop/tests/expected/publication-win-x64.md index aa7569c0a2..65684cf866 100644 --- a/apps/desktop/tests/expected/publication-win-x64.md +++ b/apps/desktop/tests/expected/publication-win-x64.md @@ -8,6 +8,7 @@ | 发布环境 | test | | 版本号 | 1.0.0 | | 升级通道 | nightly | +| 更新灰度比例 | 全量(未设置) | | 平台 | win-x64 | | 测试 release ID | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
测试下载路径的随机前缀,用于降低地址被猜到的可能性;连续升级测试需使用相同值。 | | 下载地址预览 | https://example.com/dsh-desk/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bin/win-x64/deepseek-harness-1.0.0-win-x64.exe
发布后的目标地址,审批前不保证可下载。 |