From f4260fc2f782122de7499d2e1fd22105ac70498b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:31:23 +0800 Subject: [PATCH] test(desktop): cover unsigned packaging failure and cleanup --- apps/desktop/packaging/README.i18n.yaml | 6 ++--- apps/desktop/packaging/README.md | 4 +-- apps/desktop/packaging/README.zh.md | 2 +- .../desktop/tests/pack-local-packages.spec.ts | 24 ++++++++++++------ .../tests/package-target-stages.spec.ts | 25 ++++++++++++++++--- scripts/client-build-environment.ts | 2 ++ scripts/release/pack.ts | 10 ++++---- 7 files changed, 52 insertions(+), 21 deletions(-) diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index c76934d8ce..893b7eb015 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -21,7 +21,7 @@ en: 3f38ab26ee195b39 zh: f00eddafa8f3b9ca /desktop-packaging/configuration-and-local-builds: - en: 67e67bcc867b1814 + en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: en: e1b101593f6f755e @@ -39,8 +39,8 @@ en: 62c125856aa03c26 zh: 9fc00a43c8b1e8e2 /desktop-packaging/directory-builds-and-preparation: - en: 30d58517c9c2044f - zh: 16d26bdc923fc4ba + en: 5860c306375ddd63 + zh: a76e3cdbbe25c79e /desktop-packaging/local-official-bundles-from-a-loopback-registry: en: b42f103358d0dfd7 zh: a534164ad9eb00da diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 4fa4f564bf..250e78adc5 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -67,7 +67,7 @@ The installer embeds `DSH_DESKTOP_UPDATE_CHANNEL` (`nightly` by default, or `lat ## Configuration and local builds -Unsigned macOS and Windows packages use the localized **DSH Local Build** label and version/commit badge from local Web builds. Signed packages keep official branding, including test deployments and macOS builds that skip notarization. Native application and installer names are unchanged. +Unsigned macOS and Windows packages use the same localized **DSH Local Build** label and version/commit badge as local Web builds. Signed packages keep official branding, including test deployments and macOS builds that skip notarization. Native application and installer names are unchanged. Set `DSH_PACKAGING_CONFIG_DIR` to the absolute machine configuration directory and `DSH_PACKAGING_PRESET` to `test` or `production` for local Windows commands. The loader reads `presets/.env`; signed commands additionally read `secrets/windows-signing.env`. Relative certificate and SignTool paths resolve beside the file declaring them. Public presets reject credentials, and the signing file cannot override public settings. Missing configuration fails without falling back to `.env.windows`; commands without a preset retain the existing dotenv behavior. Unsigned and preparation-only presets never read the signing file. Cache maintenance also reads only public preset settings. Test signed packages also require the existing download origin and release ID, but no upload credentials. Local preset commands with `--build-version auto` number from local artifacts only; Actions uses its run number and attempt. @@ -208,7 +208,7 @@ To inspect or troubleshoot the prepared host-target resources without invoking e pnpm run prepare:desktop ``` -This diagnostic command is an alternative stopping point, not the first half of a two-command build. A later `package:desktop*` command repeats the official build and preparation so it cannot consume stale dsh packages, runtime files, or dsh content. +This diagnostic command is an alternative stopping point, not the first half of a two-command build. A later `package:desktop*` command repeats the build selected by its signing mode and all preparation so it cannot consume stale dsh packages, runtime files, or dsh content. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index b2613eb23b..457889a84c 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -208,7 +208,7 @@ pnpm run package:desktop:mac:arm64:dir pnpm run prepare:desktop ``` -这条诊断命令是另一种停止位置,并非两条命令构建流程的前半段。之后执行 `package:desktop*` 时仍会重新完成正式构建与准备,避免使用陈旧的 dsh 包、运行时文件或 dsh 内容。 +这条诊断命令是另一种停止位置,并非两条命令构建流程的前半段。之后执行 `package:desktop*` 时仍会重新完成按签名模式选择的构建与全部准备,避免使用陈旧的 dsh 包、运行时文件或 dsh 内容。 diff --git a/apps/desktop/tests/pack-local-packages.spec.ts b/apps/desktop/tests/pack-local-packages.spec.ts index 5a9f464a41..14d55a0a8b 100644 --- a/apps/desktop/tests/pack-local-packages.spec.ts +++ b/apps/desktop/tests/pack-local-packages.spec.ts @@ -8,6 +8,7 @@ import { pathToFileURL } from 'node:url' import { afterEach, expect, it } from 'vitest' import { resolveClientBuildEnvironment, writeClientBuildRecord } from '../../../scripts/client-build-environment.ts' import { runGate } from '../../../scripts/run-gates.ts' +import { COVERAGE_TEST_TIMEOUT_ENV, coverageTestTimeoutOptions } from '../../../scripts/coverage-partitions.ts' import { removeFixtureSafely } from '../../../scripts/test-fixture-cleanup.ts' import { packedIdentity, PUBLISH_ORDER_FILE } from '../../../scripts/release/tarball.ts' @@ -16,6 +17,8 @@ const pnpmEntry = join(dirname(require.resolve('pnpm')), 'bin/pnpm.cjs') const tsxHook = pathToFileURL(require.resolve('tsx/esm')).href const roots: string[] = [] const metadata = { DSH_CLIENT_COMMIT_HASH: 'abcdef0', DSH_CLIENT_VERSION: '1.2.3' } +const testTimeout = Math.max(120_000, coverageTestTimeoutOptions(process.env[COVERAGE_TEST_TIMEOUT_ENV]).testTimeout ?? 0) +const activeRuns = new Map>() function write(root: string, path: string, text: string): void { mkdirSync(dirname(join(root, path)), { recursive: true }) @@ -44,23 +47,30 @@ async function pack(root: string, release = false, extra: string[] = []) { const controller = new AbortController() const timer = setTimeout(() => { controller.abort() }, 90_000) try { - const result = await runGate({ + const pending = runGate({ id: 'desktop-pack-fixture', label: 'Desktop packing', displayCommand: script, command: process.execPath, args: [pnpmEntry, '--dir', root, 'exec', process.execPath, '--import', tsxHook, script, ...(release ? ['--family', 'dsh'] : []), '--out', 'packed', '--concurrency', '2', ...extra], env: { npm_execpath: pnpmEntry, DSH_CLIENT_COMMIT_HASH: metadata.DSH_CLIENT_COMMIT_HASH }, }, controller.signal) + activeRuns.set(controller, pending) + const result = await pending expect(result.error).toBeUndefined() expect(result.aborted).toBe(false) expect(result.signalCode).toBeNull() return { code: result.exitCode, output: result.output.map(chunk => chunk.text).join('') } } finally { clearTimeout(timer) + activeRuns.delete(controller) } } -afterEach(() => { for (const root of roots.splice(0)) removeFixtureSafely(root) }) +afterEach(async () => { + for (const controller of activeRuns.keys()) controller.abort() + await Promise.all(activeRuns.values()) + for (const root of roots.splice(0)) removeFixtureSafely(root) +}, testTimeout) it('packs local client artifacts without a release publication record', async () => { const root = fixture() @@ -70,7 +80,7 @@ it('packs local client artifacts without a release publication record', async () expect(packedIdentity(join(root, 'packed/deepseek-ai-dsh-probe-1.2.3.tgz'))) .toEqual({ name: '@deepseek-ai/dsh-probe', version: '1.2.3' }) expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false) -}) +}, testTimeout) it.each(['official', 'stale', 'missing'] as const)('rejects %s client artifacts before replacing local packages', async (kind) => { const root = fixture(kind === 'official' ? 'official' : 'local') @@ -81,7 +91,7 @@ it.each(['official', 'stale', 'missing'] as const)('rejects %s client artifacts expect(result.code).not.toBe(0) expect(result.output).toMatch(kind === 'official' ? /DSH_CLIENT_BUILD_PROFILE/ : kind === 'stale' ? /artifacts differ/ : /client build record/) expect(readFileSync(join(root, 'packed/retained.txt'), 'utf8')).toBe('previous output\n') -}) +}, testTimeout) it('rejects local client artifacts in the release packing command', async () => { const root = fixture() @@ -89,7 +99,7 @@ it('rejects local client artifacts in the release packing command', async () => expect(result.code).not.toBe(0) expect(result.output).toMatch(/DSH_CLIENT_BUILD_PROFILE/) expect(existsSync(join(root, 'packed'))).toBe(false) -}) +}, testTimeout) it('validates local tarball contents before reporting success', async () => { const root = fixture() @@ -97,7 +107,7 @@ it('validates local tarball contents before reporting success', async () => { const result = await pack(root) expect(result.code).not.toBe(0) expect(result.output).toContain('publishes source file package/src/index.ts') -}) +}, testTimeout) it('rejects an invalid local pack concurrency before replacing packages', async () => { const root = fixture() @@ -105,4 +115,4 @@ it('rejects an invalid local pack concurrency before replacing packages', async expect(result.code).not.toBe(0) expect(result.output).toContain('--concurrency must be a positive integer') expect(existsSync(join(root, 'packed'))).toBe(false) -}) +}, testTimeout) diff --git a/apps/desktop/tests/package-target-stages.spec.ts b/apps/desktop/tests/package-target-stages.spec.ts index d536696003..fb8820a9ca 100644 --- a/apps/desktop/tests/package-target-stages.spec.ts +++ b/apps/desktop/tests/package-target-stages.spec.ts @@ -38,7 +38,7 @@ function supervisor(failure?: string) { const run = { directory: 'fixture-record', finish: vi.fn(), run: vi.fn(async (stage: string, _executable: string, _args: readonly string[], _options: { env: NodeJS.ProcessEnv }) => { stages.push(stage) - if (stage === failure) throw new Error('stage refused') + if (failure !== undefined && (stage === failure || stage.startsWith(`${failure} `))) throw new Error('stage refused') }) } return { run, stages } } @@ -103,13 +103,17 @@ it.each(['--unsigned', '--prepare-only'])('keeps %s hardware-free and creates no }) it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('selects branding from the signing mode for %s', async (target) => { - for (const unsigned of [false, true]) { + const modes = [[], ['--unsigned'], ['--dir'], ['--unsigned', '--dir'], ['--prepare-only'], + ...(target === 'win-x64' ? [] : [['--skip-notarization'], ['--skip-notarization', '--dir']])] + for (const flags of modes) { + const unsigned = flags.includes('--unsigned') const { run, stages } = supervisor() const platform = target === 'win-x64' ? 'win32' : 'darwin' - const invocation = parseDesktopPackageInvocation([target, ...(unsigned ? ['--unsigned'] : [])], platform, + const invocation = parseDesktopPackageInvocation([target, ...flags], platform, platform === 'darwin' ? 'arm64' : 'x64') await packageTarget(invocation, { ...environment, + APPLE_KEYCHAIN_PROFILE: 'fixture', DSH_BUILD_CLIENT_PROFILE: unsigned ? 'official' : 'local', DSH_CLIENT_BUILD_PROFILE: unsigned ? 'official' : 'local', DSH_CLIENT_TITLE: 'Inherited title', @@ -121,6 +125,21 @@ it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('selects branding from the } }) +it.each([ + ['mac-arm64', 'run build --profile local'], + ['win-x64', 'run build --profile local'], + ['mac-arm64', 'exec tsx apps/desktop/scripts/pack-local-packages.ts'], + ['win-x64', 'exec tsx apps/desktop/scripts/pack-local-packages.ts'], +] as const)('stops unsigned %s packaging when %s fails', async (target, failure) => { + const { run, stages } = supervisor(failure) + const invocation = parseDesktopPackageInvocation([target, '--unsigned'], + target === 'win-x64' ? 'win32' : 'darwin', target === 'win-x64' ? 'x64' : 'arm64') + await expect(packageTarget(invocation, environment, run)).rejects.toThrow('stage refused') + expect(stages.at(-1)?.startsWith(failure)).toBe(true) + expect(stages.some(stage => stage.startsWith('--dir apps/desktop-host pack'))).toBe(false) + expect(writeFileSync).not.toHaveBeenCalled() +}) + it('checks the assembled macOS runtime before notarizing and recording the release', async () => { const { run, stages } = supervisor() vi.mocked(packageMacOSArtifacts).mockImplementationOnce(async () => { diff --git a/scripts/client-build-environment.ts b/scripts/client-build-environment.ts index 547619751e..74021edf22 100644 --- a/scripts/client-build-environment.ts +++ b/scripts/client-build-environment.ts @@ -180,6 +180,8 @@ function clientBuildEnvironment(environment: NodeJS.ProcessEnv): ClientBuildEnvi /** * Resolve the exact public environment selected for a complete client build. + * Named profiles discard inherited public overrides. Local builds retain the supplied + * commit, version, and true dirty marker; repositoryClientBuildEnvironment supplies that metadata. * @param environment - parent process environment. * @param profile - explicit profile, or the non-public selector when omitted. * @returns the inherited public values when no profile is selected, otherwise the named profile. diff --git a/scripts/release/pack.ts b/scripts/release/pack.ts index b9a38663d7..4eb7ee9839 100644 --- a/scripts/release/pack.ts +++ b/scripts/release/pack.ts @@ -1,9 +1,9 @@ /** - * Pack one release family's whole publish set into a single directory and - * record its publish order for the publish step. Default serial packs execute + * Pack one family's public packages into a staging directory; the release command + * also records their publish order. Default serial packs execute * in that order; explicitly parallel packs use pnpm's bounded scheduler. * - * The pack step is the release boundary: it runs without credentials, produces + * The release command is the release boundary: it runs without credentials, produces * every tarball from one commit, and hands the publish step exactly those bytes * ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)). */ @@ -84,12 +84,12 @@ export function parsePackConcurrency(raw: string | undefined): number { /** * Replace a staging directory with validated family tarballs without a publication record. - * The caller must verify the client build profile and artifact hashes before calling. + * The caller must verify any client build profile and artifact hashes its packaging mode requires. * @param root - Repository containing the family members. * @param family - Package selection, version, and payload policy. * @param destination - Absolute staging directory owned by this packing invocation. * @param concurrency - Validated maximum number of concurrent package packs. - * @returns Tarball filenames in dependency order, after every package passes validation. + * @returns Tarball filenames in publish order, after every package passes validation. */ export async function packFamilyMembers( root: string, family: ReleaseFamily, destination: string, concurrency: number,