verify-release-artifact.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { spawnSync } from 'node:child_process';
  2. import {
  3. copyFileSync,
  4. mkdirSync,
  5. mkdtempSync,
  6. readdirSync,
  7. readFileSync,
  8. rmSync,
  9. writeFileSync,
  10. } from 'node:fs';
  11. import { tmpdir } from 'node:os';
  12. import path from 'node:path';
  13. import { fileURLToPath } from 'node:url';
  14. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  15. const repoRoot = path.resolve(__dirname, '..');
  16. const distDir = path.join(repoRoot, 'dist');
  17. const suspiciousPathPatterns = [
  18. /\/Users\/[^\s'"`]+(?:node_modules|oh-my-opencode-slim)[^\s'"`]*/,
  19. /\/home\/[^\s'"`]+(?:node_modules|oh-my-opencode-slim)[^\s'"`]*/,
  20. ];
  21. const suspiciousImportPatterns = [/from\s+["']vscode-jsonrpc\/node["']/];
  22. const packagedRequiredFiles = [
  23. 'package.json',
  24. 'README.md',
  25. 'LICENSE',
  26. 'dist/index.js',
  27. 'dist/index.d.ts',
  28. 'dist/cli/index.js',
  29. 'dist/divoom/council.gif',
  30. 'dist/divoom/designer.gif',
  31. 'dist/divoom/explorer.gif',
  32. 'dist/divoom/fixer.gif',
  33. 'dist/divoom/input.gif',
  34. 'dist/divoom/intro.gif',
  35. 'dist/divoom/librarian.gif',
  36. 'dist/divoom/oracle.gif',
  37. 'dist/divoom/orchestrator.gif',
  38. 'oh-my-opencode-slim.schema.json',
  39. 'src/skills/simplify/SKILL.md',
  40. 'src/skills/codemap/SKILL.md',
  41. 'src/skills/clonedeps/SKILL.md',
  42. 'src/skills/deepwork/SKILL.md',
  43. ];
  44. function fail(message: string): never {
  45. throw new Error(message);
  46. }
  47. function run(command: string, args: string[], options: { cwd?: string } = {}) {
  48. const result = spawnSync(command, args, {
  49. cwd: options.cwd ?? repoRoot,
  50. encoding: 'utf8',
  51. stdio: ['ignore', 'pipe', 'pipe'],
  52. });
  53. if (result.status !== 0) {
  54. const detail = [result.stdout, result.stderr].filter(Boolean).join('\n');
  55. fail(
  56. `Command failed: ${command} ${args.join(' ')}${detail ? `\n${detail}` : ''}`,
  57. );
  58. }
  59. return result.stdout.trim();
  60. }
  61. function parsePackJson(output: string) {
  62. const start = output.indexOf('[');
  63. const end = output.lastIndexOf(']');
  64. if (start === -1 || end === -1 || end < start) {
  65. fail(`Could not locate npm pack JSON output:\n${output}`);
  66. }
  67. return JSON.parse(output.slice(start, end + 1)) as Array<{
  68. filename?: string;
  69. files?: Array<{ path: string }>;
  70. }>;
  71. }
  72. function walkFiles(dir: string): string[] {
  73. const entries = readdirSync(dir, { withFileTypes: true });
  74. return entries.flatMap((entry) => {
  75. const fullPath = path.join(dir, entry.name);
  76. if (entry.isDirectory()) return walkFiles(fullPath);
  77. return [fullPath];
  78. });
  79. }
  80. function verifyDistHasNoLeakedPaths() {
  81. console.log('Checking dist for leaked machine paths...');
  82. const files = walkFiles(distDir).filter((file) =>
  83. /\.(?:js|d\.ts|map|json)$/.test(file),
  84. );
  85. const leaks: string[] = [];
  86. for (const file of files) {
  87. const content = readFileSync(file, 'utf8');
  88. for (const pattern of suspiciousPathPatterns) {
  89. const match = content.match(pattern);
  90. if (!match) continue;
  91. leaks.push(`${path.relative(repoRoot, file)}: ${match[0]}`);
  92. }
  93. for (const pattern of suspiciousImportPatterns) {
  94. const match = content.match(pattern);
  95. if (!match) continue;
  96. leaks.push(`${path.relative(repoRoot, file)}: ${match[0]}`);
  97. }
  98. }
  99. if (leaks.length > 0) {
  100. fail(
  101. `Built artifact contains machine-specific paths:\n${leaks.join('\n')}`,
  102. );
  103. }
  104. }
  105. function packArtifact() {
  106. console.log('Packing npm artifact...');
  107. const output = run('npm', ['pack', '--json', '--ignore-scripts'], {
  108. cwd: repoRoot,
  109. });
  110. const parsed = parsePackJson(output);
  111. const tarball = parsed[0]?.filename;
  112. if (!tarball) {
  113. fail(`npm pack did not return a tarball filename:\n${output}`);
  114. }
  115. const packagedFiles = new Set(
  116. (parsed[0]?.files ?? []).map((file) => file.path),
  117. );
  118. for (const requiredFile of packagedRequiredFiles) {
  119. if (!packagedFiles.has(requiredFile)) {
  120. fail(`npm pack artifact is missing required file: ${requiredFile}`);
  121. }
  122. }
  123. return path.join(repoRoot, tarball);
  124. }
  125. function verifyFreshInstall(tarballPath: string) {
  126. const tempRoot = mkdtempSync(path.join(tmpdir(), 'omos-release-'));
  127. try {
  128. console.log('Installing packed artifact into clean temp project...');
  129. const installDir = path.join(tempRoot, 'install');
  130. const tarballTarget = path.join(tempRoot, path.basename(tarballPath));
  131. copyFileSync(tarballPath, tarballTarget);
  132. mkdirSync(installDir, { recursive: true });
  133. writeFileSync(
  134. path.join(installDir, 'package.json'),
  135. JSON.stringify(
  136. { name: 'verify-release-artifact', private: true },
  137. null,
  138. 2,
  139. ),
  140. );
  141. run('bun', ['add', '--ignore-scripts', tarballTarget], {
  142. cwd: installDir,
  143. });
  144. const installedEntry = path.join(
  145. installDir,
  146. 'node_modules',
  147. 'oh-my-opencode-slim',
  148. 'dist',
  149. 'index.js',
  150. );
  151. const installedEntryContent = readFileSync(installedEntry, 'utf8');
  152. for (const pattern of suspiciousPathPatterns) {
  153. const match = installedEntryContent.match(pattern);
  154. if (match) {
  155. fail(
  156. `Installed package still contains machine-specific path: ${match[0]}`,
  157. );
  158. }
  159. }
  160. const smokeScript = [
  161. "import pkg from 'oh-my-opencode-slim';",
  162. "if (typeof pkg !== 'function') throw new Error('default export is not a function');",
  163. "console.log('package loads');",
  164. 'process.exit(0);',
  165. ].join('\n');
  166. console.log('Importing installed package entrypoint...');
  167. run('node', ['--input-type=module', '--eval', smokeScript], {
  168. cwd: installDir,
  169. });
  170. } finally {
  171. rmSync(tempRoot, { recursive: true, force: true });
  172. }
  173. }
  174. function cleanupTarball(tarballPath: string) {
  175. rmSync(tarballPath, { force: true });
  176. }
  177. function main() {
  178. verifyDistHasNoLeakedPaths();
  179. const tarballPath = packArtifact();
  180. try {
  181. verifyFreshInstall(tarballPath);
  182. } finally {
  183. cleanupTarball(tarballPath);
  184. }
  185. console.log('Release artifact verification passed.');
  186. }
  187. main();