pack.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  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/proposed/process/2026-08-10-npm-release-sequences.md)).
  8. */
  9. import { spawnSync } from 'node:child_process'
  10. import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
  11. import { join, resolve } from 'node:path'
  12. import { parseArgs } from 'node:util'
  13. import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
  14. /** Where pack output lands when `--out` is omitted. */
  15. const DEFAULT_OUTPUT = 'dist/npm'
  16. /** Name of the file the publish step reads to learn the upload order. */
  17. export const PUBLISH_ORDER_FILE = 'publish-order.txt'
  18. /**
  19. * Run a command, inheriting stdio, and fail the process on a non-zero exit.
  20. * @param command - executable name.
  21. * @param args - command arguments.
  22. */
  23. function run(command: string, args: readonly string[]): void {
  24. const result = spawnSync(command, [...args], { stdio: 'inherit' })
  25. if (result.error !== undefined) throw result.error
  26. if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
  27. }
  28. /**
  29. * List a tarball's members.
  30. * @param tarball - absolute tarball path.
  31. * @returns Every path inside the archive.
  32. */
  33. function tarballFiles(tarball: string): string[] {
  34. const result = spawnSync('tar', ['-tzf', tarball], { encoding: 'utf8' })
  35. if (result.error !== undefined) throw result.error
  36. if (result.status !== 0) throw new Error(`tar -tzf ${tarball} exited with ${String(result.status)}:\n${result.stderr}`)
  37. return result.stdout.split('\n').filter(line => line !== '')
  38. }
  39. /**
  40. * Pack one member and check what its tarball carries.
  41. * @param family - the release family being packed.
  42. * @param member - the member to pack.
  43. * @param destination - absolute output directory.
  44. * @returns The tarball filename.
  45. */
  46. function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
  47. run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
  48. const filename = tarballName(member)
  49. const tarball = join(destination, filename)
  50. if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
  51. family.validatePayload(member, tarballFiles(tarball))
  52. return filename
  53. }
  54. /** Pack the family named by `--family` into `--out`. */
  55. function main(): void {
  56. const { values } = parseArgs({
  57. options: { family: { type: 'string' }, out: { type: 'string' } },
  58. allowPositionals: false,
  59. })
  60. if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
  61. const family = releaseFamily(values.family)
  62. const root = process.cwd()
  63. const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
  64. const members = family.publishOrder(family.members(root))
  65. family.verifyVersions(members)
  66. rmSync(destination, { recursive: true, force: true })
  67. mkdirSync(destination, { recursive: true })
  68. const order: string[] = []
  69. for (const member of members) order.push(packMember(family, member, destination))
  70. writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
  71. console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
  72. }
  73. main()