Skip to content

Commit 324299b

Browse files
ci: skip the Publish job when every version is already on npm (#2956)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
1 parent fcef852 commit 324299b

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎.github/workflows/release.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
pull-requests: write
1717
outputs:
1818
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
19+
allPublished: ${{ steps.registry.outputs.allPublished }}
1920
steps:
2021
- uses: actions/checkout@v7
2122

@@ -34,6 +35,11 @@ jobs:
3435
- name: Install dependencies
3536
run: pnpm install
3637

38+
- name: Check npm for unpublished versions
39+
id: registry
40+
continue-on-error: true
41+
run: node scripts/check-unpublished.mjs
42+
3743
- name: Create or update Version Packages PR
3844
id: changesets
3945
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1
@@ -43,7 +49,8 @@ jobs:
4349
publish:
4450
name: Publish
4551
needs: version
46-
if: needs.version.outputs.hasChangesets == 'false'
52+
# Runs unless changesets are pending or the check proved every version is already on npm; a crashed check proves nothing.
53+
if: needs.version.outputs.hasChangesets == 'false' && needs.version.outputs.allPublished != 'true'
4754
runs-on: ubuntu-latest
4855
environment: release
4956
permissions:

‎scripts/check-unpublished.mjs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Tells the Release workflow whether every public package version is already on npm, so Publish only runs when it has work.
2+
import { appendFileSync, readdirSync, readFileSync } from 'node:fs';
3+
import path from 'node:path';
4+
5+
const registry = 'https://registry.npmjs.org';
6+
const repo = path.resolve(import.meta.dirname, '..');
7+
8+
// Every package.json outside node_modules, not only the workspace globs, so a publishable package is not left out.
9+
function packageJsonFiles(dir) {
10+
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
11+
if (entry.isDirectory()) {
12+
return entry.name === 'node_modules' || entry.name === '.git' ? [] : packageJsonFiles(path.join(dir, entry.name));
13+
}
14+
return entry.name === 'package.json' ? [path.join(dir, entry.name)] : [];
15+
});
16+
}
17+
18+
// Published means the registry returned the document for exactly this name and version; anything else is not proof.
19+
async function isPublished({ name, version }) {
20+
const response = await fetch(`${registry}/${name}/${version}`, { signal: AbortSignal.timeout(30_000) });
21+
const body = await response.text();
22+
const doc = response.status === 200 ? JSON.parse(body) : {};
23+
const published = doc.name === name && doc.version === version;
24+
console.log(`${name}@${version}: ${published ? 'on npm' : `not confirmed on npm (HTTP ${response.status})`}`);
25+
return published;
26+
}
27+
28+
// Any doubt (no packages found, a network error, an odd reply) counts as unpublished, so Publish is still reached.
29+
let allPublished = false;
30+
try {
31+
const packages = packageJsonFiles(repo)
32+
.map(file => JSON.parse(readFileSync(file, 'utf8')))
33+
.filter(pkg => !pkg.private && pkg.name && pkg.version);
34+
let published = 0;
35+
for (const pkg of packages) {
36+
if (await isPublished(pkg)) published++;
37+
}
38+
allPublished = packages.length > 0 && published === packages.length;
39+
console.log(`${published} of ${packages.length} public package versions are on npm`);
40+
} catch (error) {
41+
console.log(`Check failed: ${error.cause ?? error}`);
42+
}
43+
44+
console.log(`allPublished=${allPublished}`);
45+
if (process.env.GITHUB_OUTPUT) {
46+
appendFileSync(process.env.GITHUB_OUTPUT, `allPublished=${allPublished}\n`);
47+
}

0 commit comments

Comments
 (0)