mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
ci(release): pack rehearsal tarballs concurrently
This commit is contained in:
+35
-9
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { isEntry, run } from './process.ts'
|
||||
import { isEntry, runConcurrent } from './process.ts'
|
||||
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
|
||||
|
||||
/** Where pack output lands when `--out` is omitted. */
|
||||
@@ -24,8 +24,8 @@ const DEFAULT_OUTPUT = 'dist/npm'
|
||||
* @param destination - absolute output directory.
|
||||
* @returns The tarball filename.
|
||||
*/
|
||||
function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
|
||||
run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
|
||||
async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
|
||||
await runConcurrent('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
|
||||
|
||||
const filename = tarballName(member)
|
||||
const tarball = join(destination, filename)
|
||||
@@ -34,13 +34,27 @@ function packMember(family: ReleaseFamily, member: ReleaseMember, destination: s
|
||||
return filename
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The validated `--concurrency` value; 1 (the default) packs the
|
||||
* members one at a time, exactly as the credentialed publish workflows run it.
|
||||
*/
|
||||
function parseConcurrency(raw: string | undefined): number {
|
||||
if (raw === undefined) return 1
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`--concurrency must be a positive integer, got ${JSON.stringify(raw)}`)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
/** Pack the family named by `--family` into `--out`. */
|
||||
function main(): void {
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: { family: { type: 'string' }, out: { type: 'string' } },
|
||||
options: { family: { type: 'string' }, out: { type: 'string' }, concurrency: { type: 'string' } },
|
||||
allowPositionals: false,
|
||||
})
|
||||
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
|
||||
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm] [--concurrency 1]')
|
||||
const concurrency = parseConcurrency(values.concurrency)
|
||||
|
||||
const family = releaseFamily(values.family)
|
||||
const root = process.cwd()
|
||||
@@ -52,11 +66,23 @@ function main(): void {
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
mkdirSync(destination, { recursive: true })
|
||||
|
||||
const order: string[] = []
|
||||
for (const member of members) order.push(packMember(family, member, destination))
|
||||
// Members pack in a bounded pool; the recorded publish order stays the
|
||||
// members' order regardless of completion order, because each worker writes
|
||||
// its result at the member's own position.
|
||||
const order = new Array<string>(members.length)
|
||||
let cursor = 0
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => {
|
||||
while (cursor < members.length) {
|
||||
const index = cursor
|
||||
cursor += 1
|
||||
const member = members[index]
|
||||
if (member === undefined) break
|
||||
order[index] = await packMember(family, member, destination)
|
||||
}
|
||||
}))
|
||||
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
|
||||
|
||||
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
|
||||
}
|
||||
|
||||
if (isEntry(import.meta.url)) main()
|
||||
if (isEntry(import.meta.url)) await main()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
@@ -83,6 +83,26 @@ export function run(command: string, args: readonly string[], options: RunOption
|
||||
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command with inherited streams without blocking the event loop, so a
|
||||
* caller can hold several commands in flight, and fail on a non-zero exit.
|
||||
* Concurrent children interleave their output at line granularity.
|
||||
* @param command - executable name.
|
||||
* @param args - command arguments.
|
||||
* @param options - working directory and environment.
|
||||
* @returns Resolves when the command exits with status zero.
|
||||
*/
|
||||
export function runConcurrent(command: string, args: readonly string[], options: RunOptions = {}): Promise<void> {
|
||||
return new Promise((resolveRun, rejectRun) => {
|
||||
const child = spawn(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
|
||||
child.once('error', rejectRun)
|
||||
child.once('close', (status, signal) => {
|
||||
if (status === 0) resolveRun()
|
||||
else rejectRun(new Error(`${command} ${args.join(' ')} exited with ${String(status ?? signal)}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether Node started the given module as the process entry point.
|
||||
* @param moduleUrl - the caller's `import.meta.url`.
|
||||
|
||||
Reference in New Issue
Block a user