From b200a369b72849c8d453f52396a4d0c2ad85c3ad Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 12 May 2026 19:56:24 +0200 Subject: [PATCH] Skip publishing when all versions are published --- .github/scripts/has-unpublished-packages.mjs | 88 ++++++++++++++++++++ .github/workflows/release.yml | 7 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/has-unpublished-packages.mjs diff --git a/.github/scripts/has-unpublished-packages.mjs b/.github/scripts/has-unpublished-packages.mjs new file mode 100644 index 00000000..19b492c0 --- /dev/null +++ b/.github/scripts/has-unpublished-packages.mjs @@ -0,0 +1,88 @@ +import { appendFileSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; + +const ignoredDirectories = new Set([ + ".git", + "dist", + "node_modules", +]); + +async function findPackageManifests(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const manifests = []; + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) { + manifests.push(...await findPackageManifests(fullPath)); + } + } else if (entry.isFile() && entry.name === "package.json") { + const pkg = JSON.parse(await readFile(fullPath, "utf8")); + + if (!pkg.private && pkg.name && pkg.version) { + manifests.push({ name: pkg.name, version: pkg.version }); + } + } + } + + return manifests; +} + +async function hasPublishedVersion(pkg) { + const response = await fetch( + `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, + { headers: { accept: "application/vnd.npm.install-v1+json" } }, + ); + + if (response.status === 404) { + return false; + } + + if (!response.ok) { + throw new Error( + `Failed to query ${pkg.name}: ${response.status} ${response.statusText}`, + ); + } + + const metadata = await response.json(); + return Object.prototype.hasOwnProperty.call( + metadata.versions ?? {}, + pkg.version, + ); +} + +async function main() { + const packages = (await findPackageManifests(process.cwd())) + .sort((a, b) => a.name.localeCompare(b.name)); + let hasUnpublished = false; + + for (const pkg of packages) { + const isPublished = await hasPublishedVersion(pkg); + + if (isPublished) { + console.log(`${pkg.name}@${pkg.version} is already published`); + } else { + console.log(`${pkg.name}@${pkg.version} is not published yet`); + hasUnpublished = true; + } + } + + const output = [ + `has_unpublished=${String(hasUnpublished)}`, + `should_publish=${String(hasUnpublished)}`, + ].join("\n") + "\n"; + + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, output); + } else { + process.stdout.write(output); + } +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 393f596f..4347d1f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ jobs: runs-on: ubuntu-latest outputs: has_changesets: ${{ steps.changesets.outputs.hasChangesets }} + has_unpublished_packages: ${{ steps.unpublished.outputs.has_unpublished }} permissions: contents: write issues: read @@ -30,6 +31,10 @@ jobs: - name: Update npm run: npm install -g npm@11.11 + - name: Check for unpublished package versions + id: unpublished + run: node .github/scripts/has-unpublished-packages.mjs + - name: Setup pnpm uses: pnpm/action-setup@v3 with: @@ -63,7 +68,7 @@ jobs: publish: name: Publish needs: release - if: needs.release.outputs.has_changesets == 'false' + if: needs.release.outputs.has_changesets == 'false' && needs.release.outputs.has_unpublished_packages == 'true' runs-on: ubuntu-latest environment: name: npm