pack-release.mjs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. #!/usr/bin/env node
  2. /**
  3. * Pack every published package into release tarballs, in publish order
  4. * (platform packages first, then the entries that optionally depend on
  5. * them), and write `publish-order.txt` next to them. `pnpm pack` produces
  6. * the EXACT bytes `pnpm publish` would upload and runs each package's
  7. * `prepack` gate, so a missing binary or unbuilt `lib/` refuses here.
  8. *
  9. * Usage: `node scripts/pack-release.mjs [dest] [--current-platform-only]`.
  10. * The flag packs only THIS host's platform package plus the entries — for
  11. * per-architecture CI legs, where the other architecture's binary does not
  12. * exist (the exact refusal its prepack gate exists for).
  13. * Workflow repository metadata is projected only into disposable pack inputs;
  14. * source manifests retain the public source home. See docs/packaging.md.
  15. */
  16. import fs from 'node:fs';
  17. import os from 'node:os';
  18. import path from 'node:path';
  19. import { spawnSync } from 'node:child_process';
  20. import { entryDirs, platformDirs, readJson, root } from './repo.mjs';
  21. const args = process.argv.slice(2);
  22. const currentPlatformOnly = args.includes('--current-platform-only');
  23. const destination = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
  24. function hostPlatformDirs() {
  25. const hostPlatform = `${process.platform}-${process.arch}`;
  26. return platformDirs().filter((dir) => readJson(path.join(root, dir, 'prebuilds.json')).platform === hostPlatform);
  27. }
  28. function run(command, args, cwd) {
  29. const result = spawnSync(command, args, {
  30. cwd,
  31. stdio: 'inherit',
  32. });
  33. if (result.error) throw result.error;
  34. if (result.status !== 0) {
  35. throw new Error(`${command} failed (status=${result.status}, signal=${result.signal})`);
  36. }
  37. }
  38. function tarballName(manifest) {
  39. if (manifest.name.startsWith('@')) {
  40. return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
  41. }
  42. return `${manifest.name}-${manifest.version}.tgz`;
  43. }
  44. /** Repository identity npm verifies against the workflow's OIDC claims. */
  45. function workflowRepositoryUrl() {
  46. const repository = process.env.GITHUB_REPOSITORY;
  47. if (repository === undefined && process.env.GITHUB_ACTIONS !== 'true') return undefined;
  48. if (repository === undefined || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repository)) {
  49. throw new Error('GITHUB_REPOSITORY must identify the workflow owner/repository');
  50. }
  51. const server = new URL(process.env.GITHUB_SERVER_URL || 'https://github.com');
  52. if (server.protocol !== 'https:' || server.pathname !== '/' || server.search || server.hash
  53. || server.username || server.password) {
  54. throw new Error('GITHUB_SERVER_URL must be an HTTPS origin');
  55. }
  56. return `git+${server.origin}/${repository}.git`;
  57. }
  58. /** Copy pack inputs while keeping source manifests and completed tarballs untouched. */
  59. function stagePackages(staging, repositoryUrl) {
  60. for (const directory of ['packages', 'scripts']) {
  61. fs.cpSync(path.join(root, directory), path.join(staging, directory), {
  62. recursive: true,
  63. verbatimSymlinks: true,
  64. });
  65. }
  66. fs.copyFileSync(path.join(root, 'package.json'), path.join(staging, 'package.json'));
  67. // pnpm includes the repository workspace license when an entry has no package-local license.
  68. fs.copyFileSync(path.resolve(root, '../../LICENSE'), path.join(staging, 'LICENSE'));
  69. fs.writeFileSync(path.join(staging, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n');
  70. for (const dir of [...platformDirs(), ...entryDirs()]) {
  71. const manifestPath = path.join(staging, dir, 'package.json');
  72. const manifest = readJson(manifestPath);
  73. manifest.repository = { ...manifest.repository, url: repositoryUrl };
  74. fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
  75. }
  76. }
  77. const repositoryUrl = workflowRepositoryUrl();
  78. const staging = repositoryUrl === undefined ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), 'native-system-pack-'));
  79. try {
  80. if (staging !== undefined) stagePackages(staging, repositoryUrl);
  81. const packRoot = staging ?? root;
  82. fs.rmSync(destination, { recursive: true, force: true });
  83. fs.mkdirSync(destination, { recursive: true });
  84. const dirs = [...(currentPlatformOnly ? hostPlatformDirs() : platformDirs()), ...entryDirs()];
  85. const platformSet = new Set(platformDirs());
  86. const publishOrder = [];
  87. for (const dir of dirs) {
  88. const manifest = readJson(path.join(root, dir, 'package.json'));
  89. // Platform packages are packed with npm: pnpm pack (observed on 11.7.0)
  90. // normalizes file modes and STRIPS the executable bit, which ships a
  91. // launcher no consumer can spawn; npm pack preserves it. Platform packages
  92. // have no dependencies by construction, so they need none of pnpm's
  93. // workspace-protocol conversion — the entry packages do, and carry no
  94. // executables, so they keep pnpm pack.
  95. if (platformSet.has(dir)) {
  96. run('npm', ['pack', `./${dir}`, '--pack-destination', destination], packRoot);
  97. } else {
  98. run('pnpm', ['--dir', dir, 'pack', '--pack-destination', destination], packRoot);
  99. }
  100. const tarball = tarballName(manifest);
  101. const tarballPath = path.join(destination, tarball);
  102. if (!fs.existsSync(tarballPath)) {
  103. throw new Error(`expected pack output not found: ${tarballPath}`);
  104. }
  105. publishOrder.push(tarball);
  106. }
  107. fs.writeFileSync(path.join(destination, 'publish-order.txt'), `${publishOrder.join('\n')}\n`);
  108. console.log(`Packed ${publishOrder.length} packages into ${path.relative(root, destination)}`);
  109. } finally {
  110. if (staging !== undefined) fs.rmSync(staging, { recursive: true, force: true });
  111. }