Skip to content

Commit de97949

Browse files
Merge pull request #659 from dao-xyz/codex/release-public-publish-fix
fix: publish only public workspace packages
2 parents 3ab39a4 + ad418ec commit de97949

File tree

3 files changed

+223
-11
lines changed

3 files changed

+223
-11
lines changed

.github/workflows/release.yml

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@ on:
44
- master
55
paths:
66
- packages/**
7-
- .release-please.json
7+
- package.json
8+
- scripts/**
9+
- .release-please.json
10+
- .github/workflows/release.yml
11+
workflow_dispatch:
12+
inputs:
13+
publish_mode:
14+
description: Publish mode
15+
required: true
16+
default: stable
17+
type: choice
18+
options:
19+
- stable
20+
- rc
821
permissions:
922
contents: write
1023
issues: write
@@ -22,47 +35,48 @@ jobs:
2235
steps:
2336
- uses: googleapis/release-please-action@v4
2437
id: release
38+
if: ${{ github.event_name == 'push' }}
2539
with:
2640
token: ${{secrets.GITHUB_TOKEN}}
2741
manifest-file: .release-please-manifest.json
2842
config-file: .release-please.json
2943

3044
# The logic below handles the npm publication:
3145
- name: Checkout Repository
32-
if: ${{ steps.release.outputs.releases_created }}
46+
if: ${{ github.event_name == 'workflow_dispatch' || steps.release.outputs.releases_created == 'true' }}
3347
uses: actions/checkout@v4
3448
- name: Setup Node
3549
uses: actions/setup-node@v4
36-
if: ${{ steps.release.outputs.releases_created }}
50+
if: ${{ github.event_name == 'workflow_dispatch' || steps.release.outputs.releases_created == 'true' }}
3751
with:
3852
node-version: lts/*
3953
registry-url: 'https://registry.npmjs.org'
4054
- uses: pnpm/action-setup@v4
41-
if: ${{ steps.release.outputs.releases_created }}
55+
if: ${{ github.event_name == 'workflow_dispatch' || steps.release.outputs.releases_created == 'true' }}
4256
with:
4357
run_install: false
4458

4559
- name: Install deps
46-
if: ${{ steps.release.outputs.releases_created }}
60+
if: ${{ github.event_name == 'workflow_dispatch' || steps.release.outputs.releases_created == 'true' }}
4761
run: |
4862
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
4963
pnpm install --frozen-lockfile=false
5064
5165
- name: Build Packages
52-
if: ${{ steps.release.outputs.releases_created }}
66+
if: ${{ github.event_name == 'workflow_dispatch' || steps.release.outputs.releases_created == 'true' }}
5367
run: pnpm run build
5468

5569
# Release Please has already incremented versions and published tags, so we just
5670
# need to publish all unpublished versions to NPM here
5771
# See: https://github.com/lerna/lerna/tree/main/commands/publish#bump-from-package
5872
- name: Publish to NPM
59-
if: ${{ steps.release.outputs.releases_created == 'true' }}
73+
if: ${{ (github.event_name == 'push' && steps.release.outputs.releases_created == 'true') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_mode == 'stable') }}
6074
env:
6175
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
62-
run: npm run --if-present release
76+
run: pnpm run --if-present release
6377
- name: Publish RC to NPM
64-
if: ${{ steps.release.outputs.releases_created != 'true' }}
78+
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.publish_mode == 'rc' }}
6579
env:
6680
NPM_CONFIG_GIT_CHECKS: "false"
6781
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
68-
run: npm run --if-present release:rc
82+
run: pnpm run --if-present release:rc

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"site:sync-updates": "node apps/peerbit-org/scripts/syncUpdatesEmail.mjs",
110110
"dep-check": "aegir run dep-check",
111111
"doc-check": "aegir run doc-check",
112-
"release": "aegir exec --bail false pnpm -- publish",
112+
"release": "node ./scripts/publish-public-packages.mjs",
113113
"release:rc": "AEGIR_PACKAGE_MANAGER=pnpm aegir release-rc",
114114
"deploy": "pnpm run deploy:docs",
115115
"deploy:docs": "pnpm run site:build && gh-pages --dist apps/peerbit-org/dist --dotfiles",
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env node
2+
3+
import { spawn } from "node:child_process";
4+
import { promises as fs } from "node:fs";
5+
import path from "node:path";
6+
import process from "node:process";
7+
8+
const rootDir = process.cwd();
9+
const packagesDir = path.join(rootDir, "packages");
10+
const pnpmCmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
11+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
12+
13+
const args = process.argv.slice(2);
14+
const dryRun = args.includes("--dry-run");
15+
const tag = readFlag("--tag");
16+
17+
function readFlag(name) {
18+
const index = args.indexOf(name);
19+
return index >= 0 ? args[index + 1] : undefined;
20+
}
21+
22+
async function findPackageJsonFiles(directory) {
23+
const entries = await fs.readdir(directory, { withFileTypes: true });
24+
const results = [];
25+
for (const entry of entries) {
26+
if (entry.name === "node_modules" || entry.name === "dist") {
27+
continue;
28+
}
29+
const absolutePath = path.join(directory, entry.name);
30+
if (entry.isDirectory()) {
31+
results.push(...(await findPackageJsonFiles(absolutePath)));
32+
} else if (entry.isFile() && entry.name === "package.json") {
33+
results.push(absolutePath);
34+
}
35+
}
36+
return results;
37+
}
38+
39+
async function loadWorkspacePackages() {
40+
const packageJsonFiles = await findPackageJsonFiles(packagesDir);
41+
const packages = [];
42+
for (const packageJsonFile of packageJsonFiles) {
43+
const manifest = JSON.parse(await fs.readFile(packageJsonFile, "utf8"));
44+
if (!manifest.name || manifest.private) {
45+
continue;
46+
}
47+
packages.push({
48+
dir: path.dirname(packageJsonFile),
49+
name: manifest.name,
50+
version: manifest.version,
51+
manifest,
52+
});
53+
}
54+
return packages;
55+
}
56+
57+
function getInternalDependencies(manifest, packageNames) {
58+
const dependencySets = [
59+
manifest.dependencies,
60+
manifest.optionalDependencies,
61+
manifest.peerDependencies,
62+
];
63+
const internal = new Set();
64+
for (const dependencySet of dependencySets) {
65+
if (!dependencySet) {
66+
continue;
67+
}
68+
for (const name of Object.keys(dependencySet)) {
69+
if (packageNames.has(name)) {
70+
internal.add(name);
71+
}
72+
}
73+
}
74+
return internal;
75+
}
76+
77+
function sortTopologically(packages) {
78+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
79+
const packageNames = new Set(byName.keys());
80+
const dependencies = new Map();
81+
const reverseEdges = new Map();
82+
const indegree = new Map();
83+
84+
for (const pkg of packages) {
85+
const internalDependencies = getInternalDependencies(pkg.manifest, packageNames);
86+
dependencies.set(pkg.name, internalDependencies);
87+
indegree.set(pkg.name, internalDependencies.size);
88+
for (const dependency of internalDependencies) {
89+
const dependents = reverseEdges.get(dependency) ?? new Set();
90+
dependents.add(pkg.name);
91+
reverseEdges.set(dependency, dependents);
92+
}
93+
}
94+
95+
const queue = packages
96+
.filter((pkg) => (indegree.get(pkg.name) ?? 0) === 0)
97+
.map((pkg) => pkg.name)
98+
.sort();
99+
const ordered = [];
100+
101+
while (queue.length > 0) {
102+
const name = queue.shift();
103+
ordered.push(byName.get(name));
104+
for (const dependent of reverseEdges.get(name) ?? []) {
105+
const nextInDegree = (indegree.get(dependent) ?? 0) - 1;
106+
indegree.set(dependent, nextInDegree);
107+
if (nextInDegree === 0) {
108+
queue.push(dependent);
109+
queue.sort();
110+
}
111+
}
112+
}
113+
114+
if (ordered.length !== packages.length) {
115+
throw new Error("Unable to topologically sort publishable workspace packages");
116+
}
117+
118+
return ordered;
119+
}
120+
121+
function run(command, commandArgs, cwd) {
122+
return new Promise((resolve, reject) => {
123+
const child = spawn(command, commandArgs, {
124+
cwd,
125+
env: process.env,
126+
stdio: "inherit",
127+
});
128+
child.on("exit", (code) => {
129+
if (code === 0) {
130+
resolve();
131+
return;
132+
}
133+
reject(new Error(`${command} ${commandArgs.join(" ")} exited with code ${code ?? "null"}`));
134+
});
135+
child.on("error", reject);
136+
});
137+
}
138+
139+
function capture(command, commandArgs, cwd) {
140+
return new Promise((resolve) => {
141+
const child = spawn(command, commandArgs, {
142+
cwd,
143+
env: process.env,
144+
stdio: ["ignore", "pipe", "pipe"],
145+
});
146+
let stdout = "";
147+
let stderr = "";
148+
child.stdout.on("data", (chunk) => {
149+
stdout += chunk.toString();
150+
});
151+
child.stderr.on("data", (chunk) => {
152+
stderr += chunk.toString();
153+
});
154+
child.on("exit", (code) => {
155+
resolve({ code: code ?? 1, stdout, stderr });
156+
});
157+
child.on("error", (error) => {
158+
resolve({ code: 1, stdout, stderr: `${stderr}\n${String(error)}` });
159+
});
160+
});
161+
}
162+
163+
async function isPublished({ name, version }) {
164+
const result = await capture(npmCmd, ["view", `${name}@${version}`, "version"], rootDir);
165+
if (result.code === 0) {
166+
return true;
167+
}
168+
const combinedOutput = `${result.stdout}\n${result.stderr}`;
169+
if (combinedOutput.includes("E404") || combinedOutput.includes("No match found for version")) {
170+
return false;
171+
}
172+
throw new Error(`Failed to query npm for ${name}@${version}\n${combinedOutput}`);
173+
}
174+
175+
async function publishPackage(pkg) {
176+
const alreadyPublished = await isPublished(pkg);
177+
if (alreadyPublished) {
178+
console.log(`skip ${pkg.name}@${pkg.version} (already published)`);
179+
return;
180+
}
181+
182+
const publishArgs = ["publish", "--no-git-checks", "--access", "public"];
183+
if (dryRun) {
184+
publishArgs.push("--dry-run");
185+
}
186+
if (tag) {
187+
publishArgs.push("--tag", tag);
188+
}
189+
console.log(`publish ${pkg.name}@${pkg.version}`);
190+
await run(pnpmCmd, publishArgs, pkg.dir);
191+
}
192+
193+
const workspacePackages = await loadWorkspacePackages();
194+
const publishOrder = sortTopologically(workspacePackages);
195+
196+
for (const pkg of publishOrder) {
197+
await publishPackage(pkg);
198+
}

0 commit comments

Comments
 (0)