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