pack.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /**
  2. * Pack one release family's whole publish set into a single directory, in
  3. * publish order, and record that order for the publish step.
  4. *
  5. * The pack step is the release boundary: it runs without credentials, produces
  6. * every tarball from one commit, and hands the publish step exactly those bytes
  7. * ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
  8. */
  9. import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
  10. import { join, resolve } from 'node:path'
  11. import { parseArgs } from 'node:util'
  12. import { pnpmInvocation } from '../pnpm-invocation.ts'
  13. import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
  14. import { isEntry, runConcurrent } from './process.ts'
  15. import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
  16. /** Where pack output lands when `--out` is omitted. */
  17. const DEFAULT_OUTPUT = 'dist/npm'
  18. /**
  19. * Pack one member and check what its tarball carries.
  20. * @param family - the release family being packed.
  21. * @param member - the member to pack.
  22. * @param destination - absolute output directory.
  23. * @returns The tarball filename.
  24. */
  25. async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
  26. const invocation = pnpmInvocation(['--dir', member.directory, 'pack', '--pack-destination', destination])
  27. await runConcurrent(invocation.command, invocation.args)
  28. const filename = tarballName(member)
  29. const tarball = join(destination, filename)
  30. if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
  31. family.validatePayload(member, tarballFiles(tarball))
  32. return filename
  33. }
  34. /**
  35. * @returns The validated `--concurrency` value; 1 (the default) packs the
  36. * members one at a time, exactly as the credentialed publish workflows run it.
  37. */
  38. function parseConcurrency(raw: string | undefined): number {
  39. if (raw === undefined) return 1
  40. const parsed = Number.parseInt(raw, 10)
  41. if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
  42. throw new Error(`--concurrency must be a positive integer, got ${JSON.stringify(raw)}`)
  43. }
  44. return parsed
  45. }
  46. /** Pack the family named by `--family` into `--out`. */
  47. async function main(): Promise<void> {
  48. const { values } = parseArgs({
  49. options: { family: { type: 'string' }, out: { type: 'string' }, concurrency: { type: 'string' } },
  50. allowPositionals: false,
  51. })
  52. if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm] [--concurrency 1]')
  53. const concurrency = parseConcurrency(values.concurrency)
  54. const family = releaseFamily(values.family)
  55. const root = process.cwd()
  56. const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
  57. const members = family.publishOrder(family.members(root)).order
  58. family.verifyBuildArtifacts(root)
  59. family.verifyVersions(members)
  60. rmSync(destination, { recursive: true, force: true })
  61. mkdirSync(destination, { recursive: true })
  62. // Members pack in a bounded pool; the recorded publish order stays the
  63. // members' order regardless of completion order, because each worker writes
  64. // its result at the member's own position.
  65. const order = new Array<string>(members.length)
  66. let cursor = 0
  67. await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => {
  68. while (cursor < members.length) {
  69. const index = cursor
  70. cursor += 1
  71. const member = members[index]
  72. if (member === undefined) break
  73. order[index] = await packMember(family, member, destination)
  74. }
  75. }))
  76. writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
  77. console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
  78. }
  79. if (isEntry(import.meta.url)) await main()