|
| 1 | +#!/usr/bin/env node |
| 2 | +// Guards npm Trusted Publishing (OIDC): every publishable package — any |
| 3 | +// packages/* that isn't `"private": true` — must carry a `repository` field |
| 4 | +// whose url matches this repo and whose `directory` matches its folder. npm's |
| 5 | +// provenance check rejects a publish otherwise with E422 (this bit us once: |
| 6 | +// wallet-react@0.0.2 failed to publish while a dependent already had). |
| 7 | +// |
| 8 | +// Runs on every PR (in the Lint job), so the failure is caught before the |
| 9 | +// "Version Packages" PR is ever created — no post-merge rollback. |
| 10 | +// |
| 11 | +// node scripts/check-publishable.mjs # validate, exit 1 on problems |
| 12 | +// node scripts/check-publishable.mjs --list # print publishable dirs, one per line |
| 13 | +import { readdirSync, readFileSync, existsSync } from 'node:fs' |
| 14 | +import { join, dirname } from 'node:path' |
| 15 | +import { fileURLToPath } from 'node:url' |
| 16 | + |
| 17 | +const root = join(dirname(fileURLToPath(import.meta.url)), '..') |
| 18 | +const pkgsDir = join(root, 'packages') |
| 19 | +const EXPECTED_URL = 'git+https://github.com/zerodevapp/zerodev-wallet-sdk.git' |
| 20 | +const listOnly = process.argv.includes('--list') |
| 21 | + |
| 22 | +const publishable = [] |
| 23 | +for (const name of readdirSync(pkgsDir).sort()) { |
| 24 | + const pkgPath = join(pkgsDir, name, 'package.json') |
| 25 | + if (!existsSync(pkgPath)) continue |
| 26 | + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) |
| 27 | + if (pkg.private === true) continue // intentionally never published |
| 28 | + publishable.push({ name, pkg }) |
| 29 | +} |
| 30 | + |
| 31 | +if (listOnly) { |
| 32 | + for (const { name } of publishable) console.log(`packages/${name}`) |
| 33 | + process.exit(0) |
| 34 | +} |
| 35 | + |
| 36 | +if (!publishable.length) { |
| 37 | + console.error('check-publishable: no publishable packages found — did the packages/ layout change?') |
| 38 | + process.exit(1) |
| 39 | +} |
| 40 | + |
| 41 | +const errors = [] |
| 42 | +for (const { name, pkg } of publishable) { |
| 43 | + const dir = `packages/${name}` |
| 44 | + const repo = pkg.repository |
| 45 | + if (!repo || typeof repo !== 'object') { |
| 46 | + errors.push(`${pkg.name}: missing "repository" object`) |
| 47 | + continue |
| 48 | + } |
| 49 | + if (!repo.url || !repo.url.trim()) { |
| 50 | + errors.push(`${pkg.name}: "repository.url" is empty`) |
| 51 | + } else if (repo.url !== EXPECTED_URL) { |
| 52 | + errors.push(`${pkg.name}: "repository.url" is "${repo.url}", expected "${EXPECTED_URL}"`) |
| 53 | + } |
| 54 | + if (repo.directory !== dir) { |
| 55 | + errors.push(`${pkg.name}: "repository.directory" is "${repo.directory ?? '(unset)'}", expected "${dir}"`) |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +if (errors.length) { |
| 60 | + console.error('Publishable package checks failed:\n') |
| 61 | + for (const e of errors) console.error(` ✗ ${e}`) |
| 62 | + console.error('\nnpm Trusted Publishing rejects a publish whose package.json "repository"') |
| 63 | + console.error('does not match the source repo (E422). Fix these before merging.') |
| 64 | + process.exit(1) |
| 65 | +} |
| 66 | + |
| 67 | +console.log(`check-publishable: OK — ${publishable.length} publishable packages have a valid repository field`) |
| 68 | +for (const { pkg } of publishable) console.log(` ✓ ${pkg.name}`) |
0 commit comments