mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
fix(build): serialize release builds and give Client checks 8 GiB (#7435)
* fix(build): serialize publication and raise client aggregate heap * docs: refresh development guide translation pairing * fix(release): align pack runner and allow explicit tag-check bypass
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
# Publish the dsh release sequence to npm. This workflow is manual-only
|
||||
# (workflow_dispatch) and intentionally does not listen to pull_request or push:
|
||||
# publication must always be an explicit, reviewed act from a dsh-v* tag, and it
|
||||
# must never appear as a PR check. It repacks the current tree before publishing
|
||||
# publication is an explicit, reviewed act; dsh-v* tag validation is enabled by
|
||||
# default and may be skipped through the dispatch input. It never appears as a PR check. It repacks the current tree before publishing
|
||||
# so the bytes uploaded are exactly what this dispatch produced.
|
||||
name: Release publish (dsh)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip-tag-check:
|
||||
description: Skip release tag validation for the selected ref; other publication checks still apply.
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,7 +24,18 @@ env:
|
||||
jobs:
|
||||
pack:
|
||||
name: Pack npm tarballs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: >-
|
||||
${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
|
||||
&& github.repository == 'deepseek-harness/deepseek-harness'
|
||||
&& github.actor != 'dependabot[bot]'
|
||||
&& (github.event_name == 'workflow_dispatch'
|
||||
|| (github.event_name == 'push' && github.ref == 'refs/heads/master')
|
||||
|| (github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
&& github.event.pull_request.head.repo.fork == false
|
||||
&& github.event.pull_request.user.login != 'dependabot[bot]'))
|
||||
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
||||
|| 'ubuntu-24.04' }}
|
||||
steps:
|
||||
# Complete history: the release scripts read tags.
|
||||
- uses: actions/checkout@v7.0.1
|
||||
@@ -55,10 +72,10 @@ jobs:
|
||||
- name: Verify release version
|
||||
env:
|
||||
RELEASE_PUBLISH: 'true'
|
||||
run: pnpm run release:verify --family dsh
|
||||
run: pnpm run release:verify --family dsh ${{ inputs.skip-tag-check && '--skip-tag-check' || '' }}
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:official
|
||||
run: pnpm run build:official --serial
|
||||
|
||||
- name: Pack release tarballs
|
||||
run: pnpm run release:pack --family dsh --out dist/npm
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 3221d1076554001f
|
||||
zh: 29f166be6a5c04d9
|
||||
/development-guide/contributor-reference/typescript-project-layout:
|
||||
en: 12810e1c6ca8b8e4
|
||||
zh: b505d34cdf27856d
|
||||
en: eb79b6f8ea745a9f
|
||||
zh: de676c78231c823a
|
||||
/development-guide/contributor-reference/environment-variables:
|
||||
en: dba2c72ee6ffa930
|
||||
zh: b65c27fbb9129ec8
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ The repository uses isolated Host and Client aggregates. An ordinary package is
|
||||
| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |
|
||||
| `tsconfig.base.client.json` | Browser compiler settings (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |
|
||||
|
||||
Host aggregate type checks run with a 8 GiB Node old-space limit across build, release, and Windows diagnostic commands. This is a per-compiler limit; concurrent build steps need additional memory.
|
||||
Host and Client aggregate type checks run with an 8 GiB Node old-space limit. This is a per-compiler limit; concurrent build steps need additional memory. `pnpm run build:official --serial` prevents build steps from overlapping, even with `DSH_GATE_CONCURRENCY` set; the DSH publication workflow uses this option.
|
||||
|
||||
Host and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow:
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ pnpm run typecheck
|
||||
| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |
|
||||
| `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` 包 extends。 | 否 |
|
||||
|
||||
构建、发布和 Windows 诊断命令中的 Host aggregate 类型检查为 Node 老生代堆设置 8 GiB 上限。这是单个编译进程的上限;并发构建步骤还需要额外内存。
|
||||
Host 和 Client aggregate 类型检查为 Node 老生代堆设置 8 GiB 上限。这是单个编译进程的上限;并发构建步骤还需要额外内存。`pnpm run build:official --serial` 阻止构建步骤重叠,即使设置了 `DSH_GATE_CONCURRENCY` 也不例外;DSH 发布 workflow 使用此选项。
|
||||
|
||||
Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:
|
||||
|
||||
|
||||
+22
-7
@@ -43,10 +43,10 @@ describe('repository build CLI', () => {
|
||||
})
|
||||
|
||||
it('selects the complete build by default and one step set by scope', () => {
|
||||
expect(parseBuildArguments([])).toEqual({ scope: 'complete', profile: undefined })
|
||||
expect(parseBuildArguments(['--scope', 'artifacts', '--profile', 'official'])).toEqual({ scope: 'artifacts', profile: 'official' })
|
||||
expect(parseBuildArguments(['--scope', 'check'])).toEqual({ scope: 'check', profile: undefined })
|
||||
expect(parseBuildArguments(['--scope', 'typecheck'])).toEqual({ scope: 'typecheck', profile: undefined })
|
||||
expect(parseBuildArguments([])).toEqual({ scope: 'complete', profile: undefined, serial: false })
|
||||
expect(parseBuildArguments(['--scope', 'artifacts', '--profile', 'official'])).toEqual({ scope: 'artifacts', profile: 'official', serial: false })
|
||||
expect(parseBuildArguments(['--scope', 'check'])).toEqual({ scope: 'check', profile: undefined, serial: false })
|
||||
expect(parseBuildArguments(['--scope', 'typecheck'])).toEqual({ scope: 'typecheck', profile: undefined, serial: false })
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -60,12 +60,27 @@ describe('repository build CLI', () => {
|
||||
})
|
||||
|
||||
describe('repository build steps', () => {
|
||||
it.each(['complete', 'check', 'typecheck'] as const)('gives Host compilers an 8 GiB heap in %s scope', (scope) => {
|
||||
it('parses the explicit serial build option', () => {
|
||||
expect(parseBuildArguments(['--profile', 'official', '--serial'])).toEqual({ scope: 'complete', profile: 'official', serial: true })
|
||||
})
|
||||
|
||||
it.each(['complete', 'artifacts', 'check', 'typecheck'] as const)('chains every %s step when serial execution is selected', (scope) => {
|
||||
const original = buildGates(root, scope)
|
||||
const serial = buildGates(root, scope, {}, true)
|
||||
expect(serial.map(gate => gate.id)).toEqual(original.map(gate => gate.id))
|
||||
for (const [index, gate] of serial.entries()) {
|
||||
const previous = serial[index - 1]
|
||||
if (previous !== undefined) expect(gate.needs).toContain(previous.id)
|
||||
for (const dependency of original[index]!.needs ?? []) expect(gate.needs).toContain(dependency)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['complete', 'check', 'typecheck'] as const)('gives Host compilers and both aggregate checks an 8 GiB heap in %s scope', (scope) => {
|
||||
const gates = buildGates(root, scope)
|
||||
for (const gate of gates.filter(gate => gate.id === 'host-src' || gate.id === 'host-check')) {
|
||||
for (const gate of gates.filter(gate => gate.id === 'host-src' || gate.id === 'host-check' || gate.id === 'client-check')) {
|
||||
expect(gate.args[0]).toBe('--max-old-space-size=8192')
|
||||
}
|
||||
for (const gate of gates.filter(gate => gate.id === 'client-src' || gate.id === 'client-check')) {
|
||||
for (const gate of gates.filter(gate => gate.id === 'client-src')) {
|
||||
expect(gate.args.some(arg => arg.startsWith('--max-old-space-size'))).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
+17
-6
@@ -38,6 +38,8 @@ export interface BuildArguments {
|
||||
readonly scope: BuildScope
|
||||
/** Client build profile requested by `--profile`; `complete` and `artifacts` scopes only. */
|
||||
readonly profile: string | undefined
|
||||
/** Run build steps without overlap, including when a concurrency override is set. */
|
||||
readonly serial: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,7 @@ export function parseBuildArguments(args: readonly string[]): BuildArguments {
|
||||
options: {
|
||||
profile: { type: 'string' },
|
||||
scope: { type: 'string' },
|
||||
serial: { type: 'boolean', default: false },
|
||||
},
|
||||
allowPositionals: false,
|
||||
})
|
||||
@@ -63,7 +66,7 @@ export function parseBuildArguments(args: readonly string[]): BuildArguments {
|
||||
if ((scope === 'check' || scope === 'typecheck') && values.profile !== undefined) {
|
||||
throw new Error(`build: --profile selects artifact values and does not apply to --scope ${scope}`)
|
||||
}
|
||||
return { scope, profile: values.profile }
|
||||
return { scope, profile: values.profile, serial: values.serial }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +82,7 @@ export function parseBuildArguments(args: readonly string[]): BuildArguments {
|
||||
* @param root - Repository root containing the compiler and bundle configurations.
|
||||
* @param scope - Steps to include.
|
||||
* @param environment - Environment overlay applied to every step; the parent value of a key mapped to `undefined` is removed.
|
||||
* @param serial - Whether each step must wait for the preceding step in report order.
|
||||
* @returns The gate graph in report order.
|
||||
* @throws If an aggregate configuration is invalid.
|
||||
*/
|
||||
@@ -86,6 +90,7 @@ export function buildGates(
|
||||
root: string,
|
||||
scope: BuildScope,
|
||||
environment: Readonly<Record<string, string | undefined>> = {},
|
||||
serial = false,
|
||||
): Gate[] {
|
||||
const env = { ...environment }
|
||||
const gates: Gate[] = []
|
||||
@@ -112,6 +117,12 @@ export function buildGates(
|
||||
})
|
||||
}
|
||||
}
|
||||
if (serial) {
|
||||
return gates.map((gate, index) => {
|
||||
const previous = gates[index - 1]
|
||||
return previous === undefined ? gate : { ...gate, needs: [...new Set([...(gate.needs ?? []), previous.id])] }
|
||||
})
|
||||
}
|
||||
return gates
|
||||
}
|
||||
|
||||
@@ -153,16 +164,16 @@ async function main(): Promise<number> {
|
||||
if (!process.features.typescript) {
|
||||
throw new Error('build: Node.js TypeScript type stripping is unavailable in this Node.js process; remove --no-experimental-strip-types from NODE_OPTIONS or use a Node.js build with TypeScript support')
|
||||
}
|
||||
const { scope, profile } = parseBuildArguments(process.argv.slice(2))
|
||||
const { scope, profile, serial } = parseBuildArguments(process.argv.slice(2))
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const name = scope === 'complete' ? 'build' : scope === 'typecheck' ? 'typecheck' : `build:${scope}`
|
||||
const budget = defaultParallelism()
|
||||
const workers = (gates: readonly Gate[]) => ({
|
||||
workers: Math.min(gates.length, budget),
|
||||
source: describeDefaultParallelism(),
|
||||
workers: serial ? 1 : Math.min(gates.length, budget),
|
||||
source: serial ? '--serial' : describeDefaultParallelism(),
|
||||
})
|
||||
if (scope === 'check' || scope === 'typecheck') {
|
||||
const gates = buildGates(root, scope)
|
||||
const gates = buildGates(root, scope, {}, serial)
|
||||
return runAggregate(name, gates, workers(gates))
|
||||
}
|
||||
|
||||
@@ -170,7 +181,7 @@ async function main(): Promise<number> {
|
||||
const clientEnvironment = resolveClientBuildEnvironment(repositoryEnvironment, profile ?? process.env[CLIENT_BUILD_PROFILE_SELECTOR])
|
||||
const environment = environmentOverlay(process.env, clientBuildProcessEnvironment(process.env, clientEnvironment))
|
||||
rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true })
|
||||
const gates = buildGates(root, scope, environment)
|
||||
const gates = buildGates(root, scope, environment, serial)
|
||||
const exitCode = await runAggregate(name, gates, workers(gates))
|
||||
if (exitCode !== 0) return exitCode
|
||||
const record = writeClientBuildRecord(root, clientEnvironment)
|
||||
|
||||
@@ -94,7 +94,7 @@ export function referencedProjectsCompilerArgs(
|
||||
* @returns Arguments for `process.execPath`, run from the repository root.
|
||||
*/
|
||||
export function aggregateCheckArgs(face: 'host' | 'client'): string[] {
|
||||
return [...hostHeapArgs(face), fileURLToPath(import.meta.resolve('typescript/bin/tsc')), '-p', `tsconfig.${face}.json`]
|
||||
return ['--max-old-space-size=8192', fileURLToPath(import.meta.resolve('typescript/bin/tsc')), '-p', `tsconfig.${face}.json`]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ export function aggregateCheckArgs(face: 'host' | 'client'): string[] {
|
||||
* @returns Arguments for `process.execPath`, run from the repository root.
|
||||
*/
|
||||
export function aggregateBuildArgs(face: 'host' | 'client'): string[] {
|
||||
return [...hostHeapArgs(face), fileURLToPath(import.meta.resolve('typescript/bin/tsc')), '-b', `tsconfig.${face}.json`]
|
||||
return ['--max-old-space-size=8192', fileURLToPath(import.meta.resolve('typescript/bin/tsc')), '-b', `tsconfig.${face}.json`]
|
||||
}
|
||||
|
||||
function hostHeapArgs(face: 'host' | 'client'): string[] {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Verify a release family's version baseline, and — when publishing — that the
|
||||
* run comes from the family's tag and its members are publishable.
|
||||
*
|
||||
* Publication happens only from GitHub Actions, so the tag and publishability
|
||||
* checks are gates on the workflow, not advisory local warnings
|
||||
* Publication happens only from GitHub Actions. Publishability is mandatory;
|
||||
* tag validation may be explicitly bypassed with `--skip-tag-check`
|
||||
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
|
||||
*/
|
||||
|
||||
@@ -73,7 +73,10 @@ function verifyTag(family: ReleaseFamily, members: readonly ReleaseMember[], ref
|
||||
/** Run the verification for the family named by `--family`. */
|
||||
function main(): void {
|
||||
const { values } = parseArgs({
|
||||
options: { family: { type: 'string' } },
|
||||
options: {
|
||||
family: { type: 'string' },
|
||||
'skip-tag-check': { type: 'boolean', default: false },
|
||||
},
|
||||
allowPositionals: false,
|
||||
})
|
||||
if (values.family === undefined) throw new Error('usage: verify.ts --family <dsh|vendor>')
|
||||
@@ -95,7 +98,11 @@ function main(): void {
|
||||
const publishing = process.env.RELEASE_PUBLISH === 'true'
|
||||
if (publishing) {
|
||||
verifyPublishable(members)
|
||||
verifyTag(family, members, process.env.GITHUB_REF ?? '')
|
||||
if (values['skip-tag-check']) {
|
||||
console.warn(`release verify: WARNING tag validation explicitly skipped for ref ${process.env.GITHUB_REF || '(no ref)'}`)
|
||||
} else {
|
||||
verifyTag(family, members, process.env.GITHUB_REF ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
const versions = [...new Set(members.map(member => member.version))]
|
||||
|
||||
Reference in New Issue
Block a user