Skip to content

Commit 3efb994

Browse files
killaguclaude
andauthored
fix(ci): publish release with npm instead of ut publish (#6016)
## Motivation The [Manual Release run](https://github.com/eggjs/egg/actions/runs/28323984300/job/83910742571) failed at the **Publish packages** step: ``` 📦 Publishing 79 packages (tag: beta, provenance) error: unexpected argument '--access' found Usage: ut publish [OPTIONS] ``` `ut publish` does not accept npm-native flags (`--access`, `--provenance`), and more importantly it cannot do npm **trusted publishing / provenance via OIDC** — which is the whole reason the release workflow uses them. So this switches `scripts/publish.js` back to `npm publish`. ## The catch: npm doesn't replicate everything `pnpm publish` did A naive `ut` → `npm` swap would publish **broken** packages, because npm understands neither of two things that `pnpm publish` handled for us before the utoo migration: 1. **`workspace:` / `catalog:` protocol specifiers.** 79 publishable packages ship `workspace:*`, `catalog:` and `catalog:path-to-regexp1` in their runtime `dependencies` (984 such specs). npm publishes them verbatim → uninstallable packages. 2. **`publishConfig` field hoisting.** 77/79 packages set a dev-time root `exports` pointing at `./src/*.ts` and a `publishConfig.exports` pointing at compiled `./dist/*.js`. pnpm hoists `publishConfig` on publish; **npm does not**. Since `files: ["dist"]`, the source isn't even in the tarball — every consumer's `import 'egg'` would resolve to a non-existent `./src/index.ts`. (npm exits 0 either way, so it fails silently.) ## Changes `scripts/publish.js` now prepares each manifest right before `npm publish` and restores it in a `finally` (crash-safe), mirroring pnpm: - resolve `workspace:*` → exact in-repo version, `workspace:^`/`~` → prefixed; `catalog:` / `catalog:<name>` → the spec from `pnpm-workspace.yaml` - hoist `publishConfig` overrides (notably `exports`) onto the top-level manifest `scripts/utils.js` gains `getWorkspaceVersionMap`, `getCatalogs`, `resolveWorkspaceProtocols`, and `applyPublishConfigOverrides` (and a `collectWorkspacePackages` refactor that keeps `getPublishablePackages` behavior identical). Also: `isPublished()` now uses the resolved `npm`/`npm.cmd` binary for Windows parity, and a redundant `.filter(!private)` in `sync-cnpm.js` is dropped. ## Tests / verification - Across **all 79 publishable packages / 984 deps**: 0 leftover `workspace:`/`catalog:` specifiers after rewrite; every `workspace:*` → exact version, every `catalog:` → its table spec. - **Ground-truth match** against the last pnpm-published manifests on npm: `@eggjs/router` (`path-to-regexp: ^1.9.0` via the named catalog), `@eggjs/security` (`@eggjs/ip: ^2.1.0`, `@eggjs/path-matching` exact), and `@eggjs/core` reproduce exactly. - `publishConfig.exports` hoisted for all 77 packages that declare it; **0** resolved `exports` left pointing at `src/`. - Real `npm pack` of `@eggjs/core` with the full rewrite produces `exports["."] = "./dist/index.js"`; the manifest is restored afterwards (git clean). - `npm publish --dry-run` accepts `--access`/`--tag`/`--dry-run` (the flags `ut publish` rejected). - `oxfmt --check` and `oxlint --type-aware` clean; pre-commit hook passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Publishing now supports workspace-linked and catalog-based dependency/version resolution for each package. * Publish settings are applied automatically during publishing so released metadata stays consistent. * **Bug Fixes** * Prevents leaving modified manifests behind by restoring package files after publish operations, even on failure. * Improved cross-platform publishing behavior (including Windows). * **Behavior Changes** * Dry-run no longer retries after simulated failures; failures are reported accurately for visibility. * Still skips packages already available on the registry and retries failed publishes once (non-dry-run). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 10705cd commit 3efb994

3 files changed

Lines changed: 245 additions & 69 deletions

File tree

scripts/publish.js

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
/**
44
* Resilient per-package publish script.
55
*
6-
* Unlike `ut -r publish`, this script:
6+
* Publishes with npm so the release keeps npm trusted publishing / provenance
7+
* via OIDC (`ut publish` supports neither `--access` nor `--provenance`).
8+
* Because npm does not understand the pnpm/utoo `workspace:` and `catalog:`
9+
* protocol specifiers, each package manifest is rewritten to concrete version
10+
* ranges right before publishing and restored afterwards — the same rewrite
11+
* `pnpm publish` performed for us before the utoo migration.
12+
*
13+
* On top of that, unlike a bulk publish this script:
714
* - Skips packages that are already published on npm (safe for retries)
815
* - Publishes each package individually so one failure doesn't block others
916
* - Retries failed packages once
@@ -14,9 +21,16 @@
1421
*/
1522

1623
import { execFileSync } from 'node:child_process';
24+
import fs from 'node:fs';
1725
import path from 'node:path';
1826

19-
import { getPublishablePackages } from './utils.js';
27+
import {
28+
applyPublishConfigOverrides,
29+
getCatalogs,
30+
getPublishablePackages,
31+
getWorkspaceVersionMap,
32+
resolveWorkspaceProtocols,
33+
} from './utils.js';
2034

2135
const args = process.argv.slice(2);
2236
const isDryRun = args.includes('--dry-run');
@@ -30,7 +44,9 @@ if (tagArg) {
3044

3145
const baseDir = path.join(import.meta.dirname, '..');
3246
const packages = getPublishablePackages(baseDir);
33-
const utBin = process.platform === 'win32' ? 'ut.cmd' : 'ut';
47+
const versionMap = getWorkspaceVersionMap(baseDir);
48+
const catalogs = getCatalogs(baseDir);
49+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
3450

3551
console.log(
3652
`📦 Publishing ${packages.length} packages (tag: ${npmTag}${isDryRun ? ', dry-run' : ''}${useProvenance ? ', provenance' : ''})`,
@@ -41,7 +57,7 @@ console.log(
4157
*/
4258
function isPublished(name, version) {
4359
try {
44-
const result = execFileSync('npm', ['view', `${name}@${version}`, 'version'], {
60+
const result = execFileSync(npmBin, ['view', `${name}@${version}`, 'version'], {
4561
encoding: 'utf8',
4662
stdio: ['pipe', 'pipe', 'pipe'],
4763
timeout: 15000,
@@ -55,23 +71,39 @@ function isPublished(name, version) {
5571
}
5672

5773
/**
58-
* Publish a single package by running `ut publish` from the package
59-
* directory. utoo's publish only documents --tag/--dry-run/--otp, so we
60-
* keep the npm-standard --access/--provenance flags (forwarded to npm)
61-
* and configure npm to skip git checks because the release workflow builds
62-
* gitignored dist outputs before publishing.
74+
* Publish a single package with npm from the package directory. The manifest is
75+
* rewritten in place to resolve `workspace:` / `catalog:` protocol specifiers
76+
* (npm understands neither) and to hoist `publishConfig` overrides such as
77+
* `exports`, then restored in a `finally` block so a crash mid-publish can never
78+
* leave the rewritten manifest on disk. `--access` and `--provenance` are
79+
* npm-native flags.
6380
*/
6481
function publishOne(pkg) {
6582
const publishArgs = ['publish', '--access', 'public', '--tag', npmTag];
6683
if (useProvenance) publishArgs.push('--provenance');
6784
if (isDryRun) publishArgs.push('--dry-run');
6885

69-
execFileSync(utBin, publishArgs, {
70-
cwd: path.join(baseDir, pkg.directory, pkg.folder),
71-
stdio: 'inherit',
72-
env: { ...process.env, NPM_CONFIG_LOGLEVEL: 'verbose', NPM_CONFIG_GIT_CHECKS: 'false' },
73-
timeout: 120000,
74-
});
86+
const packageDir = path.join(baseDir, pkg.directory, pkg.folder);
87+
const manifestPath = path.join(packageDir, 'package.json');
88+
const originalManifest = fs.readFileSync(manifestPath, 'utf8');
89+
90+
try {
91+
const withVersions = resolveWorkspaceProtocols(JSON.parse(originalManifest), {
92+
versionMap,
93+
catalogs,
94+
});
95+
const resolved = applyPublishConfigOverrides(withVersions);
96+
fs.writeFileSync(manifestPath, `${JSON.stringify(resolved, null, 2)}\n`);
97+
98+
execFileSync(npmBin, publishArgs, {
99+
cwd: packageDir,
100+
stdio: 'inherit',
101+
env: { ...process.env, NPM_CONFIG_LOGLEVEL: 'verbose' },
102+
timeout: 120000,
103+
});
104+
} finally {
105+
fs.writeFileSync(manifestPath, originalManifest);
106+
}
75107
}
76108

77109
const published = [];
@@ -105,31 +137,40 @@ for (const pkg of packages) {
105137
}
106138
}
107139

108-
// Retry failed packages once
140+
// Retry failed packages once. A dry-run retry would just reproduce the same
141+
// result, so we skip the retry but still report the failures — otherwise a
142+
// dry-run would exit 0 even when every package failed to pack, defeating its
143+
// purpose as a pre-flight check.
109144
const finalFailed = [];
110-
if (toRetry.length > 0 && !isDryRun) {
111-
console.log(`\n🔄 Retrying ${toRetry.length} failed package(s)...`);
112-
113-
for (const pkg of toRetry) {
114-
const label = `${pkg.name}@${pkg.version}`;
115-
116-
if (isPublished(pkg.name, pkg.version)) {
117-
console.log(` ⏭️ ${label} now published`);
118-
skipped.push(label);
119-
continue;
145+
if (toRetry.length > 0) {
146+
if (isDryRun) {
147+
for (const pkg of toRetry) {
148+
finalFailed.push(`${pkg.name}@${pkg.version}`);
120149
}
150+
} else {
151+
console.log(`\n🔄 Retrying ${toRetry.length} failed package(s)...`);
152+
153+
for (const pkg of toRetry) {
154+
const label = `${pkg.name}@${pkg.version}`;
121155

122-
try {
123-
publishOne(pkg);
124-
console.log(` ✅ ${label} (retry)`);
125-
published.push(label);
126-
} catch {
127156
if (isPublished(pkg.name, pkg.version)) {
128-
console.log(` ⏭️ ${label} now published (confirmed after retry error)`);
157+
console.log(` ⏭️ ${label} now published`);
129158
skipped.push(label);
130-
} else {
131-
console.error(` ❌ ${label} retry failed`);
132-
finalFailed.push(label);
159+
continue;
160+
}
161+
162+
try {
163+
publishOne(pkg);
164+
console.log(` ✅ ${label} (retry)`);
165+
published.push(label);
166+
} catch {
167+
if (isPublished(pkg.name, pkg.version)) {
168+
console.log(` ⏭️ ${label} now published (confirmed after retry error)`);
169+
skipped.push(label);
170+
} else {
171+
console.error(` ❌ ${label} retry failed`);
172+
finalFailed.push(label);
173+
}
133174
}
134175
}
135176
}

scripts/sync-cnpm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import urllib from 'urllib';
77
import { getPublishablePackages } from './utils.js';
88

99
const baseDir = path.join(import.meta.dirname, '..');
10-
const packages = getPublishablePackages(baseDir).filter((pkg) => !pkg.private);
10+
const packages = getPublishablePackages(baseDir);
1111

1212
console.log(`🚀 Syncing to https://npmmirror.com ...`);
1313

0 commit comments

Comments
 (0)