publish-path-run.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. /**
  2. * End-to-end publish-path fixture: runs the real publisher against a stub npm CLI
  3. * and a first-publication registry, so the exact argv, the package order and the
  4. * retry behaviour are observed instead of assumed.
  5. *
  6. * Usage: node publish-path-run.mjs <assets-directory> <work-directory> [--publish|--verify-only]
  7. */
  8. import fs from 'node:fs'
  9. import path from 'node:path'
  10. import { spawn } from 'node:child_process'
  11. import { createServer } from 'node:http'
  12. import { once } from 'node:events'
  13. const assets = path.resolve(process.argv[2])
  14. const work = path.resolve(process.argv[3])
  15. const mode = process.argv[4] ?? '--publish'
  16. // The caller decides whether an earlier run's registry state is kept, so a retry
  17. // sees the versions an earlier publish created.
  18. if (process.env.SCRIPTOR_KEEP_STATE !== '1' && fs.existsSync(work)) fs.rmSync(work, { recursive: true, force: true })
  19. fs.mkdirSync(work, { recursive: true })
  20. const log = path.join(work, 'invocations.jsonl')
  21. const publishState = path.join(work, 'published.json')
  22. fs.writeFileSync(log, '')
  23. const readState = () => (fs.existsSync(publishState) ? JSON.parse(fs.readFileSync(publishState, 'utf8')) : {})
  24. const registry = createServer((request, response) => {
  25. if (!['GET', 'HEAD'].includes(request.method)) { response.writeHead(405); response.end(); return }
  26. const name = decodeURIComponent(new URL(request.url, 'http://localhost').pathname.slice(1))
  27. const tarball = name.match(/^tarballs\/([A-Za-z0-9][A-Za-z0-9._-]*)$/)
  28. if (tarball) {
  29. const file = path.join(assets, tarball[1])
  30. if (!fs.existsSync(file)) { response.writeHead(404); response.end(); return }
  31. response.writeHead(200, { 'content-type': 'application/octet-stream' })
  32. response.end(fs.readFileSync(file))
  33. return
  34. }
  35. const entry = readState()[name]
  36. if (!entry) { response.writeHead(404); response.end(); return }
  37. const now = new Date().toISOString()
  38. response.writeHead(200, { 'content-type': 'application/json' })
  39. response.end(JSON.stringify({ name, 'dist-tags': { [entry.distTag]: entry.version },
  40. time: { created: now, modified: now, [entry.version]: now },
  41. versions: { [entry.version]: { name, version: entry.version,
  42. dist: { tarball: `http://127.0.0.1:${registry.address().port}/tarballs/${entry.tarball}`, integrity: entry.integrity } } } }))
  43. })
  44. registry.listen(0, '127.0.0.1', async () => {
  45. const origin = `http://127.0.0.1:${registry.address().port}/`
  46. try {
  47. // Absolute paths come from the caller so the fixture never depends on module-relative resolution.
  48. const publisher = process.env.SCRIPTOR_PUBLISHER
  49. const stub = process.env.SCRIPTOR_STUB_NPM
  50. if (!publisher || !stub || !fs.existsSync(publisher) || !fs.existsSync(stub)) {
  51. throw new Error(`publish-path fixture needs existing SCRIPTOR_PUBLISHER and SCRIPTOR_STUB_NPM: ${publisher}, ${stub}`)
  52. }
  53. const environment = {
  54. ...process.env,
  55. SCRIPTOR_NPM_REGISTRY: origin,
  56. SCRIPTOR_STUB_REGISTRY: origin,
  57. SCRIPTOR_STUB_LOG: log,
  58. SCRIPTOR_STUB_PUBLISH: publishState,
  59. NPM_CLI_ENTRY: stub,
  60. GITHUB_ACTIONS: 'true',
  61. NODE_AUTH_TOKEN: 'fixture-token',
  62. }
  63. // The publisher must refuse to upload without the Actions provenance authority.
  64. if (environment.SCRIPTOR_STRIP_GITHUB) delete environment.GITHUB_ACTIONS
  65. const child = spawn(process.execPath, [publisher, '--assets', assets, ...mode.split(' ')], {
  66. env: environment,
  67. stdio: ['ignore', 'pipe', 'pipe'],
  68. windowsHide: true,
  69. })
  70. const stdout = []
  71. const stderr = []
  72. child.stdout.on('data', chunk => stdout.push(chunk))
  73. child.stderr.on('data', chunk => stderr.push(chunk))
  74. const [code] = await once(child, 'exit')
  75. fs.writeFileSync(path.join(work, 'publisher.out'), Buffer.concat(stdout).toString('utf8'))
  76. fs.writeFileSync(path.join(work, 'publisher.err'), Buffer.concat(stderr).toString('utf8'))
  77. console.log(JSON.stringify({ code, origin, work }))
  78. } catch (error) {
  79. console.error(error.message)
  80. process.exitCode = 1
  81. } finally {
  82. registry.close()
  83. }
  84. })