Merge pull request #5396 from deepseek-harness/07akioni/desktop-latest-downloads

feat(desktop): add fixed latest installer upload commands
This commit is contained in:
07akioni authored and GitHub committed 2026-09-29 14:16:55 +08:00
commit 010f0e230a
13 files changed
+238 -30

No files matched your search

+2 -2
View File
@@ -39,8 +39,8 @@
en: f0e345fd89868803
zh: fd2c775ad23fa2f0
/deepseek-harness-desktop/package/upload-updates:
en: c64d7fe2fe2534b7
zh: 2188e1ae99ab4b33
en: 72073b2e72590067
zh: 63b1f9790395c37d
/deepseek-harness-desktop/package/unsigned-windows-test-installer:
en: cfd5780dd26a8e46
zh: f7a16b1ede793b7b
+3 -1
View File
@@ -235,9 +235,11 @@ When extraction fails, the installer writes a report with the 7-Zip result and i
### Upload updates
Fixed installer downloads use `pnpm run upload:latest:mac:arm64`, `pnpm run upload:latest:mac:x64`, or `pnpm run upload:latest:win:x64` from the repository root. They upload the completed target's DMG or EXE to `desktop/dsh-latest-macos-arm64.dmg`, `desktop/dsh-latest-macos-x64.dmg`, or `desktop/dsh-latest-windows-x64.exe`. Production URLs start with `https://download.deepseek.com/`; test URLs use `DOWNLOAD_TEST_ORIGIN`. Both deployments reuse the target dotenv's deployment, bucket, and credentials, all normal release validation, and the upload audit below. Each command replaces one fixed object without publishing feeds, blockmaps, or release tags. Test fixed downloads are shared across release IDs in the test bucket; the packaged update URL still requires its original release ID. Publication is explicit, including prerelease versions. Deployment infrastructure must configure revalidation or purge cached fixed downloads after replacement. Local loopback tests cover the upload; release operators own real COS and CDN acceptance.
Test and production uploads through `upload:*` retain a fresh `.desktop-build/upload-records/<environment>-<target>-*` directory after release preflight. `plan.json` records destination, version, every file's size/SHA-512, and published YAML bytes; flushed `events.jsonl` records PUT intent and available response status/request ID; `result.json` records completion or the last failed stage. A missing final result means interruption or unavailable storage, not success. No credential values, authorization headers, or raw SDK errors are recorded. Audit-write failures stop later PUTs. Every object is one streamed Tencent COS PUT with an explicit length and Content-MD5; the COS SDK repeats a request only when its body is not a stream, and this uploader does not retry either. Retain partial records and inspect remote state before another operation: a timeout or failed receipt write does not prove that the object was not stored. These records are local, not tamper-proof or automatically backed up; archive each release's records with its build evidence in controlled storage. Public CDN readback remains separate release qualification, explicitly marked `not-performed` in the upload result.
Windows operators can keep a CLIXML object with DPAPI-encrypted `SecretId` and `SecretKey` SecureString fields outside the repository. The [credential launcher](scripts/upload-with-credentials.ps1) requires an explicit `-CredentialFile` and `-Environment production` or `test`; without `-Upload`, it only verifies decryption and injection into a local Node child, with no network request. It requires Node on `PATH` and the Windows user and machine that encrypted the file. Plaintext, empty, and whitespace-only fields fail. The parent environment is unchanged; the child receives only the selected COS pair after unrelated secrets and Node preload options are removed. Raw child stderr is suppressed and credential values in stdout are redacted. This check does not prove COS authorization. An explicit upload additionally requires `-Upload -Target <target> -Bucket <bucket>` and the normal completed-release prerequisites below; actual cloud upload remains release-operator qualification. This launcher supports permanent keys, not STS credentials. An explicit upload requires the selected deployment and bucket to match the target dotenv and completed package record before network writes; it uses the DPAPI credential pair even if the dotenv contains other COS keys.
Windows operators can keep a CLIXML object with DPAPI-encrypted `SecretId` and `SecretKey` SecureString fields outside the repository. The [credential launcher](scripts/upload-with-credentials.ps1) requires an explicit `-CredentialFile` and `-Environment production` or `test`; without `-Upload`, it only verifies decryption and injection into a local Node child, with no network request. It requires Node on `PATH` and the Windows user and machine that encrypted the file. Plaintext, empty, and whitespace-only fields fail. The parent environment is unchanged; the child receives only the selected COS pair after unrelated secrets and Node preload options are removed. Raw child stderr is suppressed and credential values in stdout are redacted. This check does not prove COS authorization. An explicit upload additionally requires `-Upload -Target <target> -Bucket <bucket>` and the normal completed-release prerequisites below; actual cloud upload remains release-operator qualification. Add `-Latest` to that upload invocation to replace the fixed installer without publishing update feeds or a release tag. This launcher supports permanent keys, not STS credentials. An explicit upload requires the selected deployment and bucket to match the target dotenv and completed package record before network writes; it uses the DPAPI credential pair even if the dotenv contains other COS keys.
`DSH_DESKTOP_AUTO_UPDATE_ENV` selects `test` or `production` for both the URL embedded during packaging and the later COS upload; an absent value selects `test`. Test packaging requires its HTTPS origin in `DOWNLOAD_TEST_ORIGIN`; production uses `https://download.deepseek.com`. Upload requires the selected bucket in `DOWNLOAD_TEST_COS_BUCKET` or `DOWNLOAD_PROD_COS_BUCKET`. Production feeds use `dsh-desk/feeds/<target>/` and binaries use `dsh-desk/bin/<target>/`. Test releases require `DOWNLOAD_TEST_RELEASE_ID`: 32 lowercase hexadecimal characters inserted as `dsh-desk/<release-id>/feeds/<target>/` and `dsh-desk/<release-id>/bin/<target>/`. YAML references, stable-channel aliases, and blockmaps stay inside that release directory. Targets are `mac-arm64`, `mac-x64`, and `win-x64`.
+3 -1
View File
@@ -237,9 +237,11 @@ Windows 安装器在启动时和选定目标目录后检查应用是否正在运
### 上传更新
固定安装包下载在仓库根目录使用 `pnpm run upload:latest:mac:arm64`、`pnpm run upload:latest:mac:x64` 或 `pnpm run upload:latest:win:x64`。它们把已完成目标的 DMG 或 EXE 上传到 `desktop/dsh-latest-macos-arm64.dmg`、`desktop/dsh-latest-macos-x64.dmg` 或 `desktop/dsh-latest-windows-x64.exe`。生产 URL 以 `https://download.deepseek.com/` 开头;测试 URL 使用 `DOWNLOAD_TEST_ORIGIN`。两个部署环境均复用目标 dotenv 中的部署、bucket 和凭据、全部常规发布校验以及下述上传审计。每个命令覆盖一个固定对象,不发布更新清单、blockmap 或发布标签。测试固定下载在测试 bucket 中跨发布 ID 共享;包内更新 URL 仍要求原有发布 ID。发布由操作人员显式执行,也允许预发布版本。部署基础设施必须配置重新验证,或在覆盖后清除固定下载的缓存。本地回环测试覆盖上传行为;真实 COS 和 CDN 验收由发布操作人员负责。
test 与 production 的 `upload:*` 上传在发布前置检查通过后,分别保留新的 `.desktop-build/upload-records/<environment>-<target>-*` 目录。`plan.json` 记录目标、版本、每个文件的大小/SHA-512 和发布的 YAML 字节;刷盘的 `events.jsonl` 记录 PUT 意图及可用的响应状态/请求 ID;`result.json` 记录完成结果或最后失败阶段。缺少最终结果表示中断或存储不可用,不表示成功。不记录凭据值、认证头或原始 SDK 错误。审计写入失败即停止后续 PUT。每个对象都以一次流式腾讯 COS PUT 上传,并携带显式长度与 Content-MD5;COS SDK 仅在请求体不是流时才会重发请求,上传器自身也不重试。保留部分记录,检查远端状态后再执行下一次操作:超时或回执写入失败不能证明对象未存储。这些记录仅在本地,不防篡改,也不会自动备份;每次发布应将它们与构建证据一同归档到受控存储。公网 CDN 回读仍是单独的发布验收,上传结果明确标记为 `not-performed`。
Windows 操作人员可以在仓库外保存 CLIXML 对象,其中 `SecretId` 和 `SecretKey` 是经 DPAPI 加密的 SecureString 字段。[凭据启动器](scripts/upload-with-credentials.ps1)要求显式提供 `-CredentialFile` 和 `-Environment production` 或 `test`;不指定 `-Upload` 时,只验证解密以及向本地 Node 子进程注入凭据,不发起网络请求。它要求 `PATH` 中有 Node,并使用加密该文件时的 Windows 用户和机器。明文、空字段及纯空白字段都会失败。父进程环境保持不变;子进程先清除无关密钥与 Node 预加载选项,再仅接收所选 COS 凭据对。原始子进程 stderr 不会显示,stdout 中的凭据值会被遮盖。此检查不能证明 COS 授权有效。显式上传还要求 `-Upload -Target <target> -Bucket <bucket>` 及下述常规发布完成前提;真实云端上传仍需发布操作人员验收。此启动器支持长期密钥,不支持 STS 凭据。显式上传要求所选部署环境和 bucket 与目标 dotenv 文件及已完成的打包记录一致,才会发起网络写入;即使 dotenv 文件含有其他 COS 密钥,也使用 DPAPI 凭据对。
Windows 操作人员可以在仓库外保存 CLIXML 对象,其中 `SecretId` 和 `SecretKey` 是经 DPAPI 加密的 SecureString 字段。[凭据启动器](scripts/upload-with-credentials.ps1)要求显式提供 `-CredentialFile` 和 `-Environment production` 或 `test`;不指定 `-Upload` 时,只验证解密以及向本地 Node 子进程注入凭据,不发起网络请求。它要求 `PATH` 中有 Node,并使用加密该文件时的 Windows 用户和机器。明文、空字段及纯空白字段都会失败。父进程环境保持不变;子进程先清除无关密钥与 Node 预加载选项,再仅接收所选 COS 凭据对。原始子进程 stderr 不会显示,stdout 中的凭据值会被遮盖。此检查不能证明 COS 授权有效。显式上传还要求 `-Upload -Target <target> -Bucket <bucket>` 及下述常规发布完成前提;真实云端上传仍需发布操作人员验收。在该上传调用中添加 `-Latest`,即可替换固定安装包,而不发布更新清单或发布标签。此启动器支持长期密钥,不支持 STS 凭据。显式上传要求所选部署环境和 bucket 与目标 dotenv 文件及已完成的打包记录一致,才会发起网络写入;即使 dotenv 文件含有其他 COS 密钥,也使用 DPAPI 凭据对。
`DSH_DESKTOP_AUTO_UPDATE_ENV` 同时选择打包写入的 URL 与后续 COS 上传环境,可取 `test` 或 `production`;缺省为 `test`。测试打包通过 `DOWNLOAD_TEST_ORIGIN` 提供 HTTPS origin;生产使用 `https://download.deepseek.com`。上传通过 `DOWNLOAD_TEST_COS_BUCKET` 或 `DOWNLOAD_PROD_COS_BUCKET` 提供所选 bucket。生产清单位于 `dsh-desk/feeds/<target>/`,安装包位于 `dsh-desk/bin/<target>/`。测试发布必须配置 `DOWNLOAD_TEST_RELEASE_ID`:32 位小写十六进制字符,分别插入路径 `dsh-desk/<release-id>/feeds/<target>/` 和 `dsh-desk/<release-id>/bin/<target>/`。YAML 引用、稳定通道别名和 blockmap 都位于该发布目录内。目标为 `mac-arm64`、`mac-x64` 和 `win-x64`。
+4 -1
View File
@@ -35,7 +35,10 @@
"package:win:x64:dir": "tsx scripts/package-target.ts win-x64 --dir",
"upload:mac:arm64": "tsx scripts/upload-target.ts mac-arm64",
"upload:mac:x64": "tsx scripts/upload-target.ts mac-x64",
"upload:win:x64": "tsx scripts/upload-target.ts win-x64"
"upload:win:x64": "tsx scripts/upload-target.ts win-x64",
"upload:latest:mac:arm64": "tsx scripts/upload-target.ts mac-arm64 --latest",
"upload:latest:mac:x64": "tsx scripts/upload-target.ts mac-x64 --latest",
"upload:latest:win:x64": "tsx scripts/upload-target.ts win-x64 --latest"
},
"dependencies": {
"electron-updater": "^6.8.9",
+15 -7
View File
@@ -38,11 +38,12 @@ export interface DesktopUploadArtifact {
readonly contents?: string
}
/** A fully validated upload operation with channel metadata ordered last. */
/** A validated installer or update upload, with any channel metadata ordered last. */
export interface DesktopUploadPlan {
readonly environment: 'test' | 'production'
readonly target: DesktopPackageTargetName
readonly version: string
/** Update feed directory URL, or the single installer URL for a fixed download. */
readonly publicUrl: string
readonly bucket: string
readonly secretIdEnvName: string
@@ -56,6 +57,8 @@ export interface DesktopUploadPlan {
/** Filesystem and environment inputs used to validate one upload. */
export interface DesktopUploadPlanOptions {
/** Publish only the installer at its fixed download URL, replacing the previous object. */
readonly latest?: boolean
readonly environment?: NodeJS.ProcessEnv
readonly repositoryRoot?: string
readonly appRoot?: string
@@ -171,7 +174,7 @@ function uploadArtifact(
* Validate the completed package record, dsh version, update metadata, hashes, and target files.
* @param targetName - Fixed platform and architecture selected by the upload command.
* @param options - Optional filesystem roots and environment for tests or release automation.
* @returns An upload plan whose mutable channel metadata is the final entry.
* @returns A fixed installer upload or an update plan with channel metadata ordered last.
*/
export async function createDesktopUploadPlan(
targetName: DesktopPackageTargetName,
@@ -237,23 +240,26 @@ export async function createDesktopUploadPlan(
const updaterPath = await verifyChecksummedArtifact(artifactsRoot, updaterInfo)
const artifacts: DesktopUploadArtifact[] = []
const binaryPrefix = update.binaryKeyPrefix
let installerArtifact: DesktopUploadArtifact
if (target.platform === 'darwin') {
const dmgPath = await requireArtifact(artifactsRoot, `${base}.dmg`)
const blockmapPath = await requireArtifact(artifactsRoot, `${base}.zip.blockmap`)
installerArtifact = uploadArtifact(dmgPath, binaryPrefix, 'application/x-apple-diskimage')
artifacts.push(
uploadArtifact(dmgPath, binaryPrefix, 'application/x-apple-diskimage'),
installerArtifact,
uploadArtifact(updaterPath, binaryPrefix, 'application/zip'),
uploadArtifact(blockmapPath, binaryPrefix, 'application/octet-stream'),
)
}
else {
const blockmapPath = await requireArtifact(artifactsRoot, `${base}.exe.blockmap`)
artifacts.push(uploadArtifact(
installerArtifact = uploadArtifact(
updaterPath,
binaryPrefix,
'application/vnd.microsoft.portable-executable',
))
)
artifacts.push(installerArtifact)
artifacts.push(uploadArtifact(blockmapPath, binaryPrefix, 'application/octet-stream'))
}
@@ -272,15 +278,17 @@ export async function createDesktopUploadPlan(
const stableFilename = metadataFilename.replace('nightly', 'latest')
artifacts.push({ ...channelArtifact, filename: stableFilename, key: `${update.keyPrefix}/${stableFilename}` })
}
const latestFilename = `dsh-latest-${target.platform === 'darwin' ? 'macos' : 'windows'}-${target.arch}.${target.platform === 'darwin' ? 'dmg' : 'exe'}`
const latestKey = `desktop/${latestFilename}`
return {
environment: update.environment,
target: targetName,
version: buildVersion,
publicUrl: update.publicUrl,
publicUrl: options.latest ? `${update.origin}/${latestKey}` : update.publicUrl,
bucket: update.bucket,
secretIdEnvName: update.secretIdEnvName,
secretKeyEnvName: update.secretKeyEnvName,
artifacts,
artifacts: options.latest ? [{ ...installerArtifact, filename: latestFilename, key: latestKey }] : artifacts,
...typeof buildRecord.commit === 'string' ? { commit: buildRecord.commit } : {},
...typeof buildRecord.dirty === 'boolean' ? { dirty: buildRecord.dirty } : {},
}
+2 -2
View File
@@ -1,4 +1,4 @@
/** Durable, credential-free evidence for test and production release uploads. */
/** Durable, credential-free evidence for test and production Desktop uploads. */
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
@@ -52,7 +52,7 @@ function streamedBody(artifact: DesktopUploadArtifact): Readable {
}
/**
* Upload an already validated release, flushing intent and response evidence around every PUT.
* Upload an already validated installer or update plan, flushing intent and response evidence around every PUT.
*
* Each object is sent as one streamed PUT with an explicit length and Content-MD5, which is also
* what keeps the COS SDK's internal retry path unreachable: it repeats a request only when the
+12 -6
View File
@@ -1,4 +1,4 @@
/** Upload one validated Desktop release to its Tencent COS update directory. */
/** Upload validated Desktop update artifacts or a fixed installer to Tencent COS. */
import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
@@ -58,8 +58,14 @@ export function resolveCredentialUploadEnvironment(
}
}
async function main(): Promise<void> {
const { positionals, values } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, options: {
/**
* Upload the selected target and record a release tag only for production update uploads.
* @param args Target and upload options from the command line.
* @returns Resolves after upload and any release-tag attempt complete.
*/
export async function uploadDesktopTarget(args: string[]): Promise<void> {
const { positionals, values } = parseArgs({ args, allowPositionals: true, options: {
latest: { type: 'boolean' },
'credential-launcher': { type: 'boolean' }, environment: { type: 'string' }, bucket: { type: 'string' },
} })
const target = positionals[0]
@@ -79,14 +85,14 @@ async function main(): Promise<void> {
const environment = launcher
? resolveCredentialUploadEnvironment(fileEnvironment, process.env, values.environment as 'test' | 'production', values.bucket!, name)
: fileEnvironment
const plan = await createDesktopUploadPlan(name, { environment })
const plan = await createDesktopUploadPlan(name, { environment, latest: values.latest === true })
const cos = createDesktopCos({
secretId: requiredEnvironmentValue(environment, plan.secretIdEnvName),
secretKey: requiredEnvironmentValue(environment, plan.secretKeyEnvName),
})
process.stdout.write(`desktop upload: ${plan.target} ${plan.version} -> ${plan.publicUrl}\n`)
await uploadDesktopRelease(plan, cos, resolve(import.meta.dirname, '../.desktop-build/upload-records'))
if (plan.environment === 'production') recordProductionRelease(plan)
if (plan.environment === 'production' && !values.latest) recordProductionRelease(plan)
}
/**
@@ -116,7 +122,7 @@ function recordProductionRelease(plan: DesktopUploadPlan): void {
}
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
main().catch(() => {
uploadDesktopTarget(process.argv.slice(2)).catch(() => {
process.stderr.write('desktop upload: failed; inspect the printed record directory if allocated. No automatic retry; reconcile remote state before another upload.\n')
process.exitCode = 1
})
@@ -14,6 +14,8 @@ Deployment that owns the credential pair; never inferred from the filename.
Completed Desktop target to upload when Upload is explicitly selected.
.PARAMETER Bucket
COS bucket for an explicit upload. Not needed for the local credential check.
.PARAMETER Latest
Publish only the installer at its fixed download URL. Requires Upload.
.PARAMETER Upload
Authorize the existing target upload entry. Omit to check credentials locally.
#>
@@ -32,7 +34,9 @@ param(
[ValidatePattern('^[a-z0-9-]+$')]
[string]$Bucket,
[Parameter(ParameterSetName = 'Publish', Mandatory = $true)]
[switch]$Upload
[switch]$Upload,
[Parameter(ParameterSetName = 'Publish')]
[switch]$Latest
)
$ErrorActionPreference = 'Stop'
@@ -87,6 +91,7 @@ try {
if ($Upload) {
$startInfo.EnvironmentVariables["${prefix}_BUCKET"] = $Bucket
$startInfo.Arguments = "--import tsx/esm apps/desktop/scripts/upload-target.ts $Target --credential-launcher --environment $Environment --bucket $Bucket"
if ($Latest) { $startInfo.Arguments += " --latest" }
Write-Output "desktop credentials: uploading $Target to $Environment; release validation runs before network writes."
} else {
$probe = "const id=process.env.${prefix}_SECRET_ID;const key=process.env.${prefix}_SECRET_KEY;process.exit(id?.trim()&&key?.trim()?0:1)"
+64 -3
View File
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto'
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { createRequire } from 'node:module'
import { join } from 'node:path'
@@ -8,12 +8,15 @@ import { load } from 'js-yaml'
import { createDesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
import { desktopUpdateMetadataFilename } from '../scripts/desktop-auto-update-environment.mjs'
import type { DesktopPackageTargetName } from '../scripts/package-target.ts'
import { createDesktopCos } from '../scripts/desktop-cos.ts'
import { uploadDesktopRelease } from '../scripts/desktop-upload-run.ts'
import { startCosLoopback } from './cos-loopback.ts'
const temporaryDirectories: string[] = []
const TEST_ORIGIN = 'https://desktop-updates.example.com'
const TEST_BUCKET = 'test-download-bucket'
const TEST_BUCKET = 'test-download-bucket-1250000000'
const RELEASE_ID = '0123456789abcdef0123456789abcdef'
const PRODUCTION_BUCKET = 'production-download-bucket'
const PRODUCTION_BUCKET = 'production-download-bucket-1250000000'
const require = createRequire(import.meta.url)
const { createBlockmap } = require('app-builder-lib/out/targets/differentialUpdateInfoBuilder.js') as {
createBlockmap: (file: string, target: object, packager: { info: { emitArtifactBuildCompleted(event: object): Promise<void> } },
@@ -110,6 +113,64 @@ afterEach(async () => {
})
describe('desktop upload plan', () => {
it('uploads only the selected latest installer to each deployment using the existing COS transport', async () => {
const published = []
for (const environment of ['production', 'test'] as const) {
for (const target of ['mac-arm64', 'mac-x64', 'win-x64'] as const) {
const paths = await fixture(target, '1.2.3', environment)
const plan = await createDesktopUploadPlan(target, { ...paths, latest: true })
const loopback = await startCosLoopback()
try {
const cos = createDesktopCos({ secretId: 'fixture-id', secretKey: 'fixture-secret' })
loopback.redirect(cos)
const directory = await uploadDesktopRelease(plan, cos, join(paths.appRoot, 'records'))
expect(plan.artifacts).toHaveLength(1)
const artifact = plan.artifacts[0]!
expect(loopback.requests).toHaveLength(1)
const request = loopback.requests[0]!
expect(request.method).toBe('PUT')
expect(request.path).toBe(`/${artifact.key}`)
expect(request.body).toEqual(await readFile(artifact.path))
expect(JSON.parse(await readFile(join(directory, 'result.json'), 'utf8'))).toMatchObject({
success: true, confirmedPuts: 1, publicReadback: 'not-performed',
})
published.push({ environment, target, bucket: plan.bucket, publicUrl: plan.publicUrl,
filename: artifact.filename, key: artifact.key, contentType: artifact.contentType,
channelMetadata: artifact.channelMetadata })
} finally {
await loopback.close()
}
}
}
await expect(`${JSON.stringify(published, null, 2)}\n`).toMatchFileSnapshot('./expected/latest-installer-uploads.json')
})
it('allows an explicitly selected production prerelease at the fixed installer URL', async () => {
const paths = await fixture('win-x64', '1.2.3-alpha.4', 'production')
const plan = await createDesktopUploadPlan('win-x64', { ...paths, latest: true })
expect(plan.version).toBe('1.2.3-alpha.4')
expect(plan.artifacts).toHaveLength(1)
expect(plan.artifacts[0]).toMatchObject({
path: join(paths.artifactsRoot, 'deepseek-harness-1.2.3-alpha.4-win-x64.exe'),
key: 'desktop/dsh-latest-windows-x64.exe', channelMetadata: false,
})
})
it.each(['completion', 'deployment', 'checksum'] as const)('rejects invalid %s before planning a latest upload', async (failure) => {
const paths = await fixture('win-x64', '1.2.3', 'production')
if (failure === 'completion') await rm(join(paths.artifactsRoot, 'win-x64-release.json'))
if (failure === 'deployment') Object.assign(paths.environment, {
DSH_DESKTOP_AUTO_UPDATE_ENV: 'test', DOWNLOAD_TEST_ORIGIN: TEST_ORIGIN,
DOWNLOAD_TEST_RELEASE_ID: RELEASE_ID, DOWNLOAD_TEST_COS_BUCKET: TEST_BUCKET,
})
if (failure === 'checksum') {
const path = join(paths.artifactsRoot, 'deepseek-harness-1.2.3-win-x64.exe')
await writeFile(path, Buffer.alloc((await readFile(path)).length))
}
await expect(createDesktopUploadPlan('win-x64', { ...paths, latest: true }))
.rejects.toThrow(failure === 'checksum' ? /SHA-512/u : /completion record/u)
})
it('publishes fixed feeds referencing versioned binaries without overriding CDN cache policy', async () => {
const paths = await fixture('win-x64', '1.2.3', 'production')
const plan = await createDesktopUploadPlan('win-x64', paths)
@@ -0,0 +1,62 @@
[
{
"environment": "production",
"target": "mac-arm64",
"bucket": "production-download-bucket-1250000000",
"publicUrl": "https://download.deepseek.com/desktop/dsh-latest-macos-arm64.dmg",
"filename": "dsh-latest-macos-arm64.dmg",
"key": "desktop/dsh-latest-macos-arm64.dmg",
"contentType": "application/x-apple-diskimage",
"channelMetadata": false
},
{
"environment": "production",
"target": "mac-x64",
"bucket": "production-download-bucket-1250000000",
"publicUrl": "https://download.deepseek.com/desktop/dsh-latest-macos-x64.dmg",
"filename": "dsh-latest-macos-x64.dmg",
"key": "desktop/dsh-latest-macos-x64.dmg",
"contentType": "application/x-apple-diskimage",
"channelMetadata": false
},
{
"environment": "production",
"target": "win-x64",
"bucket": "production-download-bucket-1250000000",
"publicUrl": "https://download.deepseek.com/desktop/dsh-latest-windows-x64.exe",
"filename": "dsh-latest-windows-x64.exe",
"key": "desktop/dsh-latest-windows-x64.exe",
"contentType": "application/vnd.microsoft.portable-executable",
"channelMetadata": false
},
{
"environment": "test",
"target": "mac-arm64",
"bucket": "test-download-bucket-1250000000",
"publicUrl": "https://desktop-updates.example.com/desktop/dsh-latest-macos-arm64.dmg",
"filename": "dsh-latest-macos-arm64.dmg",
"key": "desktop/dsh-latest-macos-arm64.dmg",
"contentType": "application/x-apple-diskimage",
"channelMetadata": false
},
{
"environment": "test",
"target": "mac-x64",
"bucket": "test-download-bucket-1250000000",
"publicUrl": "https://desktop-updates.example.com/desktop/dsh-latest-macos-x64.dmg",
"filename": "dsh-latest-macos-x64.dmg",
"key": "desktop/dsh-latest-macos-x64.dmg",
"contentType": "application/x-apple-diskimage",
"channelMetadata": false
},
{
"environment": "test",
"target": "win-x64",
"bucket": "test-download-bucket-1250000000",
"publicUrl": "https://desktop-updates.example.com/desktop/dsh-latest-windows-x64.exe",
"filename": "dsh-latest-windows-x64.exe",
"key": "desktop/dsh-latest-windows-x64.exe",
"contentType": "application/vnd.microsoft.portable-executable",
"channelMetadata": false
}
]
+56
View File
@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createDesktopUploadPlan, type DesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
import { loadDesktopPackageEnvironment } from '../scripts/desktop-package-environment.mjs'
import { tagDesktopRelease } from '../scripts/desktop-release-tag.ts'
import { uploadDesktopRelease } from '../scripts/desktop-upload-run.ts'
import { uploadDesktopTarget } from '../scripts/upload-target.ts'
vi.mock('../scripts/desktop-package-environment.mjs', () => ({ loadDesktopPackageEnvironment: vi.fn() }))
vi.mock('../scripts/desktop-upload-plan.ts', () => ({ createDesktopUploadPlan: vi.fn() }))
vi.mock('../scripts/desktop-cos.ts', () => ({ createDesktopCos: vi.fn() }))
vi.mock('../scripts/desktop-upload-run.ts', () => ({ uploadDesktopRelease: vi.fn() }))
vi.mock('../scripts/desktop-release-tag.ts', async importOriginal => ({
...await importOriginal<typeof import('../scripts/desktop-release-tag.ts')>(),
tagDesktopRelease: vi.fn(),
}))
afterEach(() => {
vi.restoreAllMocks()
vi.resetAllMocks()
})
describe('desktop upload command', () => {
it.each([
{ environment: 'production', latest: true, tagged: false },
{ environment: 'production', latest: false, tagged: true },
{ environment: 'test', latest: true, tagged: false },
{ environment: 'test', latest: false, tagged: false },
] as const)('uploads $environment latest=$latest with tagged=$tagged', async ({ environment, latest, tagged }) => {
vi.spyOn(process.stdout, 'write').mockReturnValue(true)
const credentials = { DOWNLOAD_TEST_COS_SECRET_ID: 'fixture-id', DOWNLOAD_TEST_COS_SECRET_KEY: 'fixture-key' }
const plan: DesktopUploadPlan = {
environment, target: 'win-x64', version: '1.2.3-alpha.4',
publicUrl: 'https://desktop-updates.example.com/desktop/dsh-latest-windows-x64.exe',
bucket: 'fixture-bucket', secretIdEnvName: 'DOWNLOAD_TEST_COS_SECRET_ID',
secretKeyEnvName: 'DOWNLOAD_TEST_COS_SECRET_KEY', artifacts: [], commit: '0'.repeat(40), dirty: false,
}
vi.mocked(loadDesktopPackageEnvironment).mockReturnValue(credentials)
vi.mocked(createDesktopUploadPlan).mockResolvedValue(plan)
vi.mocked(uploadDesktopRelease).mockResolvedValue('fixture-records')
vi.mocked(tagDesktopRelease).mockReturnValue({ status: 'created', tag: 'desktop-v1.2.3-alpha.4' })
await uploadDesktopTarget(['win-x64', ...latest ? ['--latest'] : []])
expect(createDesktopUploadPlan).toHaveBeenCalledWith('win-x64', { environment: credentials, latest })
expect(uploadDesktopRelease).toHaveBeenCalledOnce()
expect(vi.mocked(uploadDesktopRelease).mock.calls[0]![0]).toBe(plan)
if (tagged) {
expect(tagDesktopRelease).toHaveBeenCalledWith(expect.objectContaining({ version: plan.version, commit: plan.commit }))
expect(vi.mocked(tagDesktopRelease).mock.invocationCallOrder[0])
.toBeGreaterThan(vi.mocked(uploadDesktopRelease).mock.invocationCallOrder[0]!)
}
else {
expect(tagDesktopRelease).not.toHaveBeenCalled()
}
})
})
@@ -12,13 +12,13 @@ const launcher = fileURLToPath(new URL('../scripts/upload-with-credentials.ps1',
const roots: string[] = []
const quote = (value: string): string => `'${value.replaceAll("'", "''")}'`
async function check(mode: 'valid' | 'plaintext' | 'blank' | 'missing' | 'upload' | 'upload-failure', deployment = 'production') {
async function check(mode: 'valid' | 'plaintext' | 'blank' | 'missing' | 'upload' | 'upload-latest' | 'upload-failure', deployment = 'production') {
const root = await mkdtemp(join(tmpdir(), 'desktop-credentials-'))
roots.push(root)
const credentialPath = join(root, 'credential.clixml')
let entry = launcher
let uploadArguments = ''
if (mode === 'upload' || mode === 'upload-failure') {
if (mode === 'upload' || mode === 'upload-latest' || mode === 'upload-failure') {
const scripts = join(root, 'apps/desktop/scripts')
await mkdir(scripts, { recursive: true })
entry = join(scripts, 'upload-with-credentials.ps1')
@@ -34,12 +34,12 @@ async function check(mode: 'valid' | 'plaintext' | 'blank' | 'missing' | 'upload
assert.equal(process.env.DSH_DESKTOP_WINDOWS_TOKEN_PIN, undefined)
assert.equal(process.env.NODE_OPTIONS, undefined)
assert.equal(process.argv[2], 'win-x64')
assert.deepEqual(process.argv.slice(3), ['--credential-launcher', '--environment', 'production', '--bucket', 'fixture-bucket'])
assert.deepEqual(process.argv.slice(3), ['--credential-launcher', '--environment', 'production', '--bucket', 'fixture-bucket'${mode === 'upload-latest' ? ", '--latest'" : ''}])
console.log('fixture-id fixture-secret')
console.error('private service details fixture-secret')
process.exit(${mode === 'upload-failure' ? 17 : 0})
`)
uploadArguments = ' -Upload -Target win-x64 -Bucket fixture-bucket'
uploadArguments = ` -Upload -Target win-x64 -Bucket fixture-bucket${mode === 'upload-latest' ? ' -Latest' : ''}`
}
const setup = mode === 'missing' ? '' : `
[pscustomobject]@{
@@ -134,9 +134,9 @@ describe.skipIf(process.platform !== 'win32')('Windows upload credentials', () =
expect(result.output).not.toContain('child environment verified')
})
it.each(['upload', 'upload-failure'] as const)('isolates the %s child and sanitizes both output streams', async (mode) => {
it.each(['upload', 'upload-latest', 'upload-failure'] as const)('isolates the %s child and sanitizes both output streams', async (mode) => {
const result = await check(mode)
expect(result.code, result.output).toBe(mode === 'upload' ? 0 : 1)
expect(result.code, result.output).toBe(mode === 'upload-failure' ? 1 : 0)
expect(result.output).toContain('[REDACTED] [REDACTED]')
expect(result.output).not.toContain('fixture-id')
expect(result.output).not.toContain('fixture-secret')
+3
View File
@@ -41,6 +41,9 @@
"upload:mac:arm64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:mac:arm64",
"upload:mac:x64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:mac:x64",
"upload:win:x64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:win:x64",
"upload:latest:mac:arm64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:latest:mac:arm64",
"upload:latest:mac:x64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:latest:mac:x64",
"upload:latest:win:x64": "pnpm --filter @deepseek-ai/dsh-desktop run upload:latest:win:x64",
"clean": "tsx scripts/clean.ts",
"change-scope": "tsx scripts/change-scope.ts",
"typecheck": "npm run build:lib:host && npm run typecheck:contracts-ready",