From 2324ca66cfb45758a2300bd9df82a5ceaf46aef3 Mon Sep 17 00:00:00 2001 From: Louis Varin Date: Tue, 10 Mar 2026 21:37:35 -0400 Subject: [PATCH] feat: verify packages exist before publishing TICKET: VL-4704 --- .../actions/verify-npm-packages/action.yml | 10 +++ .github/actions/verify-npm-packages/index.js | 80 +++++++++++++++++++ .github/workflows/npmjs-release.yml | 6 ++ 3 files changed, 96 insertions(+) create mode 100644 .github/actions/verify-npm-packages/action.yml create mode 100644 .github/actions/verify-npm-packages/index.js diff --git a/.github/actions/verify-npm-packages/action.yml b/.github/actions/verify-npm-packages/action.yml new file mode 100644 index 0000000000..5b0cbfd74a --- /dev/null +++ b/.github/actions/verify-npm-packages/action.yml @@ -0,0 +1,10 @@ +name: Verify npm Packages Exist +description: Verifies that all non-private packages in the monorepo already exist on the npm registry. Required for trusted publishing. + +runs: + using: composite + steps: + - name: Verify all packages exist on npm + id: verify + shell: bash + run: node ${{ github.action_path }}/index.js diff --git a/.github/actions/verify-npm-packages/index.js b/.github/actions/verify-npm-packages/index.js new file mode 100644 index 0000000000..230e6351bd --- /dev/null +++ b/.github/actions/verify-npm-packages/index.js @@ -0,0 +1,80 @@ +const fs = require("fs"); +const path = require("path"); + +const REGISTRY_URL = "https://registry.npmjs.org"; +const MODULES_DIR = path.resolve(__dirname, "..", "..", "..", "modules"); +const CONCURRENCY = 10; + +async function getPublicPackages() { + const entries = fs.readdirSync(MODULES_DIR, { withFileTypes: true }); + const packages = []; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const pkgPath = path.join(MODULES_DIR, entry.name, "package.json"); + if (!fs.existsSync(pkgPath)) continue; + + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + if (pkg.private) continue; + + packages.push({ name: pkg.name, dir: entry.name }); + } + + return packages; +} + +async function packageExistsOnNpm(packageName) { + const url = `${REGISTRY_URL}/${packageName}`; + const response = await fetch(url, { method: "HEAD" }); + return response.status === 200; +} + +async function processInBatches(items, fn, concurrency) { + const results = []; + for (let i = 0; i < items.length; i += concurrency) { + const batch = items.slice(i, i + concurrency); + const batchResults = await Promise.all(batch.map(fn)); + results.push(...batchResults); + } + return results; +} + +async function main() { + const packages = await getPublicPackages(); + console.log(`Found ${packages.length} public packages to verify.\n`); + + const results = await processInBatches( + packages, + async (pkg) => { + const exists = await packageExistsOnNpm(pkg.name); + if (exists) { + console.log(` ✓ ${pkg.name}`); + } else { + console.error(` ✗ ${pkg.name} — not found on npm`); + } + return { ...pkg, exists }; + }, + CONCURRENCY + ); + + const missing = results.filter((r) => !r.exists); + + if (missing.length > 0) { + console.log(`\n${missing.length} package(s) not found on npm:\n`); + for (const pkg of missing) { + console.log(` - ${pkg.name} (modules/${pkg.dir})`); + } + console.error( + "\nThese packages must be created on npm before publishing with trusted publishing." + ); + process.exit(1); + } + + console.log(`\nAll ${packages.length} packages exist on npm.`); +} + +main().catch((err) => { + console.error("Failed to verify npm packages:", err); + process.exit(1); +}); diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 53dbfa5c94..e5d7b305c1 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -163,6 +163,12 @@ jobs: run: | yarn check-deps + # Trusted publishing (OIDC) cannot publish a package that doesn't already + # exist on npm. Catch missing packages before the publish step so the + # release doesn't partially fail midway through. + - name: Verify all packages exist on npm + uses: ./.github/actions/verify-npm-packages + - name: Publish new version if: inputs.dry-run == false run: |