Skip to content

Commit d8c5ab7

Browse files
committed
ci: stage npm releases before publishing
1 parent c97e1f5 commit d8c5ab7

2 files changed

Lines changed: 122 additions & 5 deletions

File tree

.github/scripts/stage-package.mjs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { appendFileSync } from 'node:fs';
2+
import { readFile } from 'node:fs/promises';
3+
import { spawnSync } from 'node:child_process';
4+
5+
async function hasPublishedVersion(pkg) {
6+
const response = await fetch(
7+
`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`,
8+
{ headers: { accept: 'application/vnd.npm.install-v1+json' } }
9+
);
10+
11+
if (response.status === 404) {
12+
return false;
13+
}
14+
15+
if (!response.ok) {
16+
throw new Error(
17+
`Failed to query ${pkg.name}: ${response.status} ${response.statusText}`
18+
);
19+
}
20+
21+
const metadata = await response.json();
22+
return Object.prototype.hasOwnProperty.call(
23+
metadata.versions ?? {},
24+
pkg.version
25+
);
26+
}
27+
28+
function getDistTag(version) {
29+
const prerelease = version.match(/-([0-9A-Za-z-]+)(?:\.|$)/);
30+
return prerelease ? prerelease[1] : 'latest';
31+
}
32+
33+
function collectStageIds(value, ids = new Set()) {
34+
if (!value || typeof value !== 'object') return ids;
35+
36+
for (const [key, child] of Object.entries(value)) {
37+
if (/stage[-_]?id/i.test(key) && typeof child === 'string') {
38+
ids.add(child);
39+
} else {
40+
collectStageIds(child, ids);
41+
}
42+
}
43+
44+
return ids;
45+
}
46+
47+
async function main() {
48+
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
49+
const changesetConfig = JSON.parse(
50+
await readFile('.changeset/config.json', 'utf8')
51+
);
52+
53+
if (pkg.private) {
54+
console.log(`${pkg.name}@${pkg.version} is private; skipping staging`);
55+
return;
56+
}
57+
58+
if (await hasPublishedVersion(pkg)) {
59+
console.log(
60+
`${pkg.name}@${pkg.version} is already published; skipping staging`
61+
);
62+
return;
63+
}
64+
65+
const access =
66+
pkg.publishConfig?.access ?? changesetConfig.access ?? 'public';
67+
const tag = getDistTag(pkg.version);
68+
const args = [
69+
'stage',
70+
'publish',
71+
'.',
72+
'--access',
73+
access,
74+
'--tag',
75+
tag,
76+
'--json'
77+
];
78+
79+
console.log(`Staging ${pkg.name}@${pkg.version} with dist-tag ${tag}`);
80+
const result = spawnSync('npm', args, {
81+
encoding: 'utf8',
82+
stdio: ['ignore', 'pipe', 'pipe']
83+
});
84+
85+
if (result.stdout) process.stdout.write(result.stdout);
86+
if (result.stderr) process.stderr.write(result.stderr);
87+
88+
if (result.status !== 0) {
89+
process.exit(result.status ?? 1);
90+
}
91+
92+
let stageIds = [];
93+
try {
94+
stageIds = [...collectStageIds(JSON.parse(result.stdout))];
95+
} catch {
96+
// Keep the raw npm output above as the source of truth if the JSON shape changes.
97+
}
98+
99+
if (stageIds.length > 0) {
100+
const message = [
101+
`Staged ${pkg.name}@${pkg.version}.`,
102+
...stageIds.map((id) => `Stage ID: ${id}`),
103+
'Approve with `npm stage approve <stage-id>` after reviewing the staged package.'
104+
].join('\n');
105+
106+
console.log(message);
107+
108+
if (process.env.GITHUB_STEP_SUMMARY) {
109+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${message}\n`);
110+
}
111+
}
112+
}
113+
114+
main().catch((error) => {
115+
console.error(error);
116+
process.exit(1);
117+
});

.github/workflows/release.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
registry-url: "https://registry.npmjs.org"
3535

3636
- name: Update npm
37-
run: npm install -g npm@11.11.1
37+
run: npm install -g npm@^11.15.0
3838

3939
- name: Install dependencies
4040
run: npm ci --ignore-scripts
@@ -54,7 +54,7 @@ jobs:
5454
run: node .github/scripts/has-unpublished-packages.mjs
5555

5656
publish:
57-
name: Publish
57+
name: Stage package
5858
needs: release
5959
if: needs.release.outputs.should_publish == 'true'
6060
environment:
@@ -78,18 +78,18 @@ jobs:
7878
registry-url: "https://registry.npmjs.org"
7979

8080
- name: Update npm
81-
run: npm install -g npm@11.11.1
81+
run: npm install -g npm@^11.15.0
8282

8383
- name: Install dependencies
8484
run: npm ci --ignore-scripts
8585

8686
- name: Build package
8787
run: npm run build
8888

89-
- name: Publish packages
89+
- name: Stage package
9090
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
9191
with:
92-
publish: npm exec -- changeset publish
92+
publish: node .github/scripts/stage-package.mjs
9393
commitMode: github-api
9494
env:
9595
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)