From 72c83c771a52186a4971f09b5863078d7e318ae4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:51:50 +0800 Subject: [PATCH] chore: parallelize pre-push gates --- docs/development.i18n.yaml | 4 +- docs/development.md | 2 +- docs/development.zh.md | 2 +- lefthook.yml | 58 ++++++++++++++++++-- scripts/publint-all.ts | 106 +++++++++++++++++++++++++++++++++---- 5 files changed, 153 insertions(+), 19 deletions(-) diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index cef11b8352..9de851345c 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -development.md: f032764fff29baaca007211db8b69d9a5129078f -development.zh.md: 3a650d03ce7cafd0e34290ae918e5a303c2ad8a9 +development.md: 2c39e0c531638eb0fa3561ec32e6d5c00d87a8be +development.zh.md: 6c9c2a5480db3b71a2d9e25e89c6afd34d24fdb0 diff --git a/docs/development.md b/docs/development.md index f032764fff..2c39e0c531 100644 --- a/docs/development.md +++ b/docs/development.md @@ -59,7 +59,7 @@ DEEPSEEK_BASE_URL=https://... # optional lefthook is configured in `lefthook.yml` as an early local checkpoint before review: - `pre-commit` runs staged-file ESLint fixes, `pnpm run typecheck`, and the vendor manifest guard. -- `pre-push` runs `pnpm run test`, `pnpm run test:snapshot`, `pnpm run hygiene`, `pnpm run doc-sync`, and `pnpm run verify-module-graph`. +- `pre-push` runs unit tests, snapshot tests, module-graph freshness, and the member gates of `pnpm run hygiene` and `pnpm run doc-sync` as parallel lefthook jobs. The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code. diff --git a/docs/development.zh.md b/docs/development.zh.md index 3a650d03ce..6c9c2a5480 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -59,7 +59,7 @@ DEEPSEEK_BASE_URL=https://... # optional lefthook 在 `lefthook.yml` 中配置,作为评审前的本地早期检查点: - `pre-commit` 运行对暂存文件的 ESLint 修复、`pnpm run typecheck` 和 vendor manifest 守卫。 -- `pre-push` 运行 `pnpm run test`、`pnpm run test:snapshot`、`pnpm run hygiene`、`pnpm run doc-sync` 和 `pnpm run verify-module-graph`。 +- `pre-push` 将单元测试、快照测试、module graph 新鲜度,以及 `pnpm run hygiene` 和 `pnpm run doc-sync` 的成员门禁拆成并行 lefthook job 运行。 vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。编辑 vendor 代码前先看 `vendor/README.md`。 diff --git a/lefthook.yml b/lefthook.yml index 2a255424fa..3cf1f5a034 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -28,11 +28,61 @@ pre-push: - name: snapshot run: pnpm run test:snapshot - - name: hygiene - run: pnpm run hygiene + # Flatten `pnpm run hygiene` and `pnpm run doc-sync` so lefthook can run + # their independent member gates concurrently before a push. + - name: knip + run: pnpm run knip - - name: doc-sync - run: pnpm run doc-sync + - name: publint + run: pnpm run publint + + - name: constraints + run: pnpm run constraints + + - name: node-next types + run: pnpm run verify-node-next-types + + - name: doc typecheck + run: pnpm run doc-typecheck + + - name: cordis catalog + run: pnpm run verify-cordis-catalog + + - name: tool catalog + run: pnpm run verify-tool-catalog + + - name: persistence catalog + run: pnpm run verify-persistence-catalog + + - name: doc graphs + run: pnpm run verify-doc-graphs + + - name: markdown wrap + run: pnpm run verify-md-wrap + + - name: markdown links + run: pnpm run verify-md-links + + - name: doc refs + run: pnpm run verify-doc-refs + + - name: package paths + run: pnpm run verify-package-paths + + - name: mermaid + run: pnpm run verify-mermaid + + - name: rfc classification + run: pnpm run verify-rfc-classification + + - name: type equivalence + run: pnpm run verify-type-equiv + + - name: translation pairing + run: pnpm run verify-translation-pairing + + - name: doc budgets + run: pnpm run verify-doc-budgets - name: module-graph freshness run: pnpm run verify-module-graph diff --git a/scripts/publint-all.ts b/scripts/publint-all.ts index 0e06372c3f..126943c8dd 100644 --- a/scripts/publint-all.ts +++ b/scripts/publint-all.ts @@ -1,6 +1,11 @@ -import { execFileSync } from 'node:child_process' +import { execFile } from 'node:child_process' import { existsSync, readdirSync } from 'node:fs' +import { availableParallelism } from 'node:os' import { resolve } from 'node:path' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY' // publint every harness package. Packages live at packages// // (the group dirs — core/llm/bash/… — are pure containers); vendor/ is private @@ -9,15 +14,94 @@ import { resolve } from 'node:path' const root = resolve(import.meta.dirname, '..') const packagesRoot = resolve(root, 'packages') -const packages = readdirSync(packagesRoot, { withFileTypes: true }) - .filter(group => group.isDirectory()) - .flatMap(group => - readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true }) - .filter(pkg => pkg.isDirectory()) - .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json'))) - .map(pkg => `packages/${group.name}/${pkg.name}`), - ) +type PublintResult = + | { path: string; status: 'passed'; stdout: string; stderr: string } + | { path: string; status: 'failed'; stdout: string; stderr: string; message: string } -for (const path of packages) { - execFileSync('node_modules/.bin/publint', [path], { cwd: root, stdio: 'inherit' }) +function workspacePackages(): string[] { + return readdirSync(packagesRoot, { withFileTypes: true }) + .filter(group => group.isDirectory()) + .flatMap(group => + readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true }) + .filter(pkg => pkg.isDirectory()) + .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json'))) + .map(pkg => `packages/${group.name}/${pkg.name}`), + ) } + +function publintConcurrency(total: number): number { + if (total === 0) return 0 + + const raw = process.env[CONCURRENCY_ENV] + if (raw !== undefined) { + const parsed = Number.parseInt(raw, 10) + if (!Number.isSafeInteger(parsed) || parsed < 1) { + throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`) + } + return Math.min(total, parsed) + } + + return Math.min(total, availableParallelism()) +} + +function outputText(value: unknown): string { + if (typeof value === 'string') return value + if (Buffer.isBuffer(value)) return value.toString() + return '' +} + +async function runPublint(path: string): Promise { + try { + const { stdout, stderr } = await execFileAsync('node_modules/.bin/publint', [path], { + cwd: root, + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, + }) + return { path, status: 'passed', stdout, stderr } + } catch (error: unknown) { + const failed = error as { stdout?: unknown; stderr?: unknown; message?: string } + return { + path, + status: 'failed', + stdout: outputText(failed.stdout), + stderr: outputText(failed.stderr), + message: failed.message ?? 'publint failed', + } + } +} + +async function runAll(paths: string[], concurrency: number): Promise { + let next = 0 + const results: Array = [] + await Promise.all(Array.from({ length: concurrency }, async () => { + for (;;) { + const index = next + next += 1 + const path = paths[index] + if (path === undefined) return + results[index] = await runPublint(path) + } + })) + + return paths.map((path, index) => { + const result = results[index] + if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`) + return result + }) +} + +function printResult(result: PublintResult): void { + console.log(`Running publint for ${result.path}...`) + process.stdout.write(result.stdout) + process.stderr.write(result.stderr) + if (result.status === 'failed') console.error(result.message) +} + +const packages = workspacePackages() +const concurrency = publintConcurrency(packages.length) +console.log(`publint-all: linting ${packages.length} package(s) with ${concurrency} worker(s).`) + +const results = await runAll(packages, concurrency) +for (const result of results) printResult(result) + +if (results.some(result => result.status === 'failed')) process.exit(1)