fix(ci): preserve workflow concurrency and portable artifact checks

This commit is contained in:
Tianyi Cui committed 2026-10-05 12:53:52 +08:00
1 parent 853ad0bb9f
commit 536aaef0b3
22 files changed
+223 -53

No files matched your search

@@ -9,8 +9,8 @@
en: e5c984fa32cc5e69
zh: 25d5742b2106d830
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/decision:
en: 184aa812c4c857fe
zh: 953c2ec2f4e4031f
en: f4ae626ef54d10e0
zh: c2e8c30930832e12
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/alternatives-considered:
en: f61da066f9ddcd62
zh: 816fd6e0063017cd
@@ -14,6 +14,8 @@ Required Linux consumer and native Windows build jobs retain normal builds, incl
PR artifact builds compile every existing project reference from each compiler face with one serial `tsc -b` process, then run the ordinary Typert, bundling, Desktop, and Web stages. The helper reads the existing Host and Client aggregates rather than maintaining another package list. It omits only each root's additional test and script program; package diagnostics, emitted outputs, and Host-before-Client ordering remain required. Non-PR release and real-API workflow builds retain their normal checks.
CI invokes the internal build helpers directly and keeps the root package manifest unchanged. Root manifest edits activate the separate native-addon platform matrix even when only build aliases change; avoiding that trigger preserves the existing workflow set and concurrent-job ceiling without weakening native validation.
Job inventory, runner selectors, gate budgets, worker counts, and benchmark sample counts stay fixed. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order.
## Alternatives considered
@@ -14,6 +14,8 @@ PR job 需要相同的编译包来运行 benchmark、兼容性、打包和运行
PR 产物构建通过一个串行 `tsc -b` 进程编译各 compiler face 的全部现有 project reference,然后运行普通 Typert、bundling、Desktop 和 Web 阶段。helper 读取现有 Host 与 Client aggregate,而不维护另一份包清单。它只省略各 root 附加的测试和脚本 program;包诊断、emit 输出及 Host 先于 Client 的顺序仍然必需。非 PR 的 release 和真实 API 工作流构建保留普通检查。
CI 直接调用内部 build helper,并保持 root package manifest 不变。即使只修改 build alias,root manifest 改动也会激活独立的 native-addon 平台矩阵;避免这一触发条件,可以在不削弱 native 验证的情况下保留现有 workflow 集合与并发 job 上限。
job 清单、runner 选择、gate 预算、worker 数量和 benchmark sample 数量保持固定。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。
## Alternatives considered
@@ -260,7 +260,7 @@ jobs:
if: inputs.ci && github.event_name == 'pull_request'
env:
DSH_BUILD_CLIENT_PROFILE: official
run: pnpm run build:artifacts
run: pnpm run build --artifacts-only
- name: Build single-exe
env:
@@ -71,7 +71,7 @@ jobs:
# build:preview packs the image through the packer's installed bin
# (lib/bin.js), so neither half exists before the full build runs.
- name: Build workspace
run: pnpm run build:artifacts
run: pnpm run build --artifacts-only
- name: Build the preview page and pack the VFS image
env:
+1 -1
View File
@@ -107,7 +107,7 @@ jobs:
# the built artifact under plain Node, resolving plugins through real package
# exports — the shape a real consumer runs. That requires a prior build.
- name: Build (lib for the e2e example bins)
run: pnpm run ${{ github.event_name == 'pull_request' && 'build:artifacts --profile official' || 'build:official' }}
run: ${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }}
- name: Prepare Office runtime
run: pnpm run prepare:primary-runtime --target linux-x64 --output .cache/e2e-office
+1 -1
View File
@@ -89,7 +89,7 @@ jobs:
# The vendored packages publish their own sources and build outputs; the
# host build produces what their manifests select.
- name: Build
run: pnpm run ${{ github.event_name == 'pull_request' && 'build:lib:host:artifacts' || 'build:lib:host' }}
run: ${{ github.event_name == 'pull_request' && 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries' || 'pnpm run build:lib:host' }}
# Concurrency here is rehearsal-only: the credentialed publish workflows
# invoke release:pack without the flag and keep the strictly serial path.
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
run: pnpm run release:verify --family dsh
- name: Build
run: pnpm run ${{ github.event_name == 'pull_request' && 'build:artifacts --profile official' || 'build:official' }}
run: ${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }}
# Concurrency here is rehearsal-only: the credentialed publish workflows
# invoke release:pack without the flag and keep the strictly serial path.
+2 -2
View File
@@ -21,8 +21,8 @@
en: 3221d1076554001f
zh: 29f166be6a5c04d9
/development-guide/contributor-reference/typescript-project-layout:
en: b89173a1dde2f502
zh: e57bf1d7cf1e6490
en: baf046df0bea3c1a
zh: 228e218fd8fea054
/development-guide/contributor-reference/environment-variables:
en: dba2c72ee6ffa930
zh: b65c27fbb9129ec8
+1 -1
View File
@@ -85,7 +85,7 @@ tsdown --env.DSH_BUILD_FACE client
pnpm run build:web
```
`pnpm run build:artifacts` emits the same project and application artifacts while compiling only the existing package references of each compiler face. Required Linux and Windows normal builds and public `typecheck` also check the repository-wide test and script programs; PR artifact jobs reuse that ownership ([rationale](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md)).
`pnpm run build --artifacts-only` emits the same project and application artifacts while compiling only the existing package references of each compiler face. Required Linux and Windows normal builds and public `typecheck` also check the repository-wide test and script programs; PR artifact jobs reuse that ownership ([rationale](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md)).
Both tsdown passes match `vendor/*`, `packages/*/*`, and `apps/cli`; the Host pass also matches `apps/desktop-host`. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase. Tsdown builds the matched workspace members concurrently, so `apps/desktop`, whose main bundle inlines workspace devDependencies from their `lib/` output, bundles in its own step after the Host pass ([Desktop README](../apps/desktop/README.md#bundled-workspace-dependencies)).
+1 -1
View File
@@ -89,7 +89,7 @@ tsdown --env.DSH_BUILD_FACE client
pnpm run build:web
```
`pnpm run build:artifacts` 产出相同的 project 和应用产物,但只编译各 compiler face 的现有包 reference。必需的 Linux 与 Windows 普通构建以及公开 `typecheck` 还检查仓库级测试和脚本 program;PR 产物 job 复用这一职责划分([理由](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md))。
`pnpm run build --artifacts-only` 产出相同的 project 和应用产物,但只编译各 compiler face 的现有包 reference。必需的 Linux 与 Windows 普通构建以及公开 `typecheck` 还检查仓库级测试和脚本 program;PR 产物 job 复用这一职责划分([理由](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md))。
两次 tsdown 都匹配 `vendor/*`、`packages/*/*` 与 `apps/cli`,Host 阶段另外匹配 `apps/desktop-host`;两者都不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。tsdown 并发构建匹配到的 workspace 成员;`apps/desktop` 的主进程 bundle 需要从工作区 devDependencies 的 `lib/` 产物内联这些包,因此在 Host 阶段之后单独一步打 bundle([Desktop README](../apps/desktop/README.zh.md#bundled-workspace-dependencies))。
+1 -5
View File
@@ -19,15 +19,11 @@
"scripts": {
"prepare:primary-runtime": "tsx scripts/primary-runtime/prepare.ts",
"build": "tsx scripts/build.ts",
"build:artifacts": "tsx scripts/build.ts --artifacts-only",
"build:bench": "npm run build:native-system && npm run build:lib:artifacts && tsdown --config-loader native --config benchmarks/tsdown.config.ts",
"build:bench": "npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts",
"build:official": "tsx scripts/build.ts --profile official",
"build:lib": "pnpm run build:lib:host && pnpm run build:lib:client",
"build:lib:host": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -b tsconfig.host.json && tsdown --config-loader native --env.DSH_BUILD_FACE host && pnpm --filter @deepseek-ai/dsh-desktop run bundle",
"build:lib:client": "tsc -b tsconfig.client.json && tsdown --config-loader native --env.DSH_BUILD_FACE client",
"build:lib:artifacts": "pnpm run build:lib:host:artifacts && pnpm run build:lib:client:artifacts",
"build:lib:host:artifacts": "tsx scripts/compile-referenced-projects.ts host && tsdown --config-loader native --env.DSH_BUILD_FACE host && pnpm --filter @deepseek-ai/dsh-desktop run bundle",
"build:lib:client:artifacts": "tsx scripts/compile-referenced-projects.ts client && tsdown --config-loader native --env.DSH_BUILD_FACE client",
"build:web": "pnpm --filter @deepseek-ai/dsh-web-frontend run build",
"build:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run build",
"dev:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run dev",
+14 -5
View File
@@ -14,9 +14,9 @@ import {
} from './client-build-environment.ts'
import { pnpmInvocation } from './pnpm-invocation.ts'
/** Run one package script through the package manager that invoked this build. */
function runScript(script: string, environment: NodeJS.ProcessEnv): void {
const invocation = pnpmInvocation(['run', script], environment)
/** Run one package-manager command with the selected public build environment. */
function runCommand(args: readonly string[], environment: NodeJS.ProcessEnv, label: string): void {
const invocation = pnpmInvocation(args, environment)
const result = spawnSync(invocation.command, invocation.args, {
cwd: resolve(import.meta.dirname, '..'),
env: environment,
@@ -24,10 +24,15 @@ function runScript(script: string, environment: NodeJS.ProcessEnv): void {
})
if (result.error !== undefined) throw result.error
if (result.status !== 0) {
throw new Error(`build: ${script} exited with ${String(result.status ?? result.signal)}`)
throw new Error(`build: ${label} exited with ${String(result.status ?? result.signal)}`)
}
}
/** Run one package script through the package manager that invoked this build. */
function runScript(script: string, environment: NodeJS.ProcessEnv): void {
runCommand(['run', script], environment, script)
}
/** Build client artifacts and optionally omit the repository test and script typechecks. */
function main(): void {
// tsdown.config.ts loads only through Node type stripping (`--config-loader native`); this names the cause before tsdown fails.
@@ -49,7 +54,11 @@ function main(): void {
rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true })
runScript('build:native-system', buildEnvironment)
runScript(values['artifacts-only'] ? 'build:lib:artifacts' : 'build:lib', buildEnvironment)
if (values['artifacts-only']) {
runCommand(['exec', 'tsx', 'scripts/compile-referenced-projects.ts', 'libraries'], buildEnvironment, 'artifact libraries')
} else {
runScript('build:lib', buildEnvironment)
}
runScript('build:web', buildEnvironment)
const record = writeClientBuildRecord(root, clientEnvironment)
console.log(
+9 -7
View File
@@ -37,8 +37,8 @@ describe('CI workflow', () => {
})
it('selects artifact builds only for PR release and API rehearsals', () => {
const official = "pnpm run ${{ github.event_name == 'pull_request' && 'build:artifacts --profile official' || 'build:official' }}"
const vendor = "pnpm run ${{ github.event_name == 'pull_request' && 'build:lib:host:artifacts' || 'build:lib:host' }}"
const official = "${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }}"
const vendor = "${{ github.event_name == 'pull_request' && 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries' || 'pnpm run build:lib:host' }}"
for (const [file, jobName, expected] of [
['release.yml', 'pack', official],
['release-vendor.yml', 'pack', vendor],
@@ -47,14 +47,16 @@ describe('CI workflow', () => {
const job = workflowJob(loadWorkflow('.github/workflows/' + file), jobName)
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
const step = job.steps.filter(isRecord).find(candidate => typeof candidate.run === 'string'
&& (candidate.run.includes('build:artifacts') || candidate.run.includes('build:lib:host:artifacts')))
&& (candidate.run.includes('--artifacts-only') || candidate.run.includes('host-libraries')))
expect(step?.run).toBe(expected)
for (const event of ['pull_request', 'push', 'workflow_dispatch']) {
const expression = expected.slice('pnpm run ${{ '.length, -' }}'.length)
const expression = expected.slice('${{ '.length, -' }}'.length)
const selected: unknown = runInNewContext(expression, { github: { event_name: event } }, { timeout: 1000 })
expect(selected).toBe(event === 'pull_request'
? file === 'release-vendor.yml' ? 'build:lib:host:artifacts' : 'build:artifacts --profile official'
: file === 'release-vendor.yml' ? 'build:lib:host' : 'build:official')
? file === 'release-vendor.yml'
? 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries'
: 'pnpm run build --artifacts-only --profile official'
: file === 'release-vendor.yml' ? 'pnpm run build:lib:host' : 'pnpm run build:official')
}
}
})
@@ -63,7 +65,7 @@ describe('CI workflow', () => {
const job = workflowJob(loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml'), 'build')
if (!Array.isArray(job.steps)) throw new TypeError('Python runtime builder must define steps')
const steps = job.steps.filter(isRecord)
const prepare = steps.findIndex(step => step.run === 'pnpm run build:artifacts')
const prepare = steps.findIndex(step => step.run === 'pnpm run build --artifacts-only')
const execute = steps.findIndex(step => step.name === 'Build single-exe')
expect(prepare).toBeGreaterThanOrEqual(0)
expect(execute).toBeGreaterThan(prepare)
+96 -7
View File
@@ -2,15 +2,17 @@ import { spawnSync } from 'node:child_process'
import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it, onTestFinished } from 'vitest'
import { compileReferencedProjects } from './compile-referenced-projects.ts'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { buildLibraryArtifacts, compileReferencedProjects } from './compile-referenced-projects.ts'
import { runCiBench } from './run-ci-bench.ts'
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
const compiler = fileURLToPath(import.meta.resolve('typescript/bin/tsc'))
function fixture(): { root: string; leaf: string } {
const root = mkdtempSync(join(tmpdir(), 'dsh-referenced-projects-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
onTestFinished(() => { removeFixtureSafely(root) })
const leaf = join(root, 'leaf package')
mkdirSync(join(leaf, 'src'), { recursive: true })
writeFileSync(join(root, 'package.json'), JSON.stringify({ type: 'module' }))
@@ -51,10 +53,97 @@ function artifacts(leaf: string): Record<string, string> {
return Object.fromEntries(globSync('**/*', { cwd: output })
.filter(path => statSync(join(output, path)).isFile())
.sort()
.map(path => [path, readFileSync(join(output, path)).toString('base64')]))
.map(path => [path.replaceAll('\\', '/'), readFileSync(join(output, path)).toString('base64')]))
}
function recordedCommands(root: string, failHostBundle = false): () => { args: string[]; title: string }[] {
const log = join(root, 'commands.jsonl')
const entrypoint = join(root, 'pnpm', 'bin', 'pnpm.cjs')
mkdirSync(join(root, 'pnpm', 'bin'), { recursive: true })
writeFileSync(entrypoint, [
'const { appendFileSync } = require("node:fs")',
'const args = process.argv.slice(2)',
`appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args, title: process.env.DSH_CLIENT_TITLE }) + "\\n")`,
...failHostBundle ? ['if (args.includes("host")) process.exit(7)'] : [],
].join('\n') + '\n')
vi.stubEnv('npm_execpath', entrypoint)
vi.stubEnv('DSH_CLIENT_TITLE', 'Inherited Build Title')
onTestFinished(() => { vi.unstubAllEnvs() })
return () => readFileSync(log, 'utf8').trim().split('\n')
.map(line => JSON.parse(line) as { args: string[]; title: string })
}
describe('internal artifact build order', () => {
it('resolves pnpm for a direct CLI without npm_execpath and preserves its environment', () => {
const { root } = fixture()
const marker = join(root, 'direct-cli-title.txt')
const environment: NodeJS.ProcessEnv = { ...process.env, DSH_CLIENT_TITLE: 'Direct CLI Title' }
delete environment.npm_execpath
const helper = pathToFileURL(resolve(import.meta.dirname, 'compile-referenced-projects.ts')).href
const code = `import { runPnpmCommand } from ${JSON.stringify(helper)}; runPnpmCommand(${JSON.stringify(root)}, ${JSON.stringify([
'exec', process.execPath, '-e',
`require('node:fs').writeFileSync(${JSON.stringify(marker)}, process.env.DSH_CLIENT_TITLE)`,
])})`
const result = spawnSync(process.execPath, [
'--import', fileURLToPath(import.meta.resolve('tsx/esm')),
'--input-type=module', '-e', code,
], { cwd: root, env: environment, encoding: 'utf8' })
expect(result.status, result.stdout + result.stderr).toBe(0)
expect(readFileSync(marker, 'utf8')).toBe('Direct CLI Title')
})
it.each([true, false])('preserves bundler order and the public environment with hostOnly=%s', (hostOnly) => {
const { root } = fixture()
const commands = recordedCommands(root)
buildLibraryArtifacts(root, hostOnly)
const expected = [
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'],
['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'],
...hostOnly ? [] : [['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client']],
]
expect(commands()).toEqual(expected.map(args => ({ args, title: 'Inherited Build Title' })))
})
it('stops after a failed Host bundler before Desktop and Client builds', () => {
const { root } = fixture()
const commands = recordedCommands(root, true)
expect(() => { buildLibraryArtifacts(root, false) }).toThrow('exited with 7')
expect(commands()).toHaveLength(1)
})
it('prepares the original benchmark inputs in order before running its existing suite', () => {
const { root } = fixture()
const commands = recordedCommands(root)
runCiBench(root)
expect(commands()).toEqual([
['run', 'build:native-system'],
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'],
['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'],
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'],
['exec', 'tsdown', '--config-loader', 'native', '--config', 'benchmarks/tsdown.config.ts'],
['run', 'build:web'],
['run', 'test:bench:built'],
].map(args => ({ args, title: 'Inherited Build Title' })))
})
})
describe('package project compilation', () => {
it('records Windows-style glob keys without changing artifact bytes', () => {
const { leaf } = fixture()
const output = join(leaf, 'lib')
mkdirSync(join(output, 'types'), { recursive: true })
const bytes = Buffer.from([0, 255, 13, 10])
writeFileSync(join(output, 'types\\index.js'), bytes)
expect(artifacts(leaf)).toEqual({ 'types/index.js': bytes.toString('base64') })
})
it.each(['host', 'client'] as const)('preserves every %s package artifact byte', (face) => {
const { root, leaf } = fixture()
const complete = spawnSync(process.execPath, [compiler, '-b', `tsconfig.${face}.json`], {
@@ -142,7 +231,7 @@ describe('package project compilation', () => {
expect(() => { compileReferencedProjects(root, 'host') }).toThrow("Argument for '--target' option must be")
})
it('rejects an unsupported compiler face before building', () => {
it('rejects an unsupported build mode before building', () => {
const repository = resolve(import.meta.dirname, '..')
const result = spawnSync(process.execPath, [
'--import', 'tsx/esm',
@@ -151,6 +240,6 @@ describe('package project compilation', () => {
], { cwd: repository, encoding: 'utf8' })
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('expected exactly one compiler face: host or client')
expect(result.stderr).toContain('expected exactly one mode: host, client, host-libraries, or libraries')
})
})
+50 -5
View File
@@ -1,10 +1,26 @@
/** Compile package projects with their existing diagnostics and outputs, excluding the aggregate test program. */
/** Compile and bundle package projects without repeating aggregate test and script typechecks. */
import { spawnSync } from 'node:child_process'
import { relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import ts from 'typescript'
import { pnpmCommand } from './release/process.ts'
/**
* Run one package-manager command serially with the inherited build environment.
* @param root - Repository root supplying the command's working directory.
* @param args - Package-manager arguments, without shell quoting.
* @throws If the command cannot start or exits unsuccessfully.
*/
export function runPnpmCommand(root: string, args: readonly string[]): void {
const [command, ...prefix] = pnpmCommand()
const result = spawnSync(command, [...prefix, ...args], { cwd: root, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) {
throw new Error(`compile-referenced-projects: pnpm ${args.join(' ')} exited with ${String(result.status ?? result.signal)}`)
}
}
/**
* Compile one aggregate's project references with the ordinary serial TypeScript builder.
@@ -47,11 +63,40 @@ export function compileReferencedProjects(root: string, face: 'host' | 'client')
}
}
/**
* Emit package libraries in Host, Desktop, then Client dependency order.
* @param root - Repository root containing the compiler and bundle configurations.
* @param hostOnly - Omit the Client compiler and bundler after the Host artifacts are ready.
* @throws If any compiler or bundler fails.
*/
export function buildLibraryArtifacts(root: string, hostOnly: boolean): void {
compileReferencedProjects(root, 'host')
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'])
runPnpmCommand(root, ['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'])
if (hostOnly) return
compileReferencedProjects(root, 'client')
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'])
}
if (import.meta.main) {
const { positionals } = parseArgs({ allowPositionals: true })
const [face] = positionals
if (positionals.length !== 1 || (face !== 'host' && face !== 'client')) {
throw new Error('compile-referenced-projects: expected exactly one compiler face: host or client')
const [mode] = positionals
if (positionals.length !== 1) {
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
}
const root = resolve(import.meta.dirname, '..')
switch (mode) {
case 'host':
case 'client':
compileReferencedProjects(root, mode)
break
case 'host-libraries':
buildLibraryArtifacts(root, true)
break
case 'libraries':
buildLibraryArtifacts(root, false)
break
default:
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
}
compileReferencedProjects(resolve(import.meta.dirname, '..'), face)
}
+3 -3
View File
@@ -24,14 +24,14 @@ describe('PR preview workflow', () => {
expect(preview.steps.find(step => step.uses === 'actions/checkout@v6')?.with).toEqual({ 'persist-credentials': false })
})
it('keeps the immutable full build and restore-only dependency cache', () => {
it('keeps the immutable complete artifact build and restore-only dependency cache', () => {
expect(workflow.env.PRIMARY_NODE_VERSION).toBe('24')
expect(workflow.env.DSH_TELEMETRY_DISABLED).toBe('1')
const commands = preview.steps.map(step => step.run)
expect(commands).toContain('pnpm install --frozen-lockfile')
expect(commands).toContain('pnpm run build')
expect(commands).toContain('pnpm run build --artifacts-only')
expect(commands).toContain('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview')
expect(commands.indexOf('pnpm run build')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
expect(commands.indexOf('pnpm run build --artifacts-only')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
expect(preview.steps.filter(step => step.uses?.startsWith('actions/cache'))).toHaveLength(1)
expect(preview.steps.find(step => step.uses === 'actions/cache/restore@v4')?.with).toMatchObject({
key: "${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}",
+2 -1
View File
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFil
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
import { runGate, type Gate } from '../run-gates.ts'
@@ -13,7 +14,7 @@ import { packedIdentity, PUBLISH_ORDER_FILE, readPublishOrder } from './tarball.
const require = createRequire(import.meta.url)
const packScript = resolve(import.meta.dirname, 'pack.ts')
const tsxHook = require.resolve('tsx/esm')
const tsxHook = pathToFileURL(require.resolve('tsx/esm')).href
const pnpmEntry = join(dirname(require.resolve('pnpm')), 'bin/pnpm.cjs')
const roots: string[] = []
const commit = 'abcdef0'
+19
View File
@@ -0,0 +1,19 @@
/** Build the existing benchmark artifacts without repeating aggregate test and script typechecks. */
import { resolve } from 'node:path'
import { buildLibraryArtifacts, runPnpmCommand } from './compile-referenced-projects.ts'
/**
* Build native support, libraries, benchmark workers, and Web assets before running the existing benchmark suite.
* @param root - Repository root containing all build configurations.
* @throws If any build or the benchmark suite fails.
*/
export function runCiBench(root: string): void {
runPnpmCommand(root, ['run', 'build:native-system'])
buildLibraryArtifacts(root, false)
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--config', 'benchmarks/tsdown.config.ts'])
runPnpmCommand(root, ['run', 'build:web'])
runPnpmCommand(root, ['run', 'test:bench:built'])
}
if (import.meta.main) runCiBench(resolve(import.meta.dirname, '..'))
+8 -5
View File
@@ -250,7 +250,7 @@ describe('gate graph validation', () => {
await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
})
it('builds the native addon before benchmarks through the ci-bench script chain', () => {
it('uses the internal CI benchmark runner without changing the public benchmark scripts', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-bench'))
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
scripts: Record<string, string>
@@ -260,12 +260,12 @@ describe('gate graph validation', () => {
expect(subject).toHaveLength(1)
expect(subject[0]).toMatchObject({
id: 'bench',
displayCommand: 'pnpm run test:bench',
args: ['/private/pnpm.cjs', 'run', 'test:bench'],
displayCommand: 'pnpm exec tsx scripts/run-ci-bench.ts',
args: ['/private/pnpm.cjs', 'exec', 'tsx', 'scripts/run-ci-bench.ts'],
})
expect(scripts['test:bench']).toBe('npm run build:bench && npm run build:web && npm run test:bench:built')
expect(scripts['build:bench']).toBe(
'npm run build:native-system && npm run build:lib:artifacts && tsdown --config-loader native --config benchmarks/tsdown.config.ts',
'npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts',
)
expect(scripts['build:native-system']).toBe('tsx native/system/scripts/build.ts --host-addon-only')
expect(scripts['test:bench:built']).toBe('vitest run --config vitest.bench.config.ts')
@@ -841,7 +841,10 @@ describe('Node compatibility graph', () => {
withEnv('DSH_NODE_COMPAT_SKIP_TYPECHECK', skipTypecheck, () =>
withPnpmEntrypoint(() => gatesForMode('node-compat'))))
expect(subject.filter(item => item.displayCommand === 'pnpm run build:artifacts')).toHaveLength(1)
expect(subject.filter(item => item.displayCommand === 'pnpm run build --artifacts-only')).toHaveLength(1)
expect(subject.find(item => item.id === 'build')?.args).toEqual([
'/private/pnpm.cjs', 'run', 'build', '--artifacts-only',
])
expect(subject.map(item => item.id)).not.toContain('build:web')
expect(subject.find(item => item.id === 'cli-lazy-search-startup-smoke')).toMatchObject({
needs: ['build'],
+4 -2
View File
@@ -285,7 +285,7 @@ export function gatesForMode(selected: Mode): Gate[] {
case 'ci-unit':
return ciUnitGates()
case 'ci-bench':
return [pnpmScript('bench', 'test:bench', { label: 'performance benchmarks' })]
return [pnpmExec('bench', ['tsx', 'scripts/run-ci-bench.ts'], { label: 'performance benchmarks' })]
case 'ci-snapshot':
return [ciBuildGate(), snapshotGate()]
case 'ci-artifacts':
@@ -407,7 +407,9 @@ function nodeCompatGates(): Gate[] {
}
return [
...typecheck,
pnpmScript('build', 'build:artifacts', {
pnpmScript('build', 'build', {
...pnpmInvocation(['run', 'build', '--artifacts-only']),
displayCommand: 'pnpm run build --artifacts-only',
...typecheck.length === 0 ? {} : { needs: ['typecheck'] },
}),
...nodeCompatSmokeGates({ cliSmoke: true }),
+2 -2
View File
@@ -147,8 +147,8 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
expect(job.steps[0]?.with?.['fetch-depth']).toBe(0)
expect(commands).toContain('pnpm run release:verify --family ' + family)
expect(commands).toContain(family === 'dsh'
? "pnpm run ${{ github.event_name == 'pull_request' && 'build:artifacts --profile official' || 'build:official' }}"
: "pnpm run ${{ github.event_name == 'pull_request' && 'build:lib:host:artifacts' || 'build:lib:host' }}")
? "${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }}"
: "${{ github.event_name == 'pull_request' && 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries' || 'pnpm run build:lib:host' }}")
expect(commands).toContain('pnpm run release:pack --family ' + family + ' --out ' + output + ' --concurrency 8')
expect(commands).toContain('pnpm run release:verify-packed-install --family ' + family + ' --from ' + output
+ (family === 'dsh' ? ' --from dist/npm-vendor --from dist/npm-landlock' : ''))