From 912e0383fb391d1a8364ee10b9d8bace3e5b701a Mon Sep 17 00:00:00 2001 From: winewill Date: Tue, 3 Nov 2026 12:54:24 +0800 Subject: [PATCH 1/9] fix(desktop): preserve modification times in installer artifacts --- .github/workflows/macos-intel-package.yml | 11 ++++--- .github/workflows/macos-package.yml | 11 ++++--- .github/workflows/windows-package.yml | 26 +++++----------- apps/desktop/packaging/README.md | 2 +- apps/desktop/packaging/README.zh.md | 2 +- apps/desktop/scripts/archive-desktop-ci.ts | 19 +++++++++--- .../scripts/preflight-desktop-publish.mjs | 2 +- .../tests/desktop-publish-preflight.spec.ts | 2 ++ apps/desktop/tests/retain-desktop-ci.spec.ts | 31 ++++++++++++++++++- scripts/macos-package-workflow.spec.ts | 14 +++------ scripts/windows-package-workflow.spec.ts | 14 +++------ 11 files changed, 78 insertions(+), 56 deletions(-) diff --git a/.github/workflows/macos-intel-package.yml b/.github/workflows/macos-intel-package.yml index 55e3f2ee62..4e5e3b3b69 100644 --- a/.github/workflows/macos-intel-package.yml +++ b/.github/workflows/macos-intel-package.yml @@ -170,6 +170,7 @@ jobs: timeout-minutes: 30 env: DESKTOP_RETAINED_DIRECTORY: ${{ needs.package.outputs.directory }} + DESKTOP_ARCHIVE_NAME: macos-x64-${{ needs.package.outputs.mode }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }}.zip DESKTOP_PACKAGE_ATTEMPT: ${{ needs.package.outputs.attempt }} DESKTOP_PACKAGE_RUNNER: ${{ needs.package.outputs.runner-name }} steps: @@ -192,17 +193,17 @@ jobs: echo "PNPM_CONFIG_STORE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR/workspace-pnpm-store" >> "$GITHUB_ENV" - run: pnpm install --frozen-lockfile working-directory: artifact-archiver - - name: Verify original retained build before GitHub archival + - name: Archive verified retained build + id: archive working-directory: artifact-archiver run: node --import tsx apps/desktop/scripts/archive-desktop-ci.ts - name: Retain installers for the selected mode if: success() uses: actions/upload-artifact@v7.0.1 with: - name: macos-x64-${{ needs.package.outputs.mode }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }} - include-hidden-files: true - path: ${{ needs.package.outputs.directory }}/* - compression-level: 0 + name: ${{ env.DESKTOP_ARCHIVE_NAME }} + path: ${{ steps.archive.outputs.path }} + archive: false if-no-files-found: error retention-days: 14 overwrite: true diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 1fcd8005a7..d4159e7457 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -167,6 +167,7 @@ jobs: timeout-minutes: 30 env: DESKTOP_RETAINED_DIRECTORY: ${{ needs.package.outputs.directory }} + DESKTOP_ARCHIVE_NAME: macos-arm64-${{ needs.package.outputs.mode }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }}.zip DESKTOP_PACKAGE_ATTEMPT: ${{ needs.package.outputs.attempt }} DESKTOP_PACKAGE_RUNNER: ${{ needs.package.outputs.runner-name }} steps: @@ -189,17 +190,17 @@ jobs: echo "PNPM_CONFIG_STORE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR/workspace-pnpm-store" >> "$GITHUB_ENV" - run: pnpm install --frozen-lockfile working-directory: artifact-archiver - - name: Verify original retained build before GitHub archival + - name: Archive verified retained build + id: archive working-directory: artifact-archiver run: node --import tsx apps/desktop/scripts/archive-desktop-ci.ts - name: Retain installers for the selected mode if: success() uses: actions/upload-artifact@v7.0.1 with: - name: macos-arm64-${{ needs.package.outputs.mode }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }} - include-hidden-files: true - path: ${{ needs.package.outputs.directory }}/* - compression-level: 0 + name: ${{ env.DESKTOP_ARCHIVE_NAME }} + path: ${{ steps.archive.outputs.path }} + archive: false if-no-files-found: error retention-days: 14 overwrite: true diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index 4235afc46e..e32e71a0cb 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -220,6 +220,7 @@ jobs: env: PACKAGE_CACHE_NAMESPACE: ${{ inputs.cache_namespace }} DESKTOP_RETAINED_DIRECTORY: ${{ needs.package.outputs.directory }} + DESKTOP_ARCHIVE_NAME: windows-x64-${{ inputs.signing }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }}.zip DESKTOP_PACKAGE_ATTEMPT: ${{ needs.package.outputs.attempt }} DESKTOP_PACKAGE_RUNNER: ${{ needs.package.outputs.runner-name }} steps: @@ -251,28 +252,17 @@ jobs: "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV - run: pnpm install --frozen-lockfile working-directory: artifact-archiver - - name: Verify original retained build before GitHub archival + - name: Archive verified retained build + id: archive working-directory: artifact-archiver run: node --import tsx apps/desktop/scripts/archive-desktop-ci.ts - - name: Retain verified unsigned installer - if: success() && inputs.signing == 'unsigned' + - name: Retain installers for the selected mode + if: success() uses: actions/upload-artifact@v7.0.1 with: - name: windows-x64-unsigned-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }} - compression-level: 0 - include-hidden-files: true - path: ${{ needs.package.outputs.directory }}/* - if-no-files-found: error - retention-days: 14 - overwrite: true - - name: Retain verified signed installer - if: success() && inputs.signing == 'signed' - uses: actions/upload-artifact@v7.0.1 - with: - name: windows-x64-signed-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }} - compression-level: 0 - include-hidden-files: true - path: ${{ needs.package.outputs.directory }}/* + name: ${{ env.DESKTOP_ARCHIVE_NAME }} + path: ${{ steps.archive.outputs.path }} + archive: false if-no-files-found: error retention-days: 14 overwrite: true diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 28aa9ff9c7..3ff5b3d2d3 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -113,7 +113,7 @@ The `Package Desktop · Windows · x64` [manual workflow](../../../.github/workf 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. -Open the completed run in Actions and download its Artifacts. `windows-x64-signed---` and `windows-x64-unsigned---` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. The upload steps are `Retain verified signed installer`, `Retain verified unsigned installer`, and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. +Open the completed run in Actions and download its Artifacts. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs store the runner’s local modification time and the Unix modification timestamp for each entry. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 2e87933826..cf0c0a8a77 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -113,7 +113,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 在 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 打开完成的运行并下载 Artifacts。`windows-x64-signed---` 与 `windows-x64-unsigned---` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。上传步骤名为 `Retain verified signed installer`、`Retain verified unsigned installer` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 +在 Actions 打开完成的运行并下载 Artifacts。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 同时记录各文件的 runner 本地修改时间和 Unix 修改时间戳。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 849433b698..8a85165612 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -1,5 +1,7 @@ /** Validate the original machine and immutable inventory before GitHub artifact archival. */ -import { resolve } from 'node:path' +import { execFileSync } from 'node:child_process' +import { appendFile, mkdtemp } from 'node:fs/promises' +import { join, resolve } from 'node:path' import { desktopPublicationFailure } from './desktop-publication-failure.ts' import { desktopReleasesRoot } from './retain-desktop-ci.ts' import { retainedDesktopPath, verifyRetainedDesktopRelease } from './desktop-release-store.ts' @@ -7,9 +9,9 @@ import { retainedDesktopPath, verifyRetainedDesktopRelease } from './desktop-rel /** * Verify an original build without interpreting the archive job's attempt as a new build. * @param env Original build outputs and current Actions machine identity. - * @returns Resolves after the retained inventory passes all digest checks. + * @returns Verified file names to include alongside the manifest. */ -export async function verifyDesktopArchive(env: NodeJS.ProcessEnv): Promise { +export async function verifyDesktopArchive(env: NodeJS.ProcessEnv): Promise { if (!env.RUNNER_NAME || env.RUNNER_NAME !== env.DESKTOP_PACKAGE_RUNNER) throw new Error('desktop archival: wrong original machine') const root = desktopReleasesRoot(env) const runId = env.GITHUB_RUN_ID || '' @@ -20,11 +22,18 @@ export async function verifyDesktopArchive(env: NodeJS.ProcessEnv): Promise file.name)] } if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) { - try { await verifyDesktopArchive(process.env) } - catch (error) { + try { + const files = await verifyDesktopArchive(process.env) + const { DESKTOP_ARCHIVE_NAME: name, DESKTOP_RETAINED_DIRECTORY: directory, RUNNER_TEMP: temp, GITHUB_OUTPUT: output } = process.env + if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9.+_-]*\.zip$/u.test(name) || !directory || !temp || !output) throw new Error('desktop archival: invalid ZIP output settings') + const path = join(await mkdtemp(join(temp, 'desktop-archive-')), name) + execFileSync('tar', ['--format', 'zip', '--options', 'zip:compression=store', '--no-mac-metadata', '--no-xattrs', '-cf', path, '--', ...files], { cwd: directory, stdio: 'inherit' }) + await appendFile(output, `path=${path}\n`) + } catch (error) { process.stderr.write(`${JSON.stringify(desktopPublicationFailure(error, 'artifact archival'))}\n`) process.exitCode = 1 } diff --git a/apps/desktop/scripts/preflight-desktop-publish.mjs b/apps/desktop/scripts/preflight-desktop-publish.mjs index dfc1dcb4a6..fbe6947398 100644 --- a/apps/desktop/scripts/preflight-desktop-publish.mjs +++ b/apps/desktop/scripts/preflight-desktop-publish.mjs @@ -27,7 +27,7 @@ export function preflightDesktopPublish(repository, inputs, api) { throw new Error('desktop publish: GitHub returned invalid artifact metadata') } for (const { name } of pages.flatMap(page => page.artifacts)) { - if (!name.endsWith(`-${inputs.package_run_id}-${attempt}`)) continue + if (!name.replace(/\.zip$/u, '').endsWith(`-${inputs.package_run_id}-${attempt}`)) continue const qualification = name.startsWith('windows-x64-unsigned-') ? 'unsigned' : /^macos-(?:arm64|x64)-unnotarized-debug-/u.test(name) ? 'unnotarized' : '' const diagnostic = desktopPublishQualificationError(qualification) diff --git a/apps/desktop/tests/desktop-publish-preflight.spec.ts b/apps/desktop/tests/desktop-publish-preflight.spec.ts index f6c99ae280..755a0ce8a6 100644 --- a/apps/desktop/tests/desktop-publish-preflight.spec.ts +++ b/apps/desktop/tests/desktop-publish-preflight.spec.ts @@ -4,6 +4,8 @@ import { preflightDesktopPublish } from '../scripts/preflight-desktop-publish.mj it.each([ ['macos-x64-unnotarized-debug-1.0.0-42-2', '跳过了公证'], ['macos-arm64-unnotarized-debug-1.0.0-42-2', '跳过了公证'], + ['macos-arm64-unnotarized-debug-1.0.0-42-2.zip', '跳过了公证'], + ['windows-x64-unsigned-1.0.0-42-2.zip', '未签名'], ['windows-x64-unsigned-1.0.0-42-2', '未签名'], ])('rejects %s from paginated artifact metadata with rebuilding instructions', (name, reason) => { const paths: string[] = [] diff --git a/apps/desktop/tests/retain-desktop-ci.spec.ts b/apps/desktop/tests/retain-desktop-ci.spec.ts index cc26e3a2d6..cf71b12789 100644 --- a/apps/desktop/tests/retain-desktop-ci.spec.ts +++ b/apps/desktop/tests/retain-desktop-ci.spec.ts @@ -1,4 +1,5 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { execFileSync } from 'node:child_process' +import { mkdtemp, mkdir, readFile, rm, utimes, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it } from 'vitest' @@ -39,6 +40,34 @@ it.each([ const directory = await retainDesktopCi(env, appRoot) const archiveEnv = { ...env, GITHUB_RUN_ATTEMPT: '3', DESKTOP_RETAINED_DIRECTORY: directory, DESKTOP_PACKAGE_ATTEMPT: '2', DESKTOP_PACKAGE_RUNNER: env.RUNNER_NAME } + if (target === 'win-x64' && qualification === 'signed' && ['darwin', 'win32'].includes(process.platform)) { + const timestamp = Date.UTC(2026, 9, 23, 8, 2, 43) / 1000 + await utimes(join(directory, 'manifest.json'), timestamp, timestamp) + const output = join(root, 'archive-output') + execFileSync(process.execPath, ['--import', 'tsx', join(import.meta.dirname, '../scripts/archive-desktop-ci.ts')], { + env: { ...process.env, ...archiveEnv, DESKTOP_ARCHIVE_NAME: 'fixture.zip', GITHUB_OUTPUT: output, RUNNER_TEMP: root }, + }) + const path = (await readFile(output, 'utf8')).trim().slice('path='.length) + const listing = execFileSync(process.platform === 'win32' ? 'python' : 'python3', ['-c', ` +import datetime, json, struct, sys, zipfile +with zipfile.ZipFile(sys.argv[1]) as archive: + entry = archive.getinfo('manifest.json') + seconds = int(sys.argv[2]) + assert entry.date_time == datetime.datetime.fromtimestamp(seconds - 1).timetuple()[:6] + extra = entry.extra + while extra: + tag, size = struct.unpack_from(' { const installer = archive.steps.find(step => step.uses?.startsWith('actions/upload-artifact@'))! expect(uploads[0]?.if).toBe("always() && steps.checkout.outcome == 'success'") expect(installer.if).toBe('success()') - expect(installer.with?.path).toBe('${{ needs.package.outputs.directory }}/*') - expect(installer.with?.['compression-level']).toBe(0) + expect(installer.with?.path).toBe('${{ steps.archive.outputs.path }}') + expect(installer.with).toMatchObject({ archive: false, overwrite: true, name: '${{ env.DESKTOP_ARCHIVE_NAME }}' }) }) it('defaults to notarization and labels the explicit debugging option', () => { @@ -59,7 +59,7 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { } } }) const steps = workflow.jobs.package!.steps expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2) - expect(workflow.jobs['upload-artifact']!.steps.find(step => step.name === 'Retain installers for the selected mode')?.with?.name).toContain('${{ needs.package.outputs.mode }}') + expect(workflow.jobs['upload-artifact']!.env.DESKTOP_ARCHIVE_NAME).toContain('${{ needs.package.outputs.mode }}') }) it('retains build inputs outside checkout and run-specific pnpm installations', () => { @@ -87,18 +87,14 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { DESKTOP_PACKAGE_ATTEMPT: '${{ needs.package.outputs.attempt }}', DESKTOP_PACKAGE_RUNNER: '${{ needs.package.outputs.runner-name }}', }) - for (const step of archive.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))) { - expect(step.with?.name).toContain('${{ needs.package.outputs.version }}') - expect(step.with?.name).toContain('${{ needs.package.outputs.attempt }}') - expect(step.with?.name).not.toContain('github.run_attempt') - } + const job = workflow.jobs.package! expect(job.env.DSH_DESKTOP_TARGET_PLATFORM).toBe('darwin') expect(job.env.DSH_DESKTOP_TARGET_ARCH).toBe(arch) const builds = job.steps.filter(step => step.run?.includes('pnpm run package:desktop:mac:')) expect(builds).toHaveLength(2) for (const step of builds) expect(step.run).toContain(`package:desktop:mac:${arch}`) - expect(workflow.jobs['upload-artifact']!.steps.find(step => step.name === 'Retain installers for the selected mode')?.with?.name).toContain(`macos-${arch}-`) + expect(workflow.jobs['upload-artifact']!.env.DESKTOP_ARCHIVE_NAME).toContain(`macos-${arch}-`) if (arch === 'x64') { const rosetta = job.steps.findIndex(step => step.run === 'arch -x86_64 /usr/bin/true') expect(rosetta).toBeGreaterThan(-1) diff --git a/scripts/windows-package-workflow.spec.ts b/scripts/windows-package-workflow.spec.ts index 5e57eee865..a21e417709 100644 --- a/scripts/windows-package-workflow.spec.ts +++ b/scripts/windows-package-workflow.spec.ts @@ -113,22 +113,16 @@ it('retains failure diagnostics and uploads only the selected successful install DESKTOP_PACKAGE_ATTEMPT: '${{ needs.package.outputs.attempt }}', DESKTOP_PACKAGE_RUNNER: '${{ needs.package.outputs.runner-name }}', }) - for (const step of archive.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))) { - expect(step.with?.name).toContain('${{ needs.package.outputs.version }}') - expect(step.with?.name).toContain('${{ needs.package.outputs.attempt }}') - expect(step.with?.name).not.toContain('github.run_attempt') - } + const archival = workflow.jobs['upload-artifact']!.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@')) - expect(archival).toHaveLength(2) + expect(archival).toHaveLength(1) expect(uploads[0]!.if).toBe("always() && steps.checkout.outcome == 'success'") expect(uploads[0]!.with).toMatchObject({ path: '${{ github.workspace }}/apps/desktop/.desktop-build/packaging-runs/', 'include-hidden-files': true, }) - for (const mode of ['unsigned', 'signed']) { - const upload = archival.find(step => step.if === `success() && inputs.signing == '${mode}'`)! - expect(upload.with).toMatchObject({ path: '${{ needs.package.outputs.directory }}/*', 'if-no-files-found': 'error', 'compression-level': 0 }) - } + expect(archive.env.DESKTOP_ARCHIVE_NAME).toBe('windows-x64-${{ inputs.signing }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }}.zip') + expect(archival[0]!.with).toMatchObject({ path: '${{ steps.archive.outputs.path }}', 'if-no-files-found': 'error', archive: false, overwrite: true, name: '${{ env.DESKTOP_ARCHIVE_NAME }}' }) }) it('normalizes priority in the dependency and packaging shells before launching their children', () => { From 6a3c52c92b10eb7b7b4285fbaef0d5abaf82725d Mon Sep 17 00:00:00 2001 From: winewill Date: Tue, 3 Nov 2026 12:55:07 +0800 Subject: [PATCH 2/9] docs(desktop): record the updated archive download instructions --- apps/desktop/packaging/README.i18n.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index c39bb71bc3..80bd055531 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: 253e17998951931c - zh: b582b377391d2ec2 + en: 356911821dde5775 + zh: 1552fa7e0f16e640 /desktop-packaging/macos-self-hosted-packaging: en: f4cc5209bdaeefba zh: 1db959d1ed345fcc From a4135f1f2f14edb2408c66347c704a551d1b670b Mon Sep 17 00:00:00 2001 From: winewill Date: Tue, 3 Nov 2026 13:23:18 +0800 Subject: [PATCH 3/9] fix(desktop): use the system ZIP writer and verify archive failures --- apps/desktop/scripts/archive-desktop-ci.ts | 12 ++- apps/desktop/tests/retain-desktop-ci.spec.ts | 90 +++++++++++++------- scripts/macos-package-workflow.spec.ts | 3 +- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 8a85165612..7a0714efb2 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -1,4 +1,4 @@ -/** Validate the original machine and immutable inventory before GitHub artifact archival. */ +/** Create the verified installer ZIP and expose its path to the Actions upload step. */ import { execFileSync } from 'node:child_process' import { appendFile, mkdtemp } from 'node:fs/promises' import { join, resolve } from 'node:path' @@ -31,7 +31,15 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi const { DESKTOP_ARCHIVE_NAME: name, DESKTOP_RETAINED_DIRECTORY: directory, RUNNER_TEMP: temp, GITHUB_OUTPUT: output } = process.env if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9.+_-]*\.zip$/u.test(name) || !directory || !temp || !output) throw new Error('desktop archival: invalid ZIP output settings') const path = join(await mkdtemp(join(temp, 'desktop-archive-')), name) - execFileSync('tar', ['--format', 'zip', '--options', 'zip:compression=store', '--no-mac-metadata', '--no-xattrs', '-cf', path, '--', ...files], { cwd: directory, stdio: 'inherit' }) + const windows = process.platform === 'win32' + if (windows && !process.env.SystemRoot) throw new Error('desktop archival: SystemRoot is required to locate Windows bsdtar') + const tar = windows ? join(process.env.SystemRoot!, 'System32', 'tar.exe') : '/usr/bin/tar' + const metadata = windows ? [] : ['--no-mac-metadata', '--no-xattrs'] + try { + execFileSync(tar, ['--format', 'zip', '--options', 'zip:compression=store', ...metadata, '-cf', path, '--', ...files], { cwd: directory, stdio: 'inherit' }) + } catch (cause) { + throw new Error(`desktop archival: ZIP creation failed with ${tar}. Check system bsdtar availability, its stderr and free space in RUNNER_TEMP.`, { cause }) + } await appendFile(output, `path=${path}\n`) } catch (error) { process.stderr.write(`${JSON.stringify(desktopPublicationFailure(error, 'artifact archival'))}\n`) diff --git a/apps/desktop/tests/retain-desktop-ci.spec.ts b/apps/desktop/tests/retain-desktop-ci.spec.ts index cf71b12789..7bbd574670 100644 --- a/apps/desktop/tests/retain-desktop-ci.spec.ts +++ b/apps/desktop/tests/retain-desktop-ci.spec.ts @@ -1,7 +1,8 @@ -import { execFileSync } from 'node:child_process' +import { execFileSync, spawnSync } from 'node:child_process' import { mkdtemp, mkdir, readFile, rm, utimes, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { unzipSync } from 'fflate' import { afterEach, expect, it } from 'vitest' import { desktopReleasesRoot, retainDesktopCi, resolveDesktopRetentionRunner } from '../scripts/retain-desktop-ci.ts' import { verifyRetainedDesktopRelease } from '../scripts/desktop-release-store.ts' @@ -11,9 +12,7 @@ import { prepareDesktopPublication } from '../scripts/publish-desktop-ci.ts' const roots: string[] = [] afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) }) -it.each([ - ['mac-arm64', 'notarized'], ['mac-x64', 'notarized'], ['mac-x64', 'unnotarized'], ['win-x64', 'signed'], ['mac-arm64', 'unnotarized'], ['win-x64', 'unsigned'], -] as const)('retains the complete %s %s release without signing or upload credentials', async (target, qualification) => { +async function retainedFixture(target: 'win-x64' | 'mac-x64' | 'mac-arm64', qualification: 'unsigned' | 'signed' | 'notarized' | 'unnotarized') { const root = await mkdtemp(join(tmpdir(), 'retain-ci-')) roots.push(root) const appRoot = join(root, 'checkout') @@ -40,34 +39,63 @@ it.each([ const directory = await retainDesktopCi(env, appRoot) const archiveEnv = { ...env, GITHUB_RUN_ATTEMPT: '3', DESKTOP_RETAINED_DIRECTORY: directory, DESKTOP_PACKAGE_ATTEMPT: '2', DESKTOP_PACKAGE_RUNNER: env.RUNNER_NAME } - if (target === 'win-x64' && qualification === 'signed' && ['darwin', 'win32'].includes(process.platform)) { - const timestamp = Date.UTC(2026, 9, 23, 8, 2, 43) / 1000 - await utimes(join(directory, 'manifest.json'), timestamp, timestamp) - const output = join(root, 'archive-output') - execFileSync(process.execPath, ['--import', 'tsx', join(import.meta.dirname, '../scripts/archive-desktop-ci.ts')], { - env: { ...process.env, ...archiveEnv, DESKTOP_ARCHIVE_NAME: 'fixture.zip', GITHUB_OUTPUT: output, RUNNER_TEMP: root }, - }) - const path = (await readFile(output, 'utf8')).trim().slice('path='.length) - const listing = execFileSync(process.platform === 'win32' ? 'python' : 'python3', ['-c', ` -import datetime, json, struct, sys, zipfile -with zipfile.ZipFile(sys.argv[1]) as archive: - entry = archive.getinfo('manifest.json') - seconds = int(sys.argv[2]) - assert entry.date_time == datetime.datetime.fromtimestamp(seconds - 1).timetuple()[:6] - extra = entry.extra - while extra: - tag, size = struct.unpack_from(' { + const { root, directory, archiveEnv, names } = await retainedFixture('win-x64', 'signed') + const timestamp = Date.UTC(2026, 9, 23, 8, 2, 43) / 1000 + await utimes(join(directory, 'manifest.json'), timestamp, timestamp) + const output = join(root, 'archive-output') + execFileSync(process.execPath, ['--import', 'tsx', archiveScript], { + env: { ...process.env, ...archiveEnv, TZ: 'TST-3', DESKTOP_ARCHIVE_NAME: 'fixture.zip', GITHUB_OUTPUT: output, RUNNER_TEMP: root }, + }) + const path = (await readFile(output, 'utf8')).trim().slice('path='.length) + const bytes = await readFile(path) + const entries = unzipSync(bytes) + expect(Object.keys(entries)).toEqual(['manifest.json', ...names]) + for (const [name, content] of Object.entries(entries)) expect(Buffer.from(content)).toEqual(await readFile(join(directory, name))) + // The first central directory entry describes manifest.json. UTC+3 is 11:02:43 on every test host. + const directoryOffset = bytes.readUInt32LE(bytes.length - 22 + 16) + expect(bytes.readUInt32LE(directoryOffset)).toBe(0x02014b50) + const dosTime = bytes.readUInt16LE(directoryOffset + 12) + expect([dosTime >>> 11, (dosTime >>> 5) & 63, (dosTime & 31) * 2]).toEqual([11, 2, 42]) + const start = directoryOffset + 46 + bytes.readUInt16LE(directoryOffset + 28) + const end = start + bytes.readUInt16LE(directoryOffset + 30) + let unixTime: number | undefined + for (let offset = start; offset < end; offset += 4 + bytes.readUInt16LE(offset + 2)) { + if (bytes.readUInt16LE(offset) === 0x5455 && (bytes[offset + 4]! & 1)) unixTime = bytes.readUInt32LE(offset + 5) } + expect(unixTime).toBe(timestamp) +}) + +it.runIf(process.platform === 'darwin' || process.platform === 'win32')('reports a ZIP creation failure without emitting an upload path', async () => { + const { root, archiveEnv } = await retainedFixture('win-x64', 'signed') + const output = join(root, 'archive-output') + const result = spawnSync(process.execPath, ['--import', 'tsx', archiveScript], { encoding: 'utf8', + env: { ...process.env, ...archiveEnv, RUNNER_TEMP: root, GITHUB_OUTPUT: output, DESKTOP_ARCHIVE_NAME: 'a'.repeat(256) + '.zip' } }) + expect(result.status).toBe(1) + expect(result.stderr).toContain('desktop archival: ZIP creation failed') + await expect(readFile(output)).rejects.toMatchObject({ code: 'ENOENT' }) +}) + +it.each(['../escape.zip', '-option.zip', 'missing-extension', 'missing-output'])('rejects invalid archive settings %s without emitting an upload path', async (name) => { + const { root, archiveEnv } = await retainedFixture('win-x64', 'signed') + const output = join(root, 'archive-output') + const result = spawnSync(process.execPath, ['--import', 'tsx', archiveScript], { encoding: 'utf8', + env: { ...process.env, ...archiveEnv, RUNNER_TEMP: root, GITHUB_OUTPUT: name === 'missing-output' ? '' : output, + DESKTOP_ARCHIVE_NAME: name === 'missing-output' ? 'fixture.zip' : name } }) + expect(result.status).toBe(1) + expect(result.stderr).toContain('desktop archival: invalid ZIP output settings') + await expect(readFile(output)).rejects.toMatchObject({ code: 'ENOENT' }) +}) + +it.each([ + ['mac-arm64', 'notarized'], ['mac-x64', 'notarized'], ['mac-x64', 'unnotarized'], ['win-x64', 'signed'], ['mac-arm64', 'unnotarized'], ['win-x64', 'unsigned'], +] as const)('retains the complete %s %s release without signing or upload credentials', async (target, qualification) => { + const { debug, names, env, archiveEnv } = await retainedFixture(target, qualification) await verifyDesktopArchive(archiveEnv) await expect(verifyDesktopArchive({ ...archiveEnv, RUNNER_NAME: 'other' })).rejects.toThrow('wrong original machine') await expect(verifyDesktopArchive({ ...archiveEnv, DESKTOP_PACKAGE_ATTEMPT: '3' })).rejects.toThrow('original build') diff --git a/scripts/macos-package-workflow.spec.ts b/scripts/macos-package-workflow.spec.ts index a185c5b4ec..8f079c017a 100644 --- a/scripts/macos-package-workflow.spec.ts +++ b/scripts/macos-package-workflow.spec.ts @@ -59,7 +59,6 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { } } }) const steps = workflow.jobs.package!.steps expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2) - expect(workflow.jobs['upload-artifact']!.env.DESKTOP_ARCHIVE_NAME).toContain('${{ needs.package.outputs.mode }}') }) it('retains build inputs outside checkout and run-specific pnpm installations', () => { @@ -94,7 +93,7 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { const builds = job.steps.filter(step => step.run?.includes('pnpm run package:desktop:mac:')) expect(builds).toHaveLength(2) for (const step of builds) expect(step.run).toContain(`package:desktop:mac:${arch}`) - expect(workflow.jobs['upload-artifact']!.env.DESKTOP_ARCHIVE_NAME).toContain(`macos-${arch}-`) + expect(archive.env.DESKTOP_ARCHIVE_NAME).toBe('macos-' + arch + '-${{ needs.package.outputs.mode }}-${{ needs.package.outputs.version }}-${{ github.run_id }}-${{ needs.package.outputs.attempt }}.zip') if (arch === 'x64') { const rosetta = job.steps.findIndex(step => step.run === 'arch -x86_64 /usr/bin/true') expect(rosetta).toBeGreaterThan(-1) From e2e726458573b4358d767c89b02ccb65f607f486 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 12:58:15 +0800 Subject: [PATCH 4/9] fix(desktop): isolate archive tests and clarify retained timestamps --- apps/desktop/packaging/README.i18n.yaml | 8 ++++---- apps/desktop/packaging/README.md | 4 ++-- apps/desktop/packaging/README.zh.md | 4 ++-- apps/desktop/scripts/archive-desktop-ci.ts | 2 +- apps/desktop/tests/retain-desktop-ci.spec.ts | 5 ++++- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 0589a36c5e..27d558a343 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,11 +24,11 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 4094eeea728af143 - zh: 746384f34d3cb969 + en: 748f7ae145c98ed6 + zh: c91ee5ffaeaa77df /desktop-packaging/macos-self-hosted-packaging: - en: 239d5ab433020745 - zh: e8916ba0f4682ae1 + en: 004642a4a84a8ad5 + zh: 807a4517785a5153 /desktop-packaging/unsigned-windows-test-installer: en: f429bc0c280e2dfa zh: 4adbcc9e906bdf80 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 93214bd994..685e1b0cba 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -115,7 +115,7 @@ Select the workflow in Actions, choose the source branch, preset, signing mode a 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. -Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs store the runner’s local modification time and the Unix modification timestamp for each entry. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. +Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time in local DOS and absolute Unix fields; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. @@ -127,7 +127,7 @@ Installer uploads store the already-compressed release files without ZIP recompr ## macOS self-hosted packaging -The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. +The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. Artifacts are named `macos-----.zip`, with `arch` set to `arm64` or `x64`; entry timestamps describe retained copies in local DOS and absolute Unix fields, not necessarily the original build time. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. Both macOS workflows require the repository variable `DESKTOP_PUBLISH_RUNNERS`, including test and unnotarized modes. Their independent `upload-artifact` job retains installer names with the original build attempt; if archival fails after packaging succeeds, rerun only that job. See [original-machine routing and recovery](../publishing/ci/README.md#recovery-and-records). diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 383e342637..7f13f20528 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -115,7 +115,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 所有 Windows 打包模式(含 test 和 unsigned)都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`;触发前配置[原机器路由](../publishing/ci/README.zh.md#machine-setup)。 -在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 同时记录各文件的 runner 本地修改时间和 Unix 修改时间戳。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 +在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用本地 DOS 和绝对 Unix 字段记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 @@ -127,7 +127,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s ## macOS 自托管打包 -[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 +[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。产物名为 `macos-----.zip`,其中 `arch` 为 `arm64` 或 `x64`;条目时间以本地 DOS 和绝对 Unix 字段记录留存副本的修改时间,不一定是原始构建时间。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 两个 macOS 工作流都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`,包括 test 和未公证模式。独立 `upload-artifact` job 的安装包名称保留原构建 attempt;打包成功但归档失败时,仅重跑该 job。参见[原机器路由与恢复](../publishing/ci/README.zh.md#recovery-and-records)。 diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 7a0714efb2..7e89915de8 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -34,7 +34,7 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi const windows = process.platform === 'win32' if (windows && !process.env.SystemRoot) throw new Error('desktop archival: SystemRoot is required to locate Windows bsdtar') const tar = windows ? join(process.env.SystemRoot!, 'System32', 'tar.exe') : '/usr/bin/tar' - const metadata = windows ? [] : ['--no-mac-metadata', '--no-xattrs'] + const metadata = process.platform === 'darwin' ? ['--no-mac-metadata', '--no-xattrs'] : [] try { execFileSync(tar, ['--format', 'zip', '--options', 'zip:compression=store', ...metadata, '-cf', path, '--', ...files], { cwd: directory, stdio: 'inherit' }) } catch (cause) { diff --git a/apps/desktop/tests/retain-desktop-ci.spec.ts b/apps/desktop/tests/retain-desktop-ci.spec.ts index 7bbd574670..0da4e77972 100644 --- a/apps/desktop/tests/retain-desktop-ci.spec.ts +++ b/apps/desktop/tests/retain-desktop-ci.spec.ts @@ -29,7 +29,7 @@ async function retainedFixture(target: 'win-x64' | 'mac-x64' | 'mac-arm64', qual : [`deepseek-harness-1.0.0-${target}.dmg`, `deepseek-harness-1.0.0-${target}.zip`, `deepseek-harness-1.0.0-${target}.zip.blockmap`, `${target}-release.json`, 'nightly-mac.yml'] await Promise.all(names.map(name => writeFile(join(artifacts, name), 'fixture'))) const env = { - DSH_PACKAGING_CONFIG_DIR: config, DSH_PACKAGING_PRESET: 'test', DSH_DESKTOP_TARGET_PLATFORM: target === 'win-x64' ? 'win32' : 'darwin', + DSH_DESKTOP_RELEASES_DIR: '', DSH_PACKAGING_CONFIG_DIR: config, DSH_PACKAGING_PRESET: 'test', DSH_DESKTOP_TARGET_PLATFORM: target === 'win-x64' ? 'win32' : 'darwin', DSH_DESKTOP_TARGET_ARCH: target === 'mac-arm64' ? 'arm64' : 'x64', PACKAGE_SOURCE_REF: 'master', PACKAGE_SOURCE_SHA: 'b'.repeat(40), PACKAGE_VERSION: '1.0.0', PACKAGE_SIGNING: qualification === 'unsigned' ? 'unsigned' : 'signed', PACKAGE_SKIP_NOTARIZATION: qualification === 'unnotarized' ? 'true' : 'false', GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '2', GITHUB_STEP_SUMMARY: join(root, 'summary.md'), @@ -60,6 +60,9 @@ it.runIf(process.platform === 'darwin' || process.platform === 'win32')('stores // The first central directory entry describes manifest.json. UTC+3 is 11:02:43 on every test host. const directoryOffset = bytes.readUInt32LE(bytes.length - 22 + 16) expect(bytes.readUInt32LE(directoryOffset)).toBe(0x02014b50) + const nameLength = bytes.readUInt16LE(directoryOffset + 28) + expect(bytes.subarray(directoryOffset + 46, directoryOffset + 46 + nameLength).toString('utf8')).toBe('manifest.json') + expect(bytes.readUInt16LE(directoryOffset + 10)).toBe(0) const dosTime = bytes.readUInt16LE(directoryOffset + 12) expect([dosTime >>> 11, (dosTime >>> 5) & 63, (dosTime & 31) * 2]).toEqual([11, 2, 42]) const start = directoryOffset + 46 + bytes.readUInt16LE(directoryOffset + 28) From c5219dcfb25683b71ee72639a254dc849abb4846 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 13:38:16 +0800 Subject: [PATCH 5/9] fix(desktop): validate archive settings before retained files --- apps/desktop/packaging/README.i18n.yaml | 8 ++++---- apps/desktop/packaging/README.md | 4 ++-- apps/desktop/packaging/README.zh.md | 4 ++-- apps/desktop/scripts/archive-desktop-ci.ts | 2 +- apps/desktop/tests/retain-desktop-ci.spec.ts | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 27d558a343..cd2ea3a547 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,11 +24,11 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 748f7ae145c98ed6 - zh: c91ee5ffaeaa77df + en: 7bd0f4aa899a7295 + zh: a27c75934844123d /desktop-packaging/macos-self-hosted-packaging: - en: 004642a4a84a8ad5 - zh: 807a4517785a5153 + en: 3bd14c640893b192 + zh: 7c236cbe46be4a47 /desktop-packaging/unsigned-windows-test-installer: en: f429bc0c280e2dfa zh: 4adbcc9e906bdf80 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 685e1b0cba..f33819c47f 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -115,7 +115,7 @@ Select the workflow in Actions, choose the source branch, preset, signing mode a 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. -Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time in local DOS and absolute Unix fields; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. +Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time as local time in the DOS field and absolute time in the Unix field; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. @@ -127,7 +127,7 @@ Installer uploads store the already-compressed release files without ZIP recompr ## macOS self-hosted packaging -The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. Artifacts are named `macos-----.zip`, with `arch` set to `arm64` or `x64`; entry timestamps describe retained copies in local DOS and absolute Unix fields, not necessarily the original build time. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. +The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. Artifacts are named `macos-----.zip`, with `arch` set to `arm64` or `x64`; entry timestamps describe retained copies as local time in the DOS field and absolute time in the Unix field, this is not a guarantee of the original build time. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. Both macOS workflows require the repository variable `DESKTOP_PUBLISH_RUNNERS`, including test and unnotarized modes. Their independent `upload-artifact` job retains installer names with the original build attempt; if archival fails after packaging succeeds, rerun only that job. See [original-machine routing and recovery](../publishing/ci/README.md#recovery-and-records). diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 7f13f20528..2923d1c056 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -115,7 +115,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 所有 Windows 打包模式(含 test 和 unsigned)都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`;触发前配置[原机器路由](../publishing/ci/README.zh.md#machine-setup)。 -在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用本地 DOS 和绝对 Unix 字段记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 +在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用 DOS 字段的本地时间和 Unix 字段的绝对时间记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 @@ -127,7 +127,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s ## macOS 自托管打包 -[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。产物名为 `macos-----.zip`,其中 `arch` 为 `arm64` 或 `x64`;条目时间以本地 DOS 和绝对 Unix 字段记录留存副本的修改时间,不一定是原始构建时间。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 +[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。产物名为 `macos-----.zip`,其中 `arch` 为 `arm64` 或 `x64`;条目时间以 DOS 字段的本地时间和 Unix 字段的绝对时间记录留存副本的修改时间,不保证是原始构建时间。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 两个 macOS 工作流都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`,包括 test 和未公证模式。独立 `upload-artifact` job 的安装包名称保留原构建 attempt;打包成功但归档失败时,仅重跑该 job。参见[原机器路由与恢复](../publishing/ci/README.zh.md#recovery-and-records)。 diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 7e89915de8..4829a898a4 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -27,9 +27,9 @@ export async function verifyDesktopArchive(env: NodeJS.ProcessEnv): Promise>> 11, (dosTime >>> 5) & 63, (dosTime & 31) * 2]).toEqual([11, 2, 42]) - const start = directoryOffset + 46 + bytes.readUInt16LE(directoryOffset + 28) + const start = directoryOffset + 46 + nameLength const end = start + bytes.readUInt16LE(directoryOffset + 30) let unixTime: number | undefined for (let offset = start; offset < end; offset += 4 + bytes.readUInt16LE(offset + 2)) { @@ -89,7 +89,7 @@ it.each(['../escape.zip', '-option.zip', 'missing-extension', 'missing-output']) const output = join(root, 'archive-output') const result = spawnSync(process.execPath, ['--import', 'tsx', archiveScript], { encoding: 'utf8', env: { ...process.env, ...archiveEnv, RUNNER_TEMP: root, GITHUB_OUTPUT: name === 'missing-output' ? '' : output, - DESKTOP_ARCHIVE_NAME: name === 'missing-output' ? 'fixture.zip' : name } }) + DESKTOP_ARCHIVE_NAME: name === 'missing-output' ? 'fixture.zip' : name, RUNNER_NAME: 'wrong-machine' } }) expect(result.status).toBe(1) expect(result.stderr).toContain('desktop archival: invalid ZIP output settings') await expect(readFile(output)).rejects.toMatchObject({ code: 'ENOENT' }) From 679f3fbb2c0bfb3d45db15eb7f47cbf45853c704 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 14:18:53 +0800 Subject: [PATCH 6/9] test(desktop): expect archive settings failure before runner validation --- apps/desktop/tests/desktop-publication-failure.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/tests/desktop-publication-failure.spec.ts b/apps/desktop/tests/desktop-publication-failure.spec.ts index 80daeab539..cf92241607 100644 --- a/apps/desktop/tests/desktop-publication-failure.spec.ts +++ b/apps/desktop/tests/desktop-publication-failure.spec.ts @@ -48,11 +48,11 @@ it('omits malformed provider receipt fields and never copies raw errors into COS it('prints structured archival failure through the real CLI', () => { const result = spawnSync(process.execPath, ['--import', 'tsx/esm', fileURLToPath(new URL('../scripts/archive-desktop-ci.ts', import.meta.url))], { - encoding: 'utf8', timeout: 30_000, env: { ...process.env, RUNNER_NAME: '', DESKTOP_PACKAGE_RUNNER: '' }, + encoding: 'utf8', timeout: 30_000, env: { ...process.env, DESKTOP_ARCHIVE_NAME: '', GITHUB_OUTPUT: '', RUNNER_NAME: '', DESKTOP_PACKAGE_RUNNER: '' }, }) expect(result.error).toBeUndefined() expect(result.signal).toBeNull() expect(result.status).toBe(1) expect(JSON.parse(result.stderr)).toEqual({ phase: 'artifact archival', code: 'validation', - message: 'desktop archival: wrong original machine' }) + message: 'desktop archival: invalid ZIP output settings' }) }) From 6447accb5fd9fe8d9aa07e4de7ee80ed5f7e69e0 Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 14:28:46 +0800 Subject: [PATCH 7/9] docs(desktop): clarify retained archive naming and timestamps --- apps/desktop/packaging/README.i18n.yaml | 2 +- apps/desktop/packaging/README.md | 2 +- apps/desktop/scripts/archive-desktop-ci.ts | 1 + apps/desktop/tests/desktop-publication-failure.spec.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index cd2ea3a547..a4d6c47bbf 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -27,7 +27,7 @@ en: 7bd0f4aa899a7295 zh: a27c75934844123d /desktop-packaging/macos-self-hosted-packaging: - en: 3bd14c640893b192 + en: 08619f0e26782636 zh: 7c236cbe46be4a47 /desktop-packaging/unsigned-windows-test-installer: en: f429bc0c280e2dfa diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index f33819c47f..5518fe2876 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -127,7 +127,7 @@ Installer uploads store the already-compressed release files without ZIP recompr ## macOS self-hosted packaging -The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. Artifacts are named `macos-----.zip`, with `arch` set to `arm64` or `x64`; entry timestamps describe retained copies as local time in the DOS field and absolute time in the Unix field, this is not a guarantee of the original build time. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. +The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. Artifacts are named `macos-----.zip`, with `arch` set to `arm64` or `x64`; entry timestamps record each retained copy’s modification time as local time in the DOS field and absolute time in the Unix field; this is not a guarantee of the original build time. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. Both macOS workflows require the repository variable `DESKTOP_PUBLISH_RUNNERS`, including test and unnotarized modes. Their independent `upload-artifact` job retains installer names with the original build attempt; if archival fails after packaging succeeds, rerun only that job. See [original-machine routing and recovery](../publishing/ci/README.md#recovery-and-records). diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 4829a898a4..80d21fd61a 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -30,6 +30,7 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi const { DESKTOP_ARCHIVE_NAME: name, DESKTOP_RETAINED_DIRECTORY: directory, RUNNER_TEMP: temp, GITHUB_OUTPUT: output } = process.env if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9.+_-]*\.zip$/u.test(name) || !directory || !temp || !output) throw new Error('desktop archival: invalid ZIP output settings') const files = await verifyDesktopArchive(process.env) + // archive:false uploads use the file basename; keep it equal to the workflow's overwrite name. const path = join(await mkdtemp(join(temp, 'desktop-archive-')), name) const windows = process.platform === 'win32' if (windows && !process.env.SystemRoot) throw new Error('desktop archival: SystemRoot is required to locate Windows bsdtar') diff --git a/apps/desktop/tests/desktop-publication-failure.spec.ts b/apps/desktop/tests/desktop-publication-failure.spec.ts index cf92241607..d557c28e98 100644 --- a/apps/desktop/tests/desktop-publication-failure.spec.ts +++ b/apps/desktop/tests/desktop-publication-failure.spec.ts @@ -48,7 +48,7 @@ it('omits malformed provider receipt fields and never copies raw errors into COS it('prints structured archival failure through the real CLI', () => { const result = spawnSync(process.execPath, ['--import', 'tsx/esm', fileURLToPath(new URL('../scripts/archive-desktop-ci.ts', import.meta.url))], { - encoding: 'utf8', timeout: 30_000, env: { ...process.env, DESKTOP_ARCHIVE_NAME: '', GITHUB_OUTPUT: '', RUNNER_NAME: '', DESKTOP_PACKAGE_RUNNER: '' }, + encoding: 'utf8', timeout: 30_000, env: { ...process.env, DESKTOP_ARCHIVE_NAME: '', GITHUB_OUTPUT: '' }, }) expect(result.error).toBeUndefined() expect(result.signal).toBeNull() From 16743ca0888172dc979ad53a4a39ac4ea8edba4e Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 14:38:26 +0800 Subject: [PATCH 8/9] docs(desktop): document archive tool failure diagnostics --- apps/desktop/packaging/README.i18n.yaml | 4 ++-- apps/desktop/packaging/README.md | 2 ++ apps/desktop/packaging/README.zh.md | 2 ++ apps/desktop/scripts/archive-desktop-ci.ts | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index a4d6c47bbf..9c1b63efb1 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: 7bd0f4aa899a7295 - zh: a27c75934844123d + en: 96ad794fcdd65653 + zh: 302c869400d621e3 /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 5518fe2876..516eb31b00 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -117,6 +117,8 @@ All Windows packaging modes, including test and unsigned, require the repository Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time as local time in the DOS field and absolute time in the Unix field; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. +The archival job creates a store-mode ZIP with system bsdtar: `%SystemRoot%\System32\tar.exe` on Windows (`SystemRoot` is required), or `/usr/bin/tar` on macOS. `invalid ZIP output settings` rejects missing or invalid archive name, retained directory, temporary directory or output-file settings before file verification. `ZIP creation failed with ` identifies the failed archive tool; inspect its stderr, tool availability and free space in `RUNNER_TEMP`. If packaging succeeded, fix the archival prerequisite and rerun only `upload-artifact` using the retained installers. + For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. The dependency-install and packaging steps normalize their own shell to normal CPU, I/O and memory priority before launching children. The priority helper is copied from the exact workflow commit into a run-specific temporary directory, independently of the selected product source. A missing helper stops CI setup; unavailable priority adjustment emits a warning and marks scheduling unsuitable for performance comparison, but does not stop packaging. This does not modify the runner task or other processes. Version, preset and retained-artifact adapters remain tied to product source and its formats. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 2923d1c056..dae2faa421 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -117,6 +117,8 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用 DOS 字段的本地时间和 Unix 字段的绝对时间记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 +归档 job 使用系统 bsdtar 创建 store 模式 ZIP:Windows 为 `%SystemRoot%\System32\tar.exe`(必须设置 `SystemRoot`),macOS 为 `/usr/bin/tar`。`invalid ZIP output settings` 表示归档名称、保留目录、临时目录或输出文件设置缺失或无效,会在校验文件前拒绝。`ZIP creation failed with ` 指明失败的归档工具;检查其 stderr、工具是否可用,以及 `RUNNER_TEMP` 剩余空间。若打包已成功,修复归档前置条件后仅重跑 `upload-artifact`,复用保留的安装包。 + 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 依赖安装和打包步骤在启动子进程前,将自身 shell 的 CPU、I/O 和内存优先级调整为普通值。优先级辅助脚本从 workflow 的精确提交复制到本轮临时目录,与选中的产品源码分离。缺失辅助脚本会终止 CI 准备;无法调整优先级时明确告警并标记调度条件不适合性能对比,但继续打包。这不会修改 runner 任务或其他进程。版本、preset 和产物留存适配器仍跟随产品源码及其格式。 diff --git a/apps/desktop/scripts/archive-desktop-ci.ts b/apps/desktop/scripts/archive-desktop-ci.ts index 80d21fd61a..1517d20dd4 100644 --- a/apps/desktop/scripts/archive-desktop-ci.ts +++ b/apps/desktop/scripts/archive-desktop-ci.ts @@ -30,7 +30,7 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi const { DESKTOP_ARCHIVE_NAME: name, DESKTOP_RETAINED_DIRECTORY: directory, RUNNER_TEMP: temp, GITHUB_OUTPUT: output } = process.env if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9.+_-]*\.zip$/u.test(name) || !directory || !temp || !output) throw new Error('desktop archival: invalid ZIP output settings') const files = await verifyDesktopArchive(process.env) - // archive:false uploads use the file basename; keep it equal to the workflow's overwrite name. + // archive: false uses the file basename; keep it equal to the upload step's name used for overwrite deletion. const path = join(await mkdtemp(join(temp, 'desktop-archive-')), name) const windows = process.platform === 'win32' if (windows && !process.env.SystemRoot) throw new Error('desktop archival: SystemRoot is required to locate Windows bsdtar') From 3a51d539661fb74934141da57e2b473398d184de Mon Sep 17 00:00:00 2001 From: winewill Date: Wed, 4 Nov 2026 14:55:06 +0800 Subject: [PATCH 9/9] docs(desktop): centralize and clarify archival diagnostics --- apps/desktop/packaging/README.i18n.yaml | 4 ++-- apps/desktop/packaging/README.md | 4 +--- apps/desktop/packaging/README.zh.md | 4 +--- apps/desktop/publishing/ci/README.i18n.yaml | 4 ++-- apps/desktop/publishing/ci/README.md | 2 ++ apps/desktop/publishing/ci/README.zh.md | 2 ++ 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index 9c1b63efb1..5d25de6c86 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: 96ad794fcdd65653 - zh: 302c869400d621e3 + en: 52f0d14ee575ad1a + zh: 94a7f76f4c15031e /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 516eb31b00..7fdaa6f328 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -115,9 +115,7 @@ Select the workflow in Actions, choose the source branch, preset, signing mode a 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. -Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time as local time in the DOS field and absolute time in the Unix field; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. - -The archival job creates a store-mode ZIP with system bsdtar: `%SystemRoot%\System32\tar.exe` on Windows (`SystemRoot` is required), or `/usr/bin/tar` on macOS. `invalid ZIP output settings` rejects missing or invalid archive name, retained directory, temporary directory or output-file settings before file verification. `ZIP creation failed with ` identifies the failed archive tool; inspect its stderr, tool availability and free space in `RUNNER_TEMP`. If packaging succeeded, fix the archival prerequisite and rerun only `upload-artifact` using the retained installers. +Open the completed run in Actions and download its Artifacts from the independent `upload-artifact` job. Installer names use the original `package_run_attempt`; diagnostic names use the current `run_attempt`. If archival fails after packaging succeeds, fix the [archival prerequisites](../publishing/ci/README.md#recovery-and-records) and rerun only `upload-artifact` to reuse the retained installers. `windows-x64-signed---.zip` and `windows-x64-unsigned---.zip` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. Installer ZIPs record each retained copy’s modification time as local time in the DOS field and absolute time in the Unix field; this is not a guarantee of the original build time. The upload steps are `Retain installers for the selected mode` and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index dae2faa421..9853318b24 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -115,9 +115,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 所有 Windows 打包模式(含 test 和 unsigned)都要求仓库变量 `DESKTOP_PUBLISH_RUNNERS`;触发前配置[原机器路由](../publishing/ci/README.zh.md#machine-setup)。 -在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,仅重跑 `upload-artifact` 以复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用 DOS 字段的本地时间和 Unix 字段的绝对时间记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 - -归档 job 使用系统 bsdtar 创建 store 模式 ZIP:Windows 为 `%SystemRoot%\System32\tar.exe`(必须设置 `SystemRoot`),macOS 为 `/usr/bin/tar`。`invalid ZIP output settings` 表示归档名称、保留目录、临时目录或输出文件设置缺失或无效,会在校验文件前拒绝。`ZIP creation failed with ` 指明失败的归档工具;检查其 stderr、工具是否可用,以及 `RUNNER_TEMP` 剩余空间。若打包已成功,修复归档前置条件后仅重跑 `upload-artifact`,复用保留的安装包。 +在 Actions 打开完成的运行,下载独立 `upload-artifact` job 生成的 Artifacts。安装包名称使用原构建的 `package_run_attempt`,诊断名称使用当前 `run_attempt`。打包成功但归档失败时,修复[归档前置条件](../publishing/ci/README.zh.md#recovery-and-records)后仅重跑 `upload-artifact`,复用保留的安装包。`windows-x64-signed---.zip` 与 `windows-x64-unsigned---.zip` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。安装包 ZIP 用 DOS 字段的本地时间和 Unix 字段的绝对时间记录各留存副本的修改时间,不保证是原始构建时间。上传步骤名为 `Retain installers for the selected mode` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index 6764ae324f..3a0b48db93 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -27,8 +27,8 @@ en: 3f58698708671202 zh: be0ba215a67168d6 /desktop-ci-publication/recovery-and-records: - en: 266afba2adeeadef - zh: 063114103585dae8 + en: d6eb9cbac5b819a0 + zh: 0de1ec24055d1007 /desktop-ci-publication/restore-a-channel: en: d82e2ccf70cce397 zh: d93dcb77cc4b463e diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index 2232ea39c7..d1e5f18fa1 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -80,6 +80,8 @@ Before installing dependencies on the original runner, preparation and the appro ## Recovery and records +The archival job creates a store-mode ZIP with system bsdtar: `%SystemRoot%\System32\tar.exe` on Windows, or `/usr/bin/tar` on macOS. `invalid ZIP output settings` rejects a missing or malformed archive name and missing retained-directory, temporary-directory or `GITHUB_OUTPUT` settings before file verification. `desktop archival: SystemRoot is required to locate Windows bsdtar` reports a missing Windows system directory setting. `ZIP creation failed with ` identifies the failed archive tool; inspect its stderr, tool availability and free space in `RUNNER_TEMP`. + The packaging workflows separate `package` from `upload-artifact` (GitHub artifact archival). All packaging modes, including test and unsigned builds, require `DESKTOP_PUBLISH_RUNNERS` before packaging; it routes archival to the original machine. Rerun only `upload-artifact` after an archival failure; it verifies the original manifest and hashes without rebuilding, signing or notarizing. Packaging diagnostics are best effort and do not change package success. If an archival retry stays queued without a runner, inspect the resolved runner label and upstream package outputs before treating the runner as offline. Missing outputs require operator recovery of the original retained directory and manifest; rerunning the whole workflow rebuilds the package and is not archival-only recovery. Verify output reuse in the Actions acceptance test. Production COS publication enables website installer upload and cache refresh by default (`upload_website=true`). Clear it to publish only versioned resources and automatic-update feeds; test publication never changes the production website. `upload-website-installer` uploads the fixed installer and `purge-website-installer` refreshes its URL. The website installer validator needs only the final DMG/EXE and retained manifest. It matches production qualification, build identity and target, hashes the installer once and uploads through the same open file handle. Both production nightly and stable packages can replace the fixed website installer. Operators select the intended version; the fixed object has no downgrade protection, so rerunning an older publication can replace a newer installer. The pre-approval summary names the fixed website URL, selected version and lack of downgrade protection when website upload is selected. The existing version/update publication retains its own checks. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 99d245bf1a..0ff96d3ba3 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -80,6 +80,8 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任 ## 恢复与记录 +归档 job 使用系统 bsdtar 创建 store 模式 ZIP:Windows 为 `%SystemRoot%\System32\tar.exe`,macOS 为 `/usr/bin/tar`。`invalid ZIP output settings` 表示归档名称缺失或格式非法,或保留目录、临时目录、`GITHUB_OUTPUT` 缺失,会在校验文件前拒绝。`desktop archival: SystemRoot is required to locate Windows bsdtar` 表示缺失 Windows 系统目录设置。`ZIP creation failed with ` 指明失败的归档工具;检查其 stderr、工具是否可用,以及 `RUNNER_TEMP` 剩余空间。 + 打包 workflow 将 `package` 与 `upload-artifact`(GitHub 产物归档)分开。所有打包模式(包括 test 和 unsigned)都要求在打包前配置 `DESKTOP_PUBLISH_RUNNERS`,用于将归档路由到原机器。归档失败后仅重跑 `upload-artifact`;它校验原始 manifest 和 hash,不重新构建、签名或公证。打包诊断归档尽力执行,不改变构建成功状态。归档重跑若一直排队且未分配 runner,应先检查解析出的 runner 标签和上游 package 输出,不能直接认定机器离线。输出缺失时需由操作者从原留存目录和 manifest 恢复文件;重跑整个 workflow 会重新构建,不属于仅归档恢复。Actions 验收须验证输出复用。 生产 COS 发布默认启用官网安装包上传和缓存刷新(`upload_website=true`)。取消后仅发布版本化资源及自动更新清单;test 发布不修改生产官网。`upload-website-installer` 上传固定安装包,`purge-website-installer` 刷新该 URL。官网安装包校验器只需要最终 DMG/EXE 和保留的 manifest,匹配生产资格、构建身份及平台,只计算一次安装包 hash,并通过同一打开的文件句柄上传。production nightly 和 stable 包都可以替换官网固定安装包。操作者负责选择预期版本;固定对象没有防降级保护,重跑旧发布可能覆盖新安装包。勾选官网上传时,审批前摘要展示固定官网 URL、选定版本和无防降级保护的说明。原有版本与自动更新发布保留自身校验。