Skip to content

Commit 872db1c

Browse files
committed
ci: use npm staged publishing
1 parent d5014c2 commit 872db1c

2 files changed

Lines changed: 111 additions & 3 deletions

File tree

.github/scripts/stage-packages.mjs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env node
2+
import { spawnSync } from "node:child_process";
3+
import { existsSync, readFileSync, readdirSync } from "node:fs";
4+
import { dirname, join, relative } from "node:path";
5+
6+
const root = process.cwd();
7+
const config = JSON.parse(readFileSync(join(root, ".changeset/config.json"), "utf8"));
8+
const ignored = new Set(config.ignore || []);
9+
const access = config.access || "public";
10+
11+
function readJson(path) {
12+
return JSON.parse(readFileSync(path, "utf8"));
13+
}
14+
15+
function packageJsonPathsFromPnpmWorkspace() {
16+
const workspace = join(root, "pnpm-workspace.yaml");
17+
if (!existsSync(workspace)) return [];
18+
19+
const patterns = [];
20+
for (const rawLine of readFileSync(workspace, "utf8").split(/\r?\n/)) {
21+
const line = rawLine.trim();
22+
if (!line.startsWith("- ")) continue;
23+
const pattern = line.slice(2).replace(/^['\"]|['\"]$/g, "");
24+
if (!pattern || pattern.startsWith("!")) continue;
25+
patterns.push(pattern);
26+
}
27+
28+
const paths = [];
29+
for (const pattern of patterns) {
30+
if (pattern.endsWith("/*")) {
31+
const dir = join(root, pattern.slice(0, -2));
32+
if (!existsSync(dir)) continue;
33+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
34+
if (entry.isDirectory()) paths.push(join(dir, entry.name, "package.json"));
35+
}
36+
} else if (pattern.endsWith("/**")) {
37+
// These workspaces are docs/examples and not published packages in this release flow.
38+
continue;
39+
} else {
40+
paths.push(join(root, pattern, "package.json"));
41+
}
42+
}
43+
44+
return paths;
45+
}
46+
47+
function packageJsonPaths() {
48+
const paths = new Set(packageJsonPathsFromPnpmWorkspace());
49+
paths.add(join(root, "package.json"));
50+
return [...paths].filter(existsSync);
51+
}
52+
53+
function versionExists(name, version) {
54+
const result = spawnSync("npm", ["view", `${name}@${version}`, "version", "--json"], {
55+
encoding: "utf8",
56+
stdio: ["ignore", "pipe", "pipe"],
57+
});
58+
59+
if (result.status === 0) return true;
60+
const output = `${result.stdout}\n${result.stderr}`;
61+
if (output.includes("E404") || output.includes("No match found")) return false;
62+
63+
process.stdout.write(result.stdout);
64+
process.stderr.write(result.stderr);
65+
throw new Error(`Could not check npm version for ${name}@${version}`);
66+
}
67+
68+
function distTag(version) {
69+
const prerelease = version.match(/^[^-]+-([0-9A-Za-z-]+)/);
70+
return prerelease ? prerelease[1] : "latest";
71+
}
72+
73+
const staged = [];
74+
for (const packageJsonPath of packageJsonPaths()) {
75+
const pkg = readJson(packageJsonPath);
76+
if (!pkg.name || !pkg.version || pkg.private || ignored.has(pkg.name)) continue;
77+
if (versionExists(pkg.name, pkg.version)) {
78+
console.log(`Skipping ${pkg.name}@${pkg.version}; already published.`);
79+
continue;
80+
}
81+
82+
const packageDir = dirname(packageJsonPath);
83+
const tag = distTag(pkg.version);
84+
const args = ["stage", "publish", packageDir, "--provenance", "--access", pkg.publishConfig?.access || access, "--tag", tag, "--json"];
85+
86+
console.log(`Staging ${pkg.name}@${pkg.version} with dist-tag ${tag}...`);
87+
const result = spawnSync("npm", args, {
88+
cwd: root,
89+
encoding: "utf8",
90+
stdio: ["ignore", "pipe", "pipe"],
91+
});
92+
process.stdout.write(result.stdout);
93+
process.stderr.write(result.stderr);
94+
if (result.status !== 0) process.exit(result.status || 1);
95+
96+
const stageId = result.stdout.match(/"stageId"\s*:\s*"([^"]+)"/)?.[1];
97+
staged.push({ name: pkg.name, version: pkg.version, path: relative(root, packageDir) || ".", stageId });
98+
}
99+
100+
if (staged.length === 0) {
101+
console.log("No unpublished packages to stage.");
102+
} else {
103+
console.log("Staged packages:");
104+
for (const pkg of staged) {
105+
console.log(`- ${pkg.name}@${pkg.version}${pkg.stageId ? ` (${pkg.stageId})` : ""}`);
106+
}
107+
console.log("Approve staged packages with `npm stage approve <stage-id>` after review.");
108+
}

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
registry-url: 'https://registry.npmjs.org'
3131

3232
- name: Update npm
33-
run: npm install -g npm@11.11
33+
run: npm install -g npm@11.15.0
3434

3535
- name: Check for unpublished package versions
3636
id: unpublished
@@ -80,7 +80,7 @@ jobs:
8080
registry-url: 'https://registry.npmjs.org'
8181

8282
- name: Update npm
83-
run: npm install -g npm@11.11
83+
run: npm install -g npm@11.15.0
8484

8585
- name: Setup pnpm
8686
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
@@ -94,6 +94,6 @@ jobs:
9494
- name: Publish packages
9595
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
9696
with:
97-
publish: pnpm changeset publish
97+
publish: node .github/scripts/stage-packages.mjs
9898
env:
9999
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)