verify-packed-install.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. /**
  2. * Install a packed release family into a throwaway consumer outside the
  3. * repository and drive its installed executable with plain Node.
  4. *
  5. * Everything the packed tarballs need comes from the tarballs themselves: the
  6. * consumer declares every member as a `file:` dependency, so the only registry
  7. * traffic is for external dependencies. What this proves is that `files`
  8. * selected a complete payload and that the published dependency ranges resolve
  9. * — a workspace link or a stale `lib/` in the checkout cannot stand in for a
  10. * missing file here
  11. * ([rationale](../../.agents/notes/proposed/process/2026-08-10-npm-release-sequences.md)).
  12. */
  13. import { spawnSync } from 'node:child_process'
  14. import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  15. import { tmpdir } from 'node:os'
  16. import { join, resolve } from 'node:path'
  17. import { pathToFileURL } from 'node:url'
  18. import { parseArgs } from 'node:util'
  19. import { releaseFamily, tarballName, type ReleaseMember } from './families.ts'
  20. /**
  21. * Environment for the installed artifact: no host Node hooks, no host DeepSeek
  22. * Harness home, and no ambient npm user agent that would confuse npm.
  23. * @param consumerRoot - the throwaway consumer directory.
  24. * @returns The child environment.
  25. */
  26. function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
  27. const environment = { ...process.env }
  28. delete environment.npm_config_user_agent
  29. delete environment.NPM_CONFIG_USER_AGENT
  30. delete environment.NODE_OPTIONS
  31. delete environment.NODE_PATH
  32. environment.DSH_HOME = resolve(consumerRoot, '.dsh')
  33. environment.DSH_AGENTS_HOME = resolve(consumerRoot, '.agents')
  34. environment.DSH_TELEMETRY_DISABLED = '1'
  35. return environment
  36. }
  37. /**
  38. * Run a command in the consumer and fail the process on a non-zero exit.
  39. * @param command - executable name.
  40. * @param args - command arguments.
  41. * @param cwd - working directory.
  42. * @param env - child environment.
  43. * @returns The captured stdout, trimmed.
  44. */
  45. function run(command: string, args: readonly string[], cwd: string, env: NodeJS.ProcessEnv): string {
  46. const result = spawnSync(command, [...args], { cwd, env, encoding: 'utf8' })
  47. if (result.error !== undefined) throw result.error
  48. if (result.status !== 0) {
  49. throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}:\n${result.stdout}\n${result.stderr}`)
  50. }
  51. return result.stdout.trim()
  52. }
  53. /** Install the family named by `--family` from `--from` and drive its entry. */
  54. function main(): void {
  55. const { values } = parseArgs({
  56. options: { family: { type: 'string' }, from: { type: 'string' } },
  57. allowPositionals: false,
  58. })
  59. if (values.family === undefined || values.from === undefined) {
  60. throw new Error('usage: verify-packed-install.ts --family <dsh|vendor> --from <packed directory>')
  61. }
  62. const family = releaseFamily(values.family)
  63. const entry = family.installedEntry
  64. const root = process.cwd()
  65. const packed = resolve(root, values.from)
  66. const members: ReleaseMember[] = family.members(root)
  67. if (entry === undefined) {
  68. console.log(`release verify-packed-install: family ${family.id} publishes no executable, nothing to drive`)
  69. return
  70. }
  71. const consumerRoot = mkdtempSync(join(tmpdir(), `dsh-packed-${family.id}-`))
  72. try {
  73. const dependencies = Object.fromEntries(members.map(member =>
  74. [member.name, pathToFileURL(join(packed, tarballName(member))).href]))
  75. writeFileSync(join(consumerRoot, 'package.json'), `${JSON.stringify({
  76. name: `dsh-packed-install-${family.id}`,
  77. version: '0.0.0',
  78. private: true,
  79. dependencies,
  80. }, null, 2)}\n`)
  81. const environment = consumerEnvironment(consumerRoot)
  82. console.log(`release verify-packed-install: installing ${String(members.length)} tarball(s) into ${consumerRoot}`)
  83. run('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false'], consumerRoot, environment)
  84. const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)
  85. const version = run(process.execPath, [bin, '--version'], consumerRoot, environment)
  86. const expected = members.find(member => member.name === entry.packageName)?.version
  87. if (version !== expected) {
  88. throw new Error(`installed ${entry.packageName} --version reported ${JSON.stringify(version)}, expected ${String(expected)}`)
  89. }
  90. console.log(`release verify-packed-install: installed ${entry.packageName} reports ${version}`)
  91. } finally {
  92. rmSync(consumerRoot, { recursive: true, force: true })
  93. }
  94. }
  95. main()