@kb-labs/release-manager-cli 2.77.0 → 2.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/publish.js +393 -356
- package/dist/cli/commands/publish.js.map +1 -1
- package/dist/cli/commands/run.js +399 -356
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/rest/handlers/run-handler.js +250 -105
- package/dist/rest/handlers/run-handler.js.map +1 -1
- package/package.json +7 -7
|
@@ -8,10 +8,142 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
|
8
8
|
import * as readline from 'readline/promises';
|
|
9
9
|
|
|
10
10
|
// src/cli/commands/publish.ts
|
|
11
|
+
function rewriteWorkspaceDeps(pkgPath, versionMap, packageManager) {
|
|
12
|
+
const pkgJsonPath = join(pkgPath, "package.json");
|
|
13
|
+
const original = readFileSync(pkgJsonPath, "utf-8");
|
|
14
|
+
const pkgJson = JSON.parse(original);
|
|
15
|
+
let modified = false;
|
|
16
|
+
for (const section of ["dependencies", "peerDependencies"]) {
|
|
17
|
+
const deps = pkgJson[section];
|
|
18
|
+
if (!deps) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const depName of Object.keys(deps)) {
|
|
22
|
+
const val = deps[depName];
|
|
23
|
+
if (typeof val !== "string") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (val.startsWith("link:")) {
|
|
27
|
+
const pinned = versionMap.get(depName);
|
|
28
|
+
if (pinned) {
|
|
29
|
+
deps[depName] = `^${pinned}`;
|
|
30
|
+
} else {
|
|
31
|
+
try {
|
|
32
|
+
const linkPath = val.slice("link:".length);
|
|
33
|
+
const linked = JSON.parse(readFileSync(join(pkgPath, linkPath, "package.json"), "utf-8"));
|
|
34
|
+
deps[depName] = `^${linked.version ?? "*"}`;
|
|
35
|
+
} catch {
|
|
36
|
+
deps[depName] = "*";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
modified = true;
|
|
40
|
+
} else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
|
|
41
|
+
const pinned = versionMap.get(depName);
|
|
42
|
+
if (pinned) {
|
|
43
|
+
deps[depName] = val === "workspace:*" ? `^${pinned}` : val.replace("workspace:", "");
|
|
44
|
+
modified = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (modified) {
|
|
50
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
51
|
+
}
|
|
52
|
+
return () => writeFileSync(pkgJsonPath, original);
|
|
53
|
+
}
|
|
54
|
+
function topoSort(packages, packageManager) {
|
|
55
|
+
const nameSet = new Set(packages.map((p) => p.name));
|
|
56
|
+
const deps = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const pkg of packages) {
|
|
58
|
+
const intra = /* @__PURE__ */ new Set();
|
|
59
|
+
try {
|
|
60
|
+
const pkgJson = JSON.parse(readFileSync(join(pkg.path, "package.json"), "utf-8"));
|
|
61
|
+
for (const section of ["dependencies", "peerDependencies"]) {
|
|
62
|
+
for (const depName of Object.keys(pkgJson[section] ?? {})) {
|
|
63
|
+
if (nameSet.has(depName)) {
|
|
64
|
+
intra.add(depName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
deps.set(pkg.name, intra);
|
|
71
|
+
}
|
|
72
|
+
const inDegree = new Map(packages.map((p) => [p.name, 0]));
|
|
73
|
+
for (const [, d] of deps) {
|
|
74
|
+
for (const dep of d) {
|
|
75
|
+
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const dependants = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const pkg of packages) {
|
|
80
|
+
for (const dep of deps.get(pkg.name) ?? []) {
|
|
81
|
+
if (!dependants.has(dep)) {
|
|
82
|
+
dependants.set(dep, /* @__PURE__ */ new Set());
|
|
83
|
+
}
|
|
84
|
+
dependants.get(dep).add(pkg.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const remaining = new Map(
|
|
88
|
+
packages.map((p) => [p.name, (deps.get(p.name) ?? /* @__PURE__ */ new Set()).size])
|
|
89
|
+
);
|
|
90
|
+
const byName = new Map(packages.map((p) => [p.name, p]));
|
|
91
|
+
const waves = [];
|
|
92
|
+
while (remaining.size > 0) {
|
|
93
|
+
const wave = [...remaining.entries()].filter(([, count]) => count === 0).map(([name]) => byName.get(name));
|
|
94
|
+
if (wave.length === 0) {
|
|
95
|
+
waves.push([...remaining.keys()].map((n) => byName.get(n)));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
waves.push(wave);
|
|
99
|
+
for (const pkg of wave) {
|
|
100
|
+
remaining.delete(pkg.name);
|
|
101
|
+
for (const dependant of dependants.get(pkg.name) ?? []) {
|
|
102
|
+
if (remaining.has(dependant)) {
|
|
103
|
+
remaining.set(dependant, (remaining.get(dependant) ?? 1) - 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return waves.filter((w) => w.length > 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/shared/publish-programmatic.ts
|
|
112
|
+
var MAX_RETRIES = 5;
|
|
113
|
+
var RETRY_BASE_DELAYS_MS = [1e4, 2e4, 4e4, 8e4, 16e4];
|
|
114
|
+
var RETRYABLE_PATTERNS = [
|
|
115
|
+
"E429",
|
|
116
|
+
"429 Too Many Requests",
|
|
117
|
+
"ECONNRESET",
|
|
118
|
+
"ETIMEDOUT",
|
|
119
|
+
"ECONNREFUSED",
|
|
120
|
+
"ENOTFOUND",
|
|
121
|
+
"EAI_AGAIN",
|
|
122
|
+
"socket hang up",
|
|
123
|
+
"network timeout",
|
|
124
|
+
"read ECONNRESET"
|
|
125
|
+
];
|
|
126
|
+
var ALREADY_PUBLISHED_PATTERNS = [
|
|
127
|
+
"You cannot publish over the previously published versions",
|
|
128
|
+
"cannot publish over the previously published version",
|
|
129
|
+
"EPUBLISHCONFLICT"
|
|
130
|
+
// Some registries return 403 for conflicts
|
|
131
|
+
];
|
|
132
|
+
function isRetryable(message) {
|
|
133
|
+
return RETRYABLE_PATTERNS.some((p) => message.includes(p));
|
|
134
|
+
}
|
|
135
|
+
function isAlreadyPublished(message) {
|
|
136
|
+
return ALREADY_PUBLISHED_PATTERNS.some((p) => message.includes(p));
|
|
137
|
+
}
|
|
138
|
+
function retryDelay(attempt) {
|
|
139
|
+
const base = RETRY_BASE_DELAYS_MS[Math.min(attempt, RETRY_BASE_DELAYS_MS.length - 1)];
|
|
140
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
141
|
+
return Math.round(base * jitter);
|
|
142
|
+
}
|
|
11
143
|
function resolveToken(token) {
|
|
12
144
|
return token ?? useEnv("NPM_TOKEN") ?? useEnv("NODE_AUTH_TOKEN");
|
|
13
145
|
}
|
|
14
|
-
function
|
|
146
|
+
function spawnPublish(options) {
|
|
15
147
|
const { packagePath, packageManager, token, otp, dryRun, tag, access, registry } = options;
|
|
16
148
|
return new Promise((resolve, reject) => {
|
|
17
149
|
const args = ["publish"];
|
|
@@ -47,6 +179,19 @@ function publishSinglePackage(options) {
|
|
|
47
179
|
writeFileSync(npmrcPath, existing + authLine);
|
|
48
180
|
}
|
|
49
181
|
}
|
|
182
|
+
const restoreNpmrc = () => {
|
|
183
|
+
if (!token) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (npmrcBackup !== null) {
|
|
188
|
+
writeFileSync(npmrcPath, npmrcBackup);
|
|
189
|
+
} else if (existsSync(npmrcPath)) {
|
|
190
|
+
unlinkSync(npmrcPath);
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
};
|
|
50
195
|
const child = spawn(packageManager, args, {
|
|
51
196
|
cwd: packagePath,
|
|
52
197
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -55,24 +200,14 @@ function publishSinglePackage(options) {
|
|
|
55
200
|
});
|
|
56
201
|
let stdout = "";
|
|
57
202
|
let stderr = "";
|
|
58
|
-
child.stdout?.on("data", (
|
|
59
|
-
stdout +=
|
|
203
|
+
child.stdout?.on("data", (d) => {
|
|
204
|
+
stdout += d.toString();
|
|
60
205
|
});
|
|
61
|
-
child.stderr?.on("data", (
|
|
62
|
-
stderr +=
|
|
206
|
+
child.stderr?.on("data", (d) => {
|
|
207
|
+
stderr += d.toString();
|
|
63
208
|
});
|
|
64
|
-
const cleanupNpmrc = () => {
|
|
65
|
-
if (!token) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (npmrcBackup !== null) {
|
|
69
|
-
writeFileSync(npmrcPath, npmrcBackup);
|
|
70
|
-
} else if (existsSync(npmrcPath)) {
|
|
71
|
-
unlinkSync(npmrcPath);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
209
|
child.on("close", (code) => {
|
|
75
|
-
|
|
210
|
+
restoreNpmrc();
|
|
76
211
|
if (code === 0) {
|
|
77
212
|
resolve();
|
|
78
213
|
} else {
|
|
@@ -80,12 +215,66 @@ function publishSinglePackage(options) {
|
|
|
80
215
|
}
|
|
81
216
|
});
|
|
82
217
|
child.on("error", (err) => {
|
|
218
|
+
restoreNpmrc();
|
|
83
219
|
reject(err);
|
|
84
220
|
});
|
|
85
221
|
});
|
|
86
222
|
}
|
|
223
|
+
async function publishOne(pkg, opts, logger) {
|
|
224
|
+
const restore = rewriteWorkspaceDeps(pkg.path, opts.versionMap, opts.packageManager);
|
|
225
|
+
try {
|
|
226
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
227
|
+
try {
|
|
228
|
+
await spawnPublish({
|
|
229
|
+
packagePath: pkg.path,
|
|
230
|
+
packageManager: opts.packageManager,
|
|
231
|
+
token: opts.token,
|
|
232
|
+
otp: opts.otp,
|
|
233
|
+
dryRun: opts.dryRun,
|
|
234
|
+
tag: opts.tag,
|
|
235
|
+
access: opts.access,
|
|
236
|
+
registry: opts.registry
|
|
237
|
+
});
|
|
238
|
+
logger.info(`Published ${pkg.name}@${pkg.version}`);
|
|
239
|
+
return { name: pkg.name, version: pkg.version, success: true };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
if (isAlreadyPublished(message)) {
|
|
243
|
+
logger.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
|
|
244
|
+
return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
|
|
245
|
+
}
|
|
246
|
+
if (isRetryable(message) && attempt < MAX_RETRIES) {
|
|
247
|
+
const delay = retryDelay(attempt);
|
|
248
|
+
logger.warn(
|
|
249
|
+
`Transient error for ${pkg.name}@${pkg.version} (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${(delay / 1e3).toFixed(1)}s: ${message.split("\n")[0]}`
|
|
250
|
+
);
|
|
251
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
logger.error(`Failed to publish ${pkg.name}@${pkg.version}`, void 0, { error: message });
|
|
255
|
+
return { name: pkg.name, version: pkg.version, success: false, error: message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
name: pkg.name,
|
|
260
|
+
version: pkg.version,
|
|
261
|
+
success: false,
|
|
262
|
+
error: `Publish failed after ${MAX_RETRIES} retries (transient errors)`
|
|
263
|
+
};
|
|
264
|
+
} finally {
|
|
265
|
+
restore();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
87
268
|
async function publishPackagesProgrammatic(options) {
|
|
88
|
-
const {
|
|
269
|
+
const {
|
|
270
|
+
packages,
|
|
271
|
+
packageManager = "pnpm",
|
|
272
|
+
dryRun,
|
|
273
|
+
otp,
|
|
274
|
+
tag,
|
|
275
|
+
access,
|
|
276
|
+
registry
|
|
277
|
+
} = options;
|
|
89
278
|
const logger = useLogger();
|
|
90
279
|
const token = resolveToken(options.token);
|
|
91
280
|
if (!token && !dryRun) {
|
|
@@ -94,192 +283,184 @@ async function publishPackagesProgrammatic(options) {
|
|
|
94
283
|
return {
|
|
95
284
|
results: [],
|
|
96
285
|
published: [],
|
|
286
|
+
alreadyPublished: [],
|
|
97
287
|
failed: packages.map((p) => `${p.name}@${p.version}`),
|
|
98
288
|
skipped: [],
|
|
99
289
|
errors: [error]
|
|
100
290
|
};
|
|
101
291
|
}
|
|
102
292
|
const versionMap = new Map(packages.map((p) => [p.name, p.version]));
|
|
103
|
-
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ??
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
deps[depName] = "*";
|
|
137
|
-
modified = true;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
|
|
141
|
-
const planVersion = versionMap.get(depName);
|
|
142
|
-
if (planVersion) {
|
|
143
|
-
deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
|
|
144
|
-
modified = true;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (modified) {
|
|
150
|
-
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
151
|
-
}
|
|
152
|
-
for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
|
|
153
|
-
try {
|
|
154
|
-
await publishSinglePackage({
|
|
155
|
-
packagePath: pkg.path,
|
|
156
|
-
packageManager,
|
|
157
|
-
token,
|
|
158
|
-
otp,
|
|
159
|
-
dryRun,
|
|
160
|
-
tag,
|
|
161
|
-
access: access ?? "public",
|
|
162
|
-
registry
|
|
163
|
-
});
|
|
164
|
-
logger.info(`Published ${pkg.name}@${pkg.version}`);
|
|
165
|
-
return { name: pkg.name, version: pkg.version, success: true };
|
|
166
|
-
} catch (error) {
|
|
167
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
-
if ((message.includes("E429") || message.includes("429 Too Many Requests")) && attempt < MAX_429_RETRIES) {
|
|
169
|
-
const delay = RETRY_429_DELAYS[attempt];
|
|
170
|
-
logger.warn(`429 rate limit for ${pkg.name}, retrying in ${delay / 1e3}s`, { attempt: attempt + 1 });
|
|
171
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
172
|
-
continue;
|
|
293
|
+
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
|
|
294
|
+
const waves = topoSort(packages);
|
|
295
|
+
logger.info(
|
|
296
|
+
`Publishing ${packages.length} package(s) in ${waves.length} topological wave(s) (concurrency=${CONCURRENCY}, dryRun=${dryRun ?? false})`
|
|
297
|
+
);
|
|
298
|
+
const allResults = [];
|
|
299
|
+
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
300
|
+
const wave = waves[waveIdx];
|
|
301
|
+
logger.info(`Wave ${waveIdx + 1}/${waves.length}: publishing ${wave.length} package(s)`);
|
|
302
|
+
for (let i = 0; i < wave.length; i += CONCURRENCY) {
|
|
303
|
+
const batch = wave.slice(i, i + CONCURRENCY);
|
|
304
|
+
const batchResults = await Promise.all(
|
|
305
|
+
batch.map(
|
|
306
|
+
(pkg) => publishOne(pkg, { packageManager, token, versionMap, dryRun, otp, tag, access, registry }, logger)
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
allResults.push(...batchResults);
|
|
310
|
+
}
|
|
311
|
+
const waveFailed = allResults.slice(-wave.length).filter((r) => !r.success);
|
|
312
|
+
if (waveFailed.length > 0) {
|
|
313
|
+
const remaining = waves.slice(waveIdx + 1).reduce((acc, w) => acc + w.length, 0);
|
|
314
|
+
if (remaining > 0) {
|
|
315
|
+
logger.warn(
|
|
316
|
+
`Wave ${waveIdx + 1} had ${waveFailed.length} failure(s) \u2014 skipping ${remaining} package(s) in later waves to avoid broken dependency chain`
|
|
317
|
+
);
|
|
318
|
+
for (const laterWave of waves.slice(waveIdx + 1)) {
|
|
319
|
+
for (const pkg of laterWave) {
|
|
320
|
+
allResults.push({
|
|
321
|
+
name: pkg.name,
|
|
322
|
+
version: pkg.version,
|
|
323
|
+
success: false,
|
|
324
|
+
error: "Skipped: dependency wave failed"
|
|
325
|
+
});
|
|
173
326
|
}
|
|
174
|
-
logger.error(`Failed to publish ${pkg.name}@${pkg.version}`, void 0, { error: message });
|
|
175
|
-
return { name: pkg.name, version: pkg.version, success: false, error: message };
|
|
176
327
|
}
|
|
177
328
|
}
|
|
178
|
-
|
|
179
|
-
} finally {
|
|
180
|
-
if (!restored) {
|
|
181
|
-
writeFileSync(pkgJsonPath, originalPkgJson);
|
|
182
|
-
restored = true;
|
|
183
|
-
}
|
|
329
|
+
break;
|
|
184
330
|
}
|
|
185
|
-
};
|
|
186
|
-
const results = [];
|
|
187
|
-
for (let i = 0; i < packages.length; i += CONCURRENCY) {
|
|
188
|
-
const batch = packages.slice(i, i + CONCURRENCY);
|
|
189
|
-
const batchResults = await Promise.all(batch.map(publishOne));
|
|
190
|
-
results.push(...batchResults);
|
|
191
331
|
}
|
|
192
|
-
const published =
|
|
193
|
-
const
|
|
194
|
-
const
|
|
332
|
+
const published = allResults.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
|
|
333
|
+
const alreadyPublished = allResults.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
|
|
334
|
+
const failed = allResults.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
|
|
335
|
+
const errors = allResults.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
|
|
336
|
+
if (alreadyPublished.length > 0) {
|
|
337
|
+
logger.info(`${alreadyPublished.length} package(s) already published \u2014 counted as success`);
|
|
338
|
+
}
|
|
195
339
|
return {
|
|
196
|
-
results,
|
|
340
|
+
results: allResults,
|
|
197
341
|
published,
|
|
342
|
+
alreadyPublished,
|
|
198
343
|
failed,
|
|
199
344
|
skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
|
|
200
345
|
errors
|
|
201
346
|
};
|
|
202
347
|
}
|
|
348
|
+
var MAX_RETRIES2 = 5;
|
|
349
|
+
var RETRY_BASE_DELAYS_MS2 = [1e4, 2e4, 4e4, 8e4, 16e4];
|
|
350
|
+
var RETRYABLE_PATTERNS2 = [
|
|
351
|
+
"E429",
|
|
352
|
+
"429 Too Many Requests",
|
|
353
|
+
"ECONNRESET",
|
|
354
|
+
"ETIMEDOUT",
|
|
355
|
+
"ECONNREFUSED",
|
|
356
|
+
"ENOTFOUND",
|
|
357
|
+
"EAI_AGAIN",
|
|
358
|
+
"socket hang up",
|
|
359
|
+
"network timeout",
|
|
360
|
+
"read ECONNRESET"
|
|
361
|
+
];
|
|
362
|
+
var ALREADY_PUBLISHED_PATTERNS2 = [
|
|
363
|
+
"You cannot publish over the previously published versions",
|
|
364
|
+
"cannot publish over the previously published version",
|
|
365
|
+
"EPUBLISHCONFLICT"
|
|
366
|
+
];
|
|
367
|
+
function isRetryable2(msg) {
|
|
368
|
+
return RETRYABLE_PATTERNS2.some((p) => msg.includes(p));
|
|
369
|
+
}
|
|
370
|
+
function isAlreadyPublished2(msg) {
|
|
371
|
+
return ALREADY_PUBLISHED_PATTERNS2.some((p) => msg.includes(p));
|
|
372
|
+
}
|
|
373
|
+
function retryDelay2(attempt) {
|
|
374
|
+
const base = RETRY_BASE_DELAYS_MS2[Math.min(attempt, RETRY_BASE_DELAYS_MS2.length - 1)];
|
|
375
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
376
|
+
return Math.round(base * jitter);
|
|
377
|
+
}
|
|
378
|
+
function publishSinglePackage(options) {
|
|
379
|
+
const { packagePath, packageManager = "pnpm", otp, dryRun, tag, access } = options;
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const args = ["publish"];
|
|
382
|
+
if (packageManager === "pnpm") {
|
|
383
|
+
args.push("--no-git-checks");
|
|
384
|
+
}
|
|
385
|
+
if (dryRun) {
|
|
386
|
+
args.push("--dry-run");
|
|
387
|
+
}
|
|
388
|
+
if (otp) {
|
|
389
|
+
args.push(`--otp=${otp}`);
|
|
390
|
+
}
|
|
391
|
+
if (tag) {
|
|
392
|
+
args.push(`--tag=${tag}`);
|
|
393
|
+
}
|
|
394
|
+
if (access) {
|
|
395
|
+
args.push(`--access=${access}`);
|
|
396
|
+
}
|
|
397
|
+
const child = spawn(packageManager, args, {
|
|
398
|
+
cwd: packagePath,
|
|
399
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
400
|
+
shell: true
|
|
401
|
+
});
|
|
402
|
+
let stdout = "";
|
|
403
|
+
let stderr = "";
|
|
404
|
+
child.stdout?.on("data", (d) => {
|
|
405
|
+
stdout += d.toString();
|
|
406
|
+
});
|
|
407
|
+
child.stderr?.on("data", (d) => {
|
|
408
|
+
stderr += d.toString();
|
|
409
|
+
});
|
|
410
|
+
child.on("close", (code) => {
|
|
411
|
+
if (code === 0) {
|
|
412
|
+
resolve();
|
|
413
|
+
} else {
|
|
414
|
+
reject(new Error(stderr || stdout || `${packageManager} publish exited with code ${code}`));
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
child.on("error", (err) => reject(err));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
203
420
|
async function publishPackagesWithOTP(options) {
|
|
204
421
|
const { packages, packageManager = "pnpm", dryRun, tag, access, ui, logger } = options;
|
|
205
422
|
let otp = options.otp;
|
|
206
423
|
const results = [];
|
|
207
424
|
const versionMap = new Map(packages.map((p) => [p.name, p.version]));
|
|
208
|
-
const
|
|
209
|
-
const pkgJsonPath = join(pkg.path, "package.json");
|
|
210
|
-
const originalPkgJson = readFileSync(pkgJsonPath, "utf-8");
|
|
211
|
-
const pkgJson = JSON.parse(originalPkgJson);
|
|
212
|
-
let modified = false;
|
|
213
|
-
for (const section of ["dependencies", "peerDependencies"]) {
|
|
214
|
-
const deps = pkgJson[section];
|
|
215
|
-
if (!deps) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
for (const [depName, depValue] of Object.entries(deps)) {
|
|
219
|
-
if (typeof depValue !== "string") {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const val = depValue;
|
|
223
|
-
if (val.startsWith("link:")) {
|
|
224
|
-
const planVersion = versionMap.get(depName);
|
|
225
|
-
if (planVersion) {
|
|
226
|
-
deps[depName] = `^${planVersion}`;
|
|
227
|
-
modified = true;
|
|
228
|
-
} else {
|
|
229
|
-
try {
|
|
230
|
-
const linkPath = val.replace("link:", "");
|
|
231
|
-
const linkedPkg = JSON.parse(readFileSync(join(pkg.path, linkPath, "package.json"), "utf-8"));
|
|
232
|
-
deps[depName] = `^${linkedPkg.version}`;
|
|
233
|
-
modified = true;
|
|
234
|
-
} catch {
|
|
235
|
-
deps[depName] = "*";
|
|
236
|
-
modified = true;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
} else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
|
|
240
|
-
const planVersion = versionMap.get(depName);
|
|
241
|
-
if (planVersion) {
|
|
242
|
-
deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
|
|
243
|
-
modified = true;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (modified) {
|
|
249
|
-
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
250
|
-
}
|
|
251
|
-
return () => writeFileSync(pkgJsonPath, originalPkgJson);
|
|
252
|
-
};
|
|
253
|
-
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 8);
|
|
425
|
+
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
|
|
254
426
|
const remaining = [];
|
|
255
427
|
let otpFailureDetected = false;
|
|
256
|
-
const MAX_429_RETRIES = 3;
|
|
257
|
-
const RETRY_429_DELAYS = [15e3, 3e4, 6e4];
|
|
258
428
|
const fastPublishOne = async (pkg) => {
|
|
259
|
-
const restore =
|
|
429
|
+
const restore = rewriteWorkspaceDeps(pkg.path, versionMap, packageManager);
|
|
260
430
|
try {
|
|
261
|
-
for (let attempt = 0; attempt <=
|
|
431
|
+
for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
|
|
262
432
|
try {
|
|
263
|
-
await
|
|
264
|
-
logger?.info("
|
|
433
|
+
await publishSinglePackage({ packagePath: pkg.path, packageManager, otp, dryRun, tag, access });
|
|
434
|
+
logger?.info("Published (fast-path)", { name: pkg.name, version: pkg.version });
|
|
265
435
|
return { name: pkg.name, version: pkg.version, success: true };
|
|
266
|
-
} catch (
|
|
267
|
-
const msg =
|
|
436
|
+
} catch (err) {
|
|
437
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
438
|
+
if (isAlreadyPublished2(msg)) {
|
|
439
|
+
logger?.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
|
|
440
|
+
return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
|
|
441
|
+
}
|
|
268
442
|
if (msg.includes("EOTP") || msg.includes("one-time password")) {
|
|
269
443
|
otpFailureDetected = true;
|
|
270
444
|
return null;
|
|
271
445
|
}
|
|
272
|
-
if ((msg
|
|
273
|
-
const delay =
|
|
274
|
-
logger?.warn(
|
|
446
|
+
if (isRetryable2(msg) && attempt < MAX_RETRIES2) {
|
|
447
|
+
const delay = retryDelay2(attempt);
|
|
448
|
+
logger?.warn(
|
|
449
|
+
`Transient error for ${pkg.name} (attempt ${attempt + 1}/${MAX_RETRIES2}), retrying in ${(delay / 1e3).toFixed(1)}s`
|
|
450
|
+
);
|
|
275
451
|
await new Promise((r) => setTimeout(r, delay));
|
|
276
452
|
continue;
|
|
277
453
|
}
|
|
278
|
-
logger?.error("
|
|
454
|
+
logger?.error("Publish failed (fast-path)", { name: pkg.name, error: msg });
|
|
279
455
|
return { name: pkg.name, version: pkg.version, success: false, error: msg };
|
|
280
456
|
}
|
|
281
457
|
}
|
|
282
|
-
return {
|
|
458
|
+
return {
|
|
459
|
+
name: pkg.name,
|
|
460
|
+
version: pkg.version,
|
|
461
|
+
success: false,
|
|
462
|
+
error: `Publish failed after ${MAX_RETRIES2} retries (transient errors)`
|
|
463
|
+
};
|
|
283
464
|
} finally {
|
|
284
465
|
restore();
|
|
285
466
|
}
|
|
@@ -303,231 +484,87 @@ async function publishPackagesWithOTP(options) {
|
|
|
303
484
|
}
|
|
304
485
|
}
|
|
305
486
|
if (remaining.length === 0) {
|
|
306
|
-
|
|
307
|
-
const failed2 = results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
|
|
308
|
-
const errors2 = results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
|
|
309
|
-
return {
|
|
310
|
-
results,
|
|
311
|
-
published: published2,
|
|
312
|
-
failed: failed2,
|
|
313
|
-
skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
|
|
314
|
-
errors: errors2
|
|
315
|
-
};
|
|
487
|
+
return buildResult(results, packages, dryRun);
|
|
316
488
|
}
|
|
317
489
|
ui.write?.(` OTP required \u2014 falling back to sequential publish for ${remaining.length} package(s)
|
|
318
490
|
`);
|
|
319
491
|
for (const pkg of remaining) {
|
|
320
|
-
logger?.info("Publishing package", { name: pkg.name, version: pkg.version });
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
492
|
+
logger?.info("Publishing package (sequential)", { name: pkg.name, version: pkg.version });
|
|
493
|
+
const loader = useLoader(`Publishing ${pkg.name}@${pkg.version}...`);
|
|
494
|
+
loader.start();
|
|
495
|
+
const restore = rewriteWorkspaceDeps(pkg.path, versionMap, packageManager);
|
|
324
496
|
try {
|
|
325
|
-
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
for (const [depName, depValue] of Object.entries(deps)) {
|
|
333
|
-
if (typeof depValue !== "string") {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const val = depValue;
|
|
337
|
-
if (val.startsWith("link:")) {
|
|
338
|
-
const planVersion = versionMap.get(depName);
|
|
339
|
-
if (planVersion) {
|
|
340
|
-
deps[depName] = `^${planVersion}`;
|
|
341
|
-
modified = true;
|
|
342
|
-
} else {
|
|
343
|
-
try {
|
|
344
|
-
const linkPath = val.replace("link:", "");
|
|
345
|
-
const linkedPkg = JSON.parse(readFileSync(join(pkg.path, linkPath, "package.json"), "utf-8"));
|
|
346
|
-
deps[depName] = `^${linkedPkg.version}`;
|
|
347
|
-
modified = true;
|
|
348
|
-
} catch {
|
|
349
|
-
deps[depName] = "*";
|
|
350
|
-
modified = true;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
} else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
|
|
354
|
-
const planVersion = versionMap.get(depName);
|
|
355
|
-
if (planVersion) {
|
|
356
|
-
deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
|
|
357
|
-
modified = true;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (modified) {
|
|
363
|
-
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
364
|
-
logger?.info(`Replaced link: deps for ${pkg.name}`);
|
|
365
|
-
}
|
|
366
|
-
const loader = useLoader(`Publishing ${pkg.name}@${pkg.version}...`);
|
|
367
|
-
loader.start();
|
|
368
|
-
let attempts = 0;
|
|
369
|
-
const maxAttempts = 3;
|
|
370
|
-
let published2 = false;
|
|
371
|
-
while (!published2 && attempts < maxAttempts) {
|
|
372
|
-
attempts++;
|
|
497
|
+
let published = false;
|
|
498
|
+
let otpAttempts = 0;
|
|
499
|
+
const MAX_OTP_ATTEMPTS = 3;
|
|
500
|
+
while (!published && otpAttempts < MAX_OTP_ATTEMPTS) {
|
|
501
|
+
otpAttempts++;
|
|
373
502
|
try {
|
|
374
|
-
await
|
|
375
|
-
|
|
376
|
-
packageManager,
|
|
377
|
-
otp,
|
|
378
|
-
dryRun,
|
|
379
|
-
tag,
|
|
380
|
-
access
|
|
381
|
-
});
|
|
382
|
-
published2 = true;
|
|
503
|
+
await publishSinglePackage({ packagePath: pkg.path, packageManager, otp, dryRun, tag, access });
|
|
504
|
+
published = true;
|
|
383
505
|
results.push({ name: pkg.name, version: pkg.version, success: true });
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
if (
|
|
506
|
+
loader.succeed(dryRun ? `Dry-run: ${pkg.name}@${pkg.version}` : `Published ${pkg.name}@${pkg.version}`);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
509
|
+
if (isAlreadyPublished2(msg)) {
|
|
510
|
+
published = true;
|
|
511
|
+
results.push({ name: pkg.name, version: pkg.version, success: true, alreadyPublished: true });
|
|
512
|
+
loader.succeed(`${pkg.name}@${pkg.version} already published`);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
if (msg.includes("EOTP") || msg.includes("one-time password")) {
|
|
516
|
+
if (otpAttempts < MAX_OTP_ATTEMPTS) {
|
|
395
517
|
loader.succeed(`\u{1F510} 2FA required for ${pkg.name}`);
|
|
396
|
-
|
|
397
|
-
name: pkg.name,
|
|
398
|
-
attempt: attempts
|
|
399
|
-
});
|
|
400
|
-
const rl = readline.createInterface({
|
|
401
|
-
input: process.stdin,
|
|
402
|
-
output: process.stdout
|
|
403
|
-
});
|
|
518
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
404
519
|
try {
|
|
405
|
-
|
|
520
|
+
const entered = await rl.question(" Enter OTP code: ");
|
|
406
521
|
rl.close();
|
|
407
|
-
if (!
|
|
408
|
-
ui.write?.(" \u26A0\uFE0F Invalid OTP
|
|
409
|
-
logger?.warn("Invalid OTP code provided", { length: otp?.trim().length });
|
|
522
|
+
if (!entered || entered.trim().length !== 6) {
|
|
523
|
+
ui.write?.(" \u26A0\uFE0F Invalid OTP (must be 6 digits)\n");
|
|
410
524
|
otp = void 0;
|
|
411
|
-
|
|
525
|
+
} else {
|
|
526
|
+
otp = entered.trim();
|
|
412
527
|
}
|
|
413
|
-
loader.update({ text: `Publishing ${pkg.name}@${pkg.version} with OTP...` });
|
|
414
|
-
loader.start();
|
|
415
|
-
logger?.debug("Retrying with OTP");
|
|
416
528
|
} catch (e) {
|
|
417
529
|
rl.close();
|
|
418
530
|
throw e;
|
|
419
531
|
}
|
|
420
|
-
|
|
421
|
-
loader.fail("Max OTP attempts reached");
|
|
422
|
-
logger?.error("Max OTP attempts reached", {
|
|
423
|
-
name: pkg.name,
|
|
424
|
-
attempts: maxAttempts
|
|
425
|
-
});
|
|
426
|
-
results.push({
|
|
427
|
-
name: pkg.name,
|
|
428
|
-
version: pkg.version,
|
|
429
|
-
success: false,
|
|
430
|
-
error: "Max OTP attempts reached"
|
|
431
|
-
});
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
} else if (errorMessage.includes("E429") || errorMessage.includes("429 Too Many Requests")) {
|
|
435
|
-
if (attempts < maxAttempts) {
|
|
436
|
-
const delay = RETRY_429_DELAYS[Math.min(attempts - 1, RETRY_429_DELAYS.length - 1)];
|
|
437
|
-
loader.update({ text: `429 rate limit \u2014 waiting ${delay / 1e3}s before retry...` });
|
|
532
|
+
loader.update({ text: `Publishing ${pkg.name}@${pkg.version} with OTP...` });
|
|
438
533
|
loader.start();
|
|
439
|
-
logger?.warn(`429 rate limit for ${pkg.name}, retrying in ${delay / 1e3}s`, { attempt: attempts });
|
|
440
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
441
534
|
} else {
|
|
442
|
-
loader.fail("Max
|
|
443
|
-
results.push({
|
|
444
|
-
name: pkg.name,
|
|
445
|
-
version: pkg.version,
|
|
446
|
-
success: false,
|
|
447
|
-
error: "429 rate limit: max retries exceeded"
|
|
448
|
-
});
|
|
535
|
+
loader.fail("Max OTP attempts reached");
|
|
536
|
+
results.push({ name: pkg.name, version: pkg.version, success: false, error: "Max OTP attempts reached" });
|
|
449
537
|
break;
|
|
450
538
|
}
|
|
539
|
+
} else if (isRetryable2(msg)) {
|
|
540
|
+
const delay = retryDelay2(MAX_RETRIES2 - 1);
|
|
541
|
+
loader.update({ text: `Rate limited \u2014 waiting ${(delay / 1e3).toFixed(0)}s\u2026` });
|
|
542
|
+
loader.start();
|
|
543
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
544
|
+
otpAttempts--;
|
|
451
545
|
} else {
|
|
452
|
-
loader.fail(`Failed: ${
|
|
453
|
-
|
|
454
|
-
name: pkg.name,
|
|
455
|
-
version: pkg.version,
|
|
456
|
-
error: errorMessage
|
|
457
|
-
});
|
|
458
|
-
results.push({
|
|
459
|
-
name: pkg.name,
|
|
460
|
-
version: pkg.version,
|
|
461
|
-
success: false,
|
|
462
|
-
error: errorMessage
|
|
463
|
-
});
|
|
546
|
+
loader.fail(`Failed: ${msg.split("\n")[0]}`);
|
|
547
|
+
results.push({ name: pkg.name, version: pkg.version, success: false, error: msg });
|
|
464
548
|
break;
|
|
465
549
|
}
|
|
466
550
|
}
|
|
467
551
|
}
|
|
468
552
|
} finally {
|
|
469
|
-
|
|
470
|
-
writeFileSync(pkgJsonPath, originalPkgJson);
|
|
471
|
-
restored = true;
|
|
472
|
-
}
|
|
553
|
+
restore();
|
|
473
554
|
}
|
|
474
555
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
556
|
+
return buildResult(results, packages, dryRun);
|
|
557
|
+
}
|
|
558
|
+
function buildResult(results, packages, dryRun) {
|
|
478
559
|
return {
|
|
479
560
|
results,
|
|
480
|
-
published,
|
|
481
|
-
|
|
561
|
+
published: results.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
|
|
562
|
+
alreadyPublished: results.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
|
|
563
|
+
failed: results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`),
|
|
482
564
|
skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
|
|
483
|
-
errors
|
|
565
|
+
errors: results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`)
|
|
484
566
|
};
|
|
485
567
|
}
|
|
486
|
-
function publishSinglePackage2(options) {
|
|
487
|
-
const { packagePath, packageManager = "pnpm", otp, dryRun, tag, access } = options;
|
|
488
|
-
return new Promise((resolve, reject) => {
|
|
489
|
-
const args = ["publish"];
|
|
490
|
-
if (packageManager === "pnpm") {
|
|
491
|
-
args.push("--no-git-checks");
|
|
492
|
-
}
|
|
493
|
-
if (dryRun) {
|
|
494
|
-
args.push("--dry-run");
|
|
495
|
-
}
|
|
496
|
-
if (otp) {
|
|
497
|
-
args.push(`--otp=${otp}`);
|
|
498
|
-
}
|
|
499
|
-
if (tag) {
|
|
500
|
-
args.push(`--tag=${tag}`);
|
|
501
|
-
}
|
|
502
|
-
if (access) {
|
|
503
|
-
args.push(`--access=${access}`);
|
|
504
|
-
}
|
|
505
|
-
const child = spawn(packageManager, args, {
|
|
506
|
-
cwd: packagePath,
|
|
507
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
508
|
-
shell: true
|
|
509
|
-
});
|
|
510
|
-
let stdout = "";
|
|
511
|
-
let stderr = "";
|
|
512
|
-
child.stdout?.on("data", (data) => {
|
|
513
|
-
stdout += data.toString();
|
|
514
|
-
});
|
|
515
|
-
child.stderr?.on("data", (data) => {
|
|
516
|
-
stderr += data.toString();
|
|
517
|
-
});
|
|
518
|
-
child.on("close", (code) => {
|
|
519
|
-
if (code === 0) {
|
|
520
|
-
resolve();
|
|
521
|
-
} else {
|
|
522
|
-
const errorOutput = stderr || stdout;
|
|
523
|
-
reject(new Error(errorOutput));
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
child.on("error", (err) => {
|
|
527
|
-
reject(err);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
568
|
|
|
532
569
|
// src/cli/commands/publish.ts
|
|
533
570
|
var publish_default = defineCommand({
|