Skip to content

Commit 2eb7e78

Browse files
authored
ci: guard publishable packages before release (repository + packaging) (#313)
1 parent fd59eee commit 2eb7e78

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ jobs:
2929
- name: Run Biome CI
3030
run: pnpm run ci
3131

32+
# Fail fast if a publishable package lacks a repository field — npm's
33+
# provenance check would otherwise reject the publish with E422.
34+
- name: Check publishable packages
35+
run: node scripts/check-publishable.mjs
36+
3237
typecheck:
3338
name: Type Check
3439
runs-on: ubuntu-latest
@@ -76,6 +81,16 @@ jobs:
7681
- name: Build packages
7782
run: pnpm build
7883

84+
# Packaging smoke test: confirm each publishable package can be packed
85+
# (catches missing dist / bad `files` / `exports` before release).
86+
- name: Verify packaging (npm pack --dry-run)
87+
run: |
88+
for d in $(node scripts/check-publishable.mjs --list); do
89+
echo "::group::npm pack $d"
90+
( cd "$d" && npm pack --dry-run --ignore-scripts )
91+
echo "::endgroup::"
92+
done
93+
7994
test:
8095
name: Test
8196
runs-on: ubuntu-latest

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test:integration": "vitest run --config e2e/vitest.integration.config.ts",
2424
"test:e2e": "playwright test --config e2e/playwright.config.ts",
2525
"test:e2e:headed": "playwright test --config e2e/playwright.config.ts --headed",
26+
"check:publishable": "node scripts/check-publishable.mjs",
2627
"prepare": "husky",
2728
"changeset": "changeset",
2829
"changeset:version": "changeset version && pnpm install --lockfile-only",

scripts/check-publishable.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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

Comments
 (0)