deepseek-harness/scripts/release/pack.ts

89 lines
3.7 KiB
TypeScript
Raw Permalink Normal View History

/**
* Pack one release family's whole publish set into a single directory, in
* publish order, and record that order for the publish step.
*
* The pack step is the release boundary: it runs without credentials, produces
* every tarball from one commit, and hands the publish step exactly those bytes
fix(release): close the review findings on the release sequences The root manifest carries the dsh family version. bump writes it with the members, because the workspace constraint requires them to match, and that constraint now accepts a prerelease segment: without both, release:dsh 0.0.2 left the root behind and 0.0.1-rc.1 could satisfy neither check. The Landlock workflow no longer passes --access public, which overrode the restricted publishConfig this repository just adopted for those packages. Vendored change detection reads build inputs when a package publishes build output, and vendor/cordis publishes the src its export map already pointed at: its lib/ is untracked, so a real source edit read as 'nothing changed' and the next publish would fail on a version whose bytes moved. The next version also takes the last published version as its baseline, so a re-sync that restores a lower upstream version cannot recompute a version already on the registry, and bump confirms the registry carries what the newest tag names. Tag prefixes are constructed rather than recovered from a full tag, which a hyphenated version defeated. Pack runs group per ref so concurrent pull requests stop displacing each other, the publish job carries the global group, and the unused id-token permission is gone. Every release script sits behind an entry guard, which is what lets the pure judgements carry tests: tag naming, publish order and cycle reporting, version arithmetic, payload policy, and the change judgement. The Agent Note moves to implemented and states what shipped: one probe command, the registry confirmation that now exists, and byte reproducibility recorded as assumed rather than measured.
2026-08-11 01:26:36 +08:00
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*/
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, runConcurrent } from './process.ts'
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
/** Where pack output lands when `--out` is omitted. */
const DEFAULT_OUTPUT = 'dist/npm'
/**
* Pack one member and check what its tarball carries.
* @param family - the release family being packed.
* @param member - the member to pack.
* @param destination - absolute output directory.
* @returns The tarball filename.
*/
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)
if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
family.validatePayload(member, tarballFiles(tarball))
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`. */
async function main(): Promise<void> {
const { values } = parseArgs({
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] [--concurrency 1]')
const concurrency = parseConcurrency(values.concurrency)
const family = releaseFamily(values.family)
const root = process.cwd()
const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
const members = family.publishOrder(family.members(root)).order
family.verifyBuildArtifacts(root)
family.verifyVersions(members)
rmSync(destination, { recursive: true, force: true })
mkdirSync(destination, { recursive: true })
// 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)) await main()