@kb-labs/release-manager-cli 2.100.0 → 2.101.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/plan.d.ts +2 -0
- package/dist/cli/commands/plan.js +4 -2
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/promote.d.ts +16 -0
- package/dist/cli/commands/promote.js +709 -0
- package/dist/cli/commands/promote.js.map +1 -0
- package/dist/cli/commands/publish.js +21 -11
- package/dist/cli/commands/publish.js.map +1 -1
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +33 -16
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/contracts/release.schema.d.ts +12 -0
- package/dist/contracts/release.schema.js +2 -1
- package/dist/contracts/release.schema.js.map +1 -1
- package/dist/index.js +44 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +44 -1
- package/dist/manifest.js.map +1 -1
- package/dist/rest/handlers/generate-handler.d.ts +1 -0
- package/dist/rest/handlers/generate-handler.js +1 -0
- package/dist/rest/handlers/generate-handler.js.map +1 -1
- package/dist/rest/handlers/history-plan-handler.d.ts +1 -0
- package/dist/rest/handlers/history-report-handler.d.ts +1 -0
- package/dist/rest/handlers/plan-handler.d.ts +1 -0
- package/dist/rest/handlers/report-handler.d.ts +1 -0
- package/dist/rest/handlers/run-handler.d.ts +2 -0
- package/dist/rest/handlers/run-handler.js +17 -5
- package/dist/rest/handlers/run-handler.js.map +1 -1
- package/dist/widgets/117.js +1 -1
- package/dist/widgets/165.js +1 -1
- package/dist/widgets/370.js +1 -1
- package/dist/widgets/46.js +1 -1
- package/dist/widgets/50.js +1 -1
- package/dist/widgets/546.js +1 -1
- package/dist/widgets/62.js +1 -1
- package/dist/widgets/669.js +1 -1
- package/dist/widgets/733.js +2 -2
- package/dist/widgets/775.js +1 -1
- package/dist/widgets/78.js +1 -0
- package/dist/widgets/__federation_expose_ReleasePage.js +1 -1
- package/dist/widgets/mf-manifest.json +14 -14
- package/dist/widgets/mf-stats.json +20 -20
- package/dist/widgets/remoteEntry.js +3 -3
- package/package.json +7 -7
- package/dist/widgets/697.js +0 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { defineCommand, useEnv, findRepoRoot, useConfig, useLoader, useLogger } from '@kb-labs/sdk';
|
|
2
|
+
import { resolvePublishTag, resolvePublishRegistry, discoverCurrentPackages } from '@kb-labs/release-manager-core';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import * as readline from 'readline/promises';
|
|
7
|
+
|
|
8
|
+
// src/cli/commands/promote.ts
|
|
9
|
+
function rewriteLinkDep(deps, depName, val, pkgPath, versionMap) {
|
|
10
|
+
const pinned = versionMap.get(depName);
|
|
11
|
+
if (pinned) {
|
|
12
|
+
deps[depName] = `^${pinned}`;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const linkPath = val.slice("link:".length);
|
|
17
|
+
const linked = JSON.parse(readFileSync(join(pkgPath, linkPath, "package.json"), "utf-8"));
|
|
18
|
+
deps[depName] = `^${linked.version ?? "*"}`;
|
|
19
|
+
} catch {
|
|
20
|
+
deps[depName] = "*";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function rewriteWorkspaceDep(deps, depName, val, versionMap) {
|
|
24
|
+
const pinned = versionMap.get(depName);
|
|
25
|
+
if (!pinned) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
deps[depName] = val === "workspace:*" ? `^${pinned}` : val.replace("workspace:", "");
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function rewriteDepsSection(deps, pkgPath, versionMap, packageManager) {
|
|
32
|
+
let modified = false;
|
|
33
|
+
for (const depName of Object.keys(deps)) {
|
|
34
|
+
const val = deps[depName];
|
|
35
|
+
if (typeof val !== "string") {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (val.startsWith("link:")) {
|
|
39
|
+
rewriteLinkDep(deps, depName, val, pkgPath, versionMap);
|
|
40
|
+
modified = true;
|
|
41
|
+
} else if (val.startsWith("workspace:") && packageManager !== "pnpm" && rewriteWorkspaceDep(deps, depName, val, versionMap)) {
|
|
42
|
+
modified = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return modified;
|
|
46
|
+
}
|
|
47
|
+
function rewriteWorkspaceDeps(pkg, versionMap, packageManager) {
|
|
48
|
+
const pkgPath = pkg.path;
|
|
49
|
+
const pkgJsonPath = join(pkgPath, "package.json");
|
|
50
|
+
const original = readFileSync(pkgJsonPath, "utf-8");
|
|
51
|
+
const pkgJson = JSON.parse(original);
|
|
52
|
+
let modified = false;
|
|
53
|
+
if (pkgJson.version !== pkg.version) {
|
|
54
|
+
pkgJson.version = pkg.version;
|
|
55
|
+
modified = true;
|
|
56
|
+
}
|
|
57
|
+
for (const section of ["dependencies", "peerDependencies"]) {
|
|
58
|
+
const deps = pkgJson[section];
|
|
59
|
+
if (!deps) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (rewriteDepsSection(deps, pkgPath, versionMap, packageManager)) {
|
|
63
|
+
modified = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (modified) {
|
|
67
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
return () => writeFileSync(pkgJsonPath, original);
|
|
70
|
+
}
|
|
71
|
+
function buildIntraDeps(packages, nameSet) {
|
|
72
|
+
const deps = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const pkg of packages) {
|
|
74
|
+
const intra = /* @__PURE__ */ new Set();
|
|
75
|
+
try {
|
|
76
|
+
const pkgJson = JSON.parse(readFileSync(join(pkg.path, "package.json"), "utf-8"));
|
|
77
|
+
for (const section of ["dependencies", "peerDependencies"]) {
|
|
78
|
+
for (const depName of Object.keys(pkgJson[section] ?? {})) {
|
|
79
|
+
if (nameSet.has(depName)) {
|
|
80
|
+
intra.add(depName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
deps.set(pkg.name, intra);
|
|
87
|
+
}
|
|
88
|
+
return deps;
|
|
89
|
+
}
|
|
90
|
+
function buildDependantsMap(packages, deps) {
|
|
91
|
+
const dependants = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const pkg of packages) {
|
|
93
|
+
for (const dep of deps.get(pkg.name) ?? []) {
|
|
94
|
+
if (!dependants.has(dep)) {
|
|
95
|
+
dependants.set(dep, /* @__PURE__ */ new Set());
|
|
96
|
+
}
|
|
97
|
+
dependants.get(dep).add(pkg.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return dependants;
|
|
101
|
+
}
|
|
102
|
+
function computeWaves(packages, deps, dependants) {
|
|
103
|
+
const byName = new Map(packages.map((p) => [p.name, p]));
|
|
104
|
+
const remaining = new Map(
|
|
105
|
+
packages.map((p) => [p.name, (deps.get(p.name) ?? /* @__PURE__ */ new Set()).size])
|
|
106
|
+
);
|
|
107
|
+
const waves = [];
|
|
108
|
+
while (remaining.size > 0) {
|
|
109
|
+
const wave = [...remaining.entries()].filter(([, count]) => count === 0).map(([name]) => byName.get(name));
|
|
110
|
+
if (wave.length === 0) {
|
|
111
|
+
waves.push([...remaining.keys()].map((n) => byName.get(n)));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
waves.push(wave);
|
|
115
|
+
for (const pkg of wave) {
|
|
116
|
+
remaining.delete(pkg.name);
|
|
117
|
+
for (const dependant of dependants.get(pkg.name) ?? []) {
|
|
118
|
+
if (remaining.has(dependant)) {
|
|
119
|
+
remaining.set(dependant, (remaining.get(dependant) ?? 1) - 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return waves.filter((w) => w.length > 0);
|
|
125
|
+
}
|
|
126
|
+
function topoSort(packages, _packageManager) {
|
|
127
|
+
const nameSet = new Set(packages.map((p) => p.name));
|
|
128
|
+
const deps = buildIntraDeps(packages, nameSet);
|
|
129
|
+
const dependants = buildDependantsMap(packages, deps);
|
|
130
|
+
return computeWaves(packages, deps, dependants);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/shared/publish-programmatic.ts
|
|
134
|
+
var MAX_RETRIES = 5;
|
|
135
|
+
var RETRY_BASE_DELAYS_MS = [1e4, 2e4, 4e4, 8e4, 16e4];
|
|
136
|
+
var RETRYABLE_PATTERNS = [
|
|
137
|
+
"E429",
|
|
138
|
+
"429 Too Many Requests",
|
|
139
|
+
"ECONNRESET",
|
|
140
|
+
"ETIMEDOUT",
|
|
141
|
+
"ECONNREFUSED",
|
|
142
|
+
"ENOTFOUND",
|
|
143
|
+
"EAI_AGAIN",
|
|
144
|
+
"socket hang up",
|
|
145
|
+
"network timeout",
|
|
146
|
+
"read ECONNRESET"
|
|
147
|
+
];
|
|
148
|
+
var ALREADY_PUBLISHED_PATTERNS = [
|
|
149
|
+
"You cannot publish over the previously published versions",
|
|
150
|
+
"cannot publish over the previously published version",
|
|
151
|
+
"EPUBLISHCONFLICT"
|
|
152
|
+
// Some registries return 403 for conflicts
|
|
153
|
+
];
|
|
154
|
+
function isRetryable(message) {
|
|
155
|
+
return RETRYABLE_PATTERNS.some((p) => message.includes(p));
|
|
156
|
+
}
|
|
157
|
+
function isAlreadyPublished(message) {
|
|
158
|
+
return ALREADY_PUBLISHED_PATTERNS.some((p) => message.includes(p));
|
|
159
|
+
}
|
|
160
|
+
function retryDelay(attempt) {
|
|
161
|
+
const base = RETRY_BASE_DELAYS_MS[Math.min(attempt, RETRY_BASE_DELAYS_MS.length - 1)];
|
|
162
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
163
|
+
return Math.round(base * jitter);
|
|
164
|
+
}
|
|
165
|
+
function resolveToken(token) {
|
|
166
|
+
return token ?? useEnv("NPM_TOKEN") ?? useEnv("NODE_AUTH_TOKEN");
|
|
167
|
+
}
|
|
168
|
+
function spawnPublish(options) {
|
|
169
|
+
const { packagePath, packageManager, token, otp, dryRun, tag, access, registry } = options;
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const args = ["publish"];
|
|
172
|
+
if (packageManager === "pnpm") {
|
|
173
|
+
args.push("--no-git-checks");
|
|
174
|
+
}
|
|
175
|
+
if (dryRun) {
|
|
176
|
+
args.push("--dry-run");
|
|
177
|
+
}
|
|
178
|
+
if (tag) {
|
|
179
|
+
args.push(`--tag=${tag}`);
|
|
180
|
+
}
|
|
181
|
+
if (access) {
|
|
182
|
+
args.push(`--access=${access}`);
|
|
183
|
+
}
|
|
184
|
+
if (registry) {
|
|
185
|
+
args.push(`--registry=${registry}`);
|
|
186
|
+
}
|
|
187
|
+
if (otp) {
|
|
188
|
+
args.push(`--otp=${otp}`);
|
|
189
|
+
}
|
|
190
|
+
const env = { ...process.env };
|
|
191
|
+
if (token) {
|
|
192
|
+
env["NODE_AUTH_TOKEN"] = token;
|
|
193
|
+
}
|
|
194
|
+
const npmrcPath = join(packagePath, ".npmrc");
|
|
195
|
+
const npmrcExisted = existsSync(npmrcPath);
|
|
196
|
+
const npmrcBackup = npmrcExisted ? readFileSync(npmrcPath, "utf-8") : null;
|
|
197
|
+
const registryHost = registry ? new URL(registry).host : "registry.npmjs.org";
|
|
198
|
+
const authLine = `//${registryHost}/:_authToken=\${NODE_AUTH_TOKEN}
|
|
199
|
+
`;
|
|
200
|
+
if (token) {
|
|
201
|
+
const existing = npmrcExisted ? readFileSync(npmrcPath, "utf-8") : "";
|
|
202
|
+
if (!existing.includes("_authToken")) {
|
|
203
|
+
writeFileSync(npmrcPath, existing + authLine);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const restoreNpmrc = () => {
|
|
207
|
+
if (!token) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
if (npmrcBackup !== null) {
|
|
212
|
+
writeFileSync(npmrcPath, npmrcBackup);
|
|
213
|
+
} else if (existsSync(npmrcPath)) {
|
|
214
|
+
unlinkSync(npmrcPath);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const child = spawn(packageManager, args, {
|
|
220
|
+
cwd: packagePath,
|
|
221
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
222
|
+
shell: true,
|
|
223
|
+
env
|
|
224
|
+
});
|
|
225
|
+
let stdout = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
child.stdout?.on("data", (d) => {
|
|
228
|
+
stdout += d.toString();
|
|
229
|
+
});
|
|
230
|
+
child.stderr?.on("data", (d) => {
|
|
231
|
+
stderr += d.toString();
|
|
232
|
+
});
|
|
233
|
+
child.on("close", (code) => {
|
|
234
|
+
restoreNpmrc();
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve();
|
|
237
|
+
} else {
|
|
238
|
+
reject(new Error(stderr || stdout || `${packageManager} publish exited with code ${code}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
child.on("error", (err) => {
|
|
242
|
+
restoreNpmrc();
|
|
243
|
+
reject(err);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function publishOne(pkg, opts, logger) {
|
|
248
|
+
const restore = rewriteWorkspaceDeps(pkg, opts.versionMap, opts.packageManager);
|
|
249
|
+
try {
|
|
250
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
251
|
+
try {
|
|
252
|
+
await spawnPublish({
|
|
253
|
+
packagePath: pkg.path,
|
|
254
|
+
packageManager: opts.packageManager,
|
|
255
|
+
token: opts.token,
|
|
256
|
+
otp: opts.otp,
|
|
257
|
+
dryRun: opts.dryRun,
|
|
258
|
+
tag: opts.tag,
|
|
259
|
+
access: opts.access,
|
|
260
|
+
registry: opts.registry
|
|
261
|
+
});
|
|
262
|
+
logger.info(`Published ${pkg.name}@${pkg.version}`);
|
|
263
|
+
return { name: pkg.name, version: pkg.version, success: true };
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
266
|
+
if (isAlreadyPublished(message)) {
|
|
267
|
+
logger.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
|
|
268
|
+
return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
|
|
269
|
+
}
|
|
270
|
+
if (isRetryable(message) && attempt < MAX_RETRIES) {
|
|
271
|
+
const delay = retryDelay(attempt);
|
|
272
|
+
logger.warn(
|
|
273
|
+
`Transient error for ${pkg.name}@${pkg.version} (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${(delay / 1e3).toFixed(1)}s: ${message.split("\n")[0]}`
|
|
274
|
+
);
|
|
275
|
+
await new Promise((r) => {
|
|
276
|
+
setTimeout(r, delay);
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
logger.error(`Failed to publish ${pkg.name}@${pkg.version}`, void 0, { error: message });
|
|
281
|
+
return { name: pkg.name, version: pkg.version, success: false, error: message };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
name: pkg.name,
|
|
286
|
+
version: pkg.version,
|
|
287
|
+
success: false,
|
|
288
|
+
error: `Publish failed after ${MAX_RETRIES} retries (transient errors)`
|
|
289
|
+
};
|
|
290
|
+
} finally {
|
|
291
|
+
restore();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function skipRemainingWaves(waves, waveIdx, waveFailed, logger, allResults) {
|
|
295
|
+
const remaining = waves.slice(waveIdx + 1).reduce((acc, w) => acc + w.length, 0);
|
|
296
|
+
if (remaining === 0) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
logger.warn(
|
|
300
|
+
`Wave ${waveIdx + 1} had ${waveFailed.length} failure(s) \u2014 skipping ${remaining} package(s) in later waves to avoid broken dependency chain`
|
|
301
|
+
);
|
|
302
|
+
for (const laterWave of waves.slice(waveIdx + 1)) {
|
|
303
|
+
for (const pkg of laterWave) {
|
|
304
|
+
allResults.push({ name: pkg.name, version: pkg.version, success: false, error: "Skipped: dependency wave failed" });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function publishPackagesProgrammatic(options) {
|
|
309
|
+
const {
|
|
310
|
+
packages,
|
|
311
|
+
packageManager = "pnpm",
|
|
312
|
+
dryRun,
|
|
313
|
+
otp,
|
|
314
|
+
tag,
|
|
315
|
+
access,
|
|
316
|
+
registry
|
|
317
|
+
} = options;
|
|
318
|
+
const logger = useLogger();
|
|
319
|
+
const token = resolveToken(options.token);
|
|
320
|
+
if (!token && !dryRun) {
|
|
321
|
+
const error = "No npm token found. Set NPM_TOKEN (or NODE_AUTH_TOKEN) in environment.";
|
|
322
|
+
logger.error(error);
|
|
323
|
+
return {
|
|
324
|
+
results: [],
|
|
325
|
+
published: [],
|
|
326
|
+
alreadyPublished: [],
|
|
327
|
+
failed: packages.map((p) => `${p.name}@${p.version}`),
|
|
328
|
+
skipped: [],
|
|
329
|
+
errors: [error]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const versionMap = new Map(packages.map((p) => [p.name, p.version]));
|
|
333
|
+
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
|
|
334
|
+
const waves = topoSort(packages);
|
|
335
|
+
logger.info(
|
|
336
|
+
`Publishing ${packages.length} package(s) in ${waves.length} topological wave(s) (concurrency=${CONCURRENCY}, dryRun=${dryRun ?? false})`
|
|
337
|
+
);
|
|
338
|
+
const allResults = [];
|
|
339
|
+
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
340
|
+
const wave = waves[waveIdx];
|
|
341
|
+
logger.info(`Wave ${waveIdx + 1}/${waves.length}: publishing ${wave.length} package(s)`);
|
|
342
|
+
for (let i = 0; i < wave.length; i += CONCURRENCY) {
|
|
343
|
+
const batch = wave.slice(i, i + CONCURRENCY);
|
|
344
|
+
const batchResults = await Promise.all(
|
|
345
|
+
batch.map(
|
|
346
|
+
(pkg) => publishOne(pkg, { packageManager, token, versionMap, dryRun, otp, tag, access, registry }, logger)
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
allResults.push(...batchResults);
|
|
350
|
+
}
|
|
351
|
+
const waveFailed = allResults.slice(-wave.length).filter((r) => !r.success);
|
|
352
|
+
if (waveFailed.length > 0) {
|
|
353
|
+
skipRemainingWaves(waves, waveIdx, waveFailed, logger, allResults);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const published = allResults.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
|
|
358
|
+
const alreadyPublished = allResults.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
|
|
359
|
+
const failed = allResults.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
|
|
360
|
+
const errors = allResults.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
|
|
361
|
+
if (alreadyPublished.length > 0) {
|
|
362
|
+
logger.info(`${alreadyPublished.length} package(s) already published \u2014 counted as success`);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
results: allResults,
|
|
366
|
+
published,
|
|
367
|
+
alreadyPublished,
|
|
368
|
+
failed,
|
|
369
|
+
skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
|
|
370
|
+
errors
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
var MAX_RETRIES2 = 5;
|
|
374
|
+
var RETRY_BASE_DELAYS_MS2 = [1e4, 2e4, 4e4, 8e4, 16e4];
|
|
375
|
+
var RETRYABLE_PATTERNS2 = [
|
|
376
|
+
"E429",
|
|
377
|
+
"429 Too Many Requests",
|
|
378
|
+
"ECONNRESET",
|
|
379
|
+
"ETIMEDOUT",
|
|
380
|
+
"ECONNREFUSED",
|
|
381
|
+
"ENOTFOUND",
|
|
382
|
+
"EAI_AGAIN",
|
|
383
|
+
"socket hang up",
|
|
384
|
+
"network timeout",
|
|
385
|
+
"read ECONNRESET"
|
|
386
|
+
];
|
|
387
|
+
var ALREADY_PUBLISHED_PATTERNS2 = [
|
|
388
|
+
"You cannot publish over the previously published versions",
|
|
389
|
+
"cannot publish over the previously published version",
|
|
390
|
+
"EPUBLISHCONFLICT"
|
|
391
|
+
];
|
|
392
|
+
function isRetryable2(msg) {
|
|
393
|
+
return RETRYABLE_PATTERNS2.some((p) => msg.includes(p));
|
|
394
|
+
}
|
|
395
|
+
function isAlreadyPublished2(msg) {
|
|
396
|
+
return ALREADY_PUBLISHED_PATTERNS2.some((p) => msg.includes(p));
|
|
397
|
+
}
|
|
398
|
+
function retryDelay2(attempt) {
|
|
399
|
+
const base = RETRY_BASE_DELAYS_MS2[Math.min(attempt, RETRY_BASE_DELAYS_MS2.length - 1)];
|
|
400
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
401
|
+
return Math.round(base * jitter);
|
|
402
|
+
}
|
|
403
|
+
function publishSinglePackage(options) {
|
|
404
|
+
const { packagePath, packageManager = "pnpm", otp, dryRun, tag, access, registry } = options;
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
const args = ["publish"];
|
|
407
|
+
if (packageManager === "pnpm") {
|
|
408
|
+
args.push("--no-git-checks");
|
|
409
|
+
}
|
|
410
|
+
if (dryRun) {
|
|
411
|
+
args.push("--dry-run");
|
|
412
|
+
}
|
|
413
|
+
if (otp) {
|
|
414
|
+
args.push(`--otp=${otp}`);
|
|
415
|
+
}
|
|
416
|
+
if (tag) {
|
|
417
|
+
args.push(`--tag=${tag}`);
|
|
418
|
+
}
|
|
419
|
+
if (access) {
|
|
420
|
+
args.push(`--access=${access}`);
|
|
421
|
+
}
|
|
422
|
+
if (registry) {
|
|
423
|
+
args.push(`--registry=${registry}`);
|
|
424
|
+
}
|
|
425
|
+
const child = spawn(packageManager, args, {
|
|
426
|
+
cwd: packagePath,
|
|
427
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
428
|
+
shell: true
|
|
429
|
+
});
|
|
430
|
+
let stdout = "";
|
|
431
|
+
let stderr = "";
|
|
432
|
+
child.stdout?.on("data", (d) => {
|
|
433
|
+
stdout += d.toString();
|
|
434
|
+
});
|
|
435
|
+
child.stderr?.on("data", (d) => {
|
|
436
|
+
stderr += d.toString();
|
|
437
|
+
});
|
|
438
|
+
child.on("close", (code) => {
|
|
439
|
+
if (code === 0) {
|
|
440
|
+
resolve();
|
|
441
|
+
} else {
|
|
442
|
+
reject(new Error(stderr || stdout || `${packageManager} publish exited with code ${code}`));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
child.on("error", (err) => reject(err));
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
async function fastPublishPkg(pkg, ctx, logger) {
|
|
449
|
+
const restore = rewriteWorkspaceDeps(pkg, ctx.versionMap, ctx.packageManager);
|
|
450
|
+
try {
|
|
451
|
+
for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
|
|
452
|
+
try {
|
|
453
|
+
await publishSinglePackage({ packagePath: pkg.path, packageManager: ctx.packageManager, otp: ctx.otp, dryRun: ctx.dryRun, tag: ctx.tag, access: ctx.access, registry: ctx.registry });
|
|
454
|
+
logger?.info("Published (fast-path)", { name: pkg.name, version: pkg.version });
|
|
455
|
+
return { result: { name: pkg.name, version: pkg.version, success: true }, otpFailed: false };
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
458
|
+
if (isAlreadyPublished2(msg)) {
|
|
459
|
+
logger?.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
|
|
460
|
+
return { result: { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true }, otpFailed: false };
|
|
461
|
+
}
|
|
462
|
+
if (msg.includes("EOTP") || msg.includes("one-time password")) {
|
|
463
|
+
return { result: null, otpFailed: true };
|
|
464
|
+
}
|
|
465
|
+
if (isRetryable2(msg) && attempt < MAX_RETRIES2) {
|
|
466
|
+
const delay = retryDelay2(attempt);
|
|
467
|
+
logger?.warn(`Transient error for ${pkg.name} (attempt ${attempt + 1}/${MAX_RETRIES2}), retrying in ${(delay / 1e3).toFixed(1)}s`);
|
|
468
|
+
await new Promise((r) => {
|
|
469
|
+
setTimeout(r, delay);
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
logger?.error("Publish failed (fast-path)", void 0, { name: pkg.name, error: msg });
|
|
474
|
+
return { result: { name: pkg.name, version: pkg.version, success: false, error: msg }, otpFailed: false };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { result: { name: pkg.name, version: pkg.version, success: false, error: `Publish failed after ${MAX_RETRIES2} retries (transient errors)` }, otpFailed: false };
|
|
478
|
+
} finally {
|
|
479
|
+
restore();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function promptForOtp(ui, loader, pkgName) {
|
|
483
|
+
loader.succeed(`\u{1F510} 2FA required for ${pkgName}`);
|
|
484
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
485
|
+
try {
|
|
486
|
+
const entered = await rl.question(" Enter OTP code: ");
|
|
487
|
+
rl.close();
|
|
488
|
+
if (!entered || entered.trim().length !== 6) {
|
|
489
|
+
ui.write?.(" \u26A0\uFE0F Invalid OTP (must be 6 digits)\n");
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
return entered.trim();
|
|
493
|
+
} catch (e) {
|
|
494
|
+
rl.close();
|
|
495
|
+
throw e;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function sequentialPublishPkg(pkg, ctx, otpState, ui, logger) {
|
|
499
|
+
const loader = useLoader(`Publishing ${pkg.name}@${pkg.version}...`);
|
|
500
|
+
loader.start();
|
|
501
|
+
const restore = rewriteWorkspaceDeps(pkg, ctx.versionMap, ctx.packageManager);
|
|
502
|
+
try {
|
|
503
|
+
const MAX_OTP_ATTEMPTS = 3;
|
|
504
|
+
for (let otpAttempts = 0; otpAttempts < MAX_OTP_ATTEMPTS; otpAttempts++) {
|
|
505
|
+
try {
|
|
506
|
+
await publishSinglePackage({ packagePath: pkg.path, packageManager: ctx.packageManager, otp: otpState.otp, dryRun: ctx.dryRun, tag: ctx.tag, access: ctx.access, registry: ctx.registry });
|
|
507
|
+
loader.succeed(ctx.dryRun ? `Dry-run: ${pkg.name}@${pkg.version}` : `Published ${pkg.name}@${pkg.version}`);
|
|
508
|
+
return { name: pkg.name, version: pkg.version, success: true };
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
511
|
+
if (isAlreadyPublished2(msg)) {
|
|
512
|
+
loader.succeed(`${pkg.name}@${pkg.version} already published`);
|
|
513
|
+
return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
|
|
514
|
+
}
|
|
515
|
+
if (msg.includes("EOTP") || msg.includes("one-time password")) {
|
|
516
|
+
if (otpAttempts + 1 < MAX_OTP_ATTEMPTS) {
|
|
517
|
+
otpState.otp = await promptForOtp(ui, loader, pkg.name);
|
|
518
|
+
loader.update({ text: `Publishing ${pkg.name}@${pkg.version} with OTP...` });
|
|
519
|
+
loader.start();
|
|
520
|
+
} else {
|
|
521
|
+
loader.fail("Max OTP attempts reached");
|
|
522
|
+
return { name: pkg.name, version: pkg.version, success: false, error: "Max OTP attempts reached" };
|
|
523
|
+
}
|
|
524
|
+
} else if (isRetryable2(msg)) {
|
|
525
|
+
const delay = retryDelay2(MAX_RETRIES2 - 1);
|
|
526
|
+
loader.update({ text: `Rate limited \u2014 waiting ${(delay / 1e3).toFixed(0)}s\u2026` });
|
|
527
|
+
loader.start();
|
|
528
|
+
await new Promise((r) => {
|
|
529
|
+
setTimeout(r, delay);
|
|
530
|
+
});
|
|
531
|
+
otpAttempts--;
|
|
532
|
+
} else {
|
|
533
|
+
loader.fail(`Failed: ${msg.split("\n")[0]}`);
|
|
534
|
+
return { name: pkg.name, version: pkg.version, success: false, error: msg };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { name: pkg.name, version: pkg.version, success: false, error: "Publish failed after max OTP attempts" };
|
|
539
|
+
} finally {
|
|
540
|
+
restore();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function publishPackagesWithOTP(options) {
|
|
544
|
+
const { packages, packageManager = "pnpm", dryRun, tag, access, registry, ui, logger } = options;
|
|
545
|
+
const otpState = { otp: options.otp };
|
|
546
|
+
const versionMap = new Map(packages.map((p) => [p.name, p.version]));
|
|
547
|
+
const fastCtx = { packageManager, versionMap, otp: otpState.otp, dryRun, tag, access, registry };
|
|
548
|
+
const seqCtx = { packageManager, versionMap, dryRun, tag, access, registry };
|
|
549
|
+
const results = [];
|
|
550
|
+
const remaining = [];
|
|
551
|
+
const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
|
|
552
|
+
let otpFailureDetected = false;
|
|
553
|
+
ui.write?.(` Publishing ${packages.length} package(s) in parallel (concurrency=${CONCURRENCY})...
|
|
554
|
+
`);
|
|
555
|
+
for (let i = 0; i < packages.length; i += CONCURRENCY) {
|
|
556
|
+
const batch = packages.slice(i, i + CONCURRENCY);
|
|
557
|
+
const outcomes = await Promise.all(batch.map((pkg) => fastPublishPkg(pkg, fastCtx, logger)));
|
|
558
|
+
for (let j = 0; j < batch.length; j++) {
|
|
559
|
+
const outcome = outcomes[j];
|
|
560
|
+
if (outcome.result === null) {
|
|
561
|
+
remaining.push(batch[j]);
|
|
562
|
+
} else {
|
|
563
|
+
results.push(outcome.result);
|
|
564
|
+
}
|
|
565
|
+
if (outcome.otpFailed) {
|
|
566
|
+
otpFailureDetected = true;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (otpFailureDetected) {
|
|
570
|
+
remaining.push(...packages.slice(i + CONCURRENCY));
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (remaining.length === 0) {
|
|
575
|
+
return buildResult(results, packages, dryRun);
|
|
576
|
+
}
|
|
577
|
+
ui.write?.(` OTP required \u2014 falling back to sequential publish for ${remaining.length} package(s)
|
|
578
|
+
`);
|
|
579
|
+
for (const pkg of remaining) {
|
|
580
|
+
logger?.info("Publishing package (sequential)", { name: pkg.name, version: pkg.version });
|
|
581
|
+
results.push(await sequentialPublishPkg(pkg, seqCtx, otpState, ui));
|
|
582
|
+
}
|
|
583
|
+
return buildResult(results, packages, dryRun);
|
|
584
|
+
}
|
|
585
|
+
function buildResult(results, packages, dryRun) {
|
|
586
|
+
return {
|
|
587
|
+
results,
|
|
588
|
+
published: results.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
|
|
589
|
+
alreadyPublished: results.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
|
|
590
|
+
failed: results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`),
|
|
591
|
+
skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
|
|
592
|
+
errors: results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/cli/commands/promote.ts
|
|
597
|
+
function buildPromoteSections(results, tag, registry, symbols) {
|
|
598
|
+
const sections = [];
|
|
599
|
+
sections.push({ header: "Promoting to", items: [`registry: ${registry}`, `tag: ${tag}`] });
|
|
600
|
+
const successful = results.filter((r) => r.success);
|
|
601
|
+
if (successful.length > 0) {
|
|
602
|
+
sections.push({
|
|
603
|
+
header: "Promoted",
|
|
604
|
+
items: successful.map((r) => `${symbols.success} ${r.name}@${r.version}`)
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const failed = results.filter((r) => !r.success);
|
|
608
|
+
if (failed.length > 0) {
|
|
609
|
+
sections.push({
|
|
610
|
+
header: "Failed",
|
|
611
|
+
items: failed.map((r) => `${symbols.error} ${r.name}@${r.version} - ${r.error}`)
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return sections;
|
|
615
|
+
}
|
|
616
|
+
function toPromoteResult(results, tag, registry) {
|
|
617
|
+
const successful = results.filter((r) => r.success).length;
|
|
618
|
+
const failed = results.filter((r) => !r.success).length;
|
|
619
|
+
return {
|
|
620
|
+
exitCode: failed === 0 ? 0 : 1,
|
|
621
|
+
published: results.filter((r) => r.success).map((r) => ({ name: r.name, version: r.version })),
|
|
622
|
+
failed: results.filter((r) => !r.success).map((r) => ({
|
|
623
|
+
name: r.name,
|
|
624
|
+
version: r.version,
|
|
625
|
+
error: r.error || "Unknown error"
|
|
626
|
+
})),
|
|
627
|
+
summary: { total: results.length, successful, failed, tag, registry }
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
var promote_default = defineCommand({
|
|
631
|
+
id: "release:promote",
|
|
632
|
+
description: "Promote already-released package versions to npm (no re-bump, no rebuild)",
|
|
633
|
+
handler: {
|
|
634
|
+
async execute(ctx, input) {
|
|
635
|
+
const { flags } = input;
|
|
636
|
+
const { scope, otp: initialOtp, access, json } = flags;
|
|
637
|
+
const dryRun = flags["dry-run"];
|
|
638
|
+
const token = flags.token ?? useEnv("NPM_TOKEN") ?? useEnv("NODE_AUTH_TOKEN");
|
|
639
|
+
const cwd = ctx.cwd || process.cwd();
|
|
640
|
+
const repoRoot = await findRepoRoot(cwd);
|
|
641
|
+
const fileConfig = await useConfig();
|
|
642
|
+
const config = fileConfig ?? {};
|
|
643
|
+
const tag = flags.tag ?? resolvePublishTag(config, "stable");
|
|
644
|
+
const registry = flags.registry ?? resolvePublishRegistry(config, "stable");
|
|
645
|
+
const discoveryLoader = useLoader("Discovering already-released packages...");
|
|
646
|
+
discoveryLoader.start();
|
|
647
|
+
const discovered = await discoverCurrentPackages(repoRoot, scope, config);
|
|
648
|
+
const packages = discovered.map((pkg) => ({
|
|
649
|
+
name: pkg.name,
|
|
650
|
+
version: pkg.currentVersion,
|
|
651
|
+
path: pkg.path
|
|
652
|
+
}));
|
|
653
|
+
discoveryLoader.succeed(`Found ${packages.length} package(s) to promote`);
|
|
654
|
+
if (packages.length === 0) {
|
|
655
|
+
const msg = `No packages found to promote${scope ? ` matching scope: ${scope}` : ""}`;
|
|
656
|
+
if (json) {
|
|
657
|
+
ctx.ui?.json?.({ error: msg });
|
|
658
|
+
} else {
|
|
659
|
+
ctx.ui?.write?.(msg);
|
|
660
|
+
}
|
|
661
|
+
return { exitCode: 1, summary: { total: 0, successful: 0, failed: 0, tag, registry } };
|
|
662
|
+
}
|
|
663
|
+
let rawResult;
|
|
664
|
+
if (token) {
|
|
665
|
+
rawResult = await publishPackagesProgrammatic({
|
|
666
|
+
packages,
|
|
667
|
+
dryRun,
|
|
668
|
+
otp: initialOtp,
|
|
669
|
+
tag,
|
|
670
|
+
registry,
|
|
671
|
+
access: access ?? config.publish?.access,
|
|
672
|
+
token
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
rawResult = await publishPackagesWithOTP({
|
|
676
|
+
packages,
|
|
677
|
+
dryRun,
|
|
678
|
+
otp: initialOtp,
|
|
679
|
+
tag,
|
|
680
|
+
registry,
|
|
681
|
+
access: access ?? config.publish?.access ?? "public",
|
|
682
|
+
ui: ctx.ui,
|
|
683
|
+
logger: ctx.platform?.logger
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const promoteResult = toPromoteResult(rawResult.results, tag, registry);
|
|
687
|
+
console.log("::kb-output::" + JSON.stringify({
|
|
688
|
+
promoted: promoteResult.published?.map((p) => `${p.name}@${p.version}`) ?? [],
|
|
689
|
+
failed: promoteResult.failed?.map((p) => p.name) ?? [],
|
|
690
|
+
tag,
|
|
691
|
+
registry
|
|
692
|
+
}));
|
|
693
|
+
if (json) {
|
|
694
|
+
ctx.ui?.json?.(promoteResult);
|
|
695
|
+
return promoteResult;
|
|
696
|
+
}
|
|
697
|
+
ctx.ui?.sideBox?.({
|
|
698
|
+
title: dryRun ? "Promote Dry-Run" : "Promote Summary",
|
|
699
|
+
sections: buildPromoteSections(rawResult.results, tag, registry, ctx.ui.symbols),
|
|
700
|
+
status: promoteResult.exitCode === 0 ? "success" : "error"
|
|
701
|
+
});
|
|
702
|
+
return promoteResult;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
export { promote_default as default };
|
|
708
|
+
//# sourceMappingURL=promote.js.map
|
|
709
|
+
//# sourceMappingURL=promote.js.map
|