@kb-labs/release-manager-core 0.6.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/README.md +204 -0
- package/dist/index.d.ts +550 -0
- package/dist/index.js +1457 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rm, rename, cp } from 'fs/promises';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, rmSync, statSync } from 'fs';
|
|
3
|
+
import { join, relative, resolve, dirname } from 'path';
|
|
4
|
+
import simpleGit from 'simple-git';
|
|
5
|
+
import semver2 from 'semver';
|
|
6
|
+
import globby from 'globby';
|
|
7
|
+
import { discoverSubRepoPaths } from '@kb-labs/sdk';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import { spawn, execSync } from 'child_process';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { randomBytes } from 'crypto';
|
|
12
|
+
|
|
13
|
+
// src/planner.ts
|
|
14
|
+
function applyVersionStrategy(packages, options) {
|
|
15
|
+
if (options.strategy === "lockstep") {
|
|
16
|
+
return applyLockstep(packages);
|
|
17
|
+
}
|
|
18
|
+
if (options.strategy === "adaptive") {
|
|
19
|
+
return applyAdaptive(packages);
|
|
20
|
+
}
|
|
21
|
+
return packages;
|
|
22
|
+
}
|
|
23
|
+
function applyLockstep(packages) {
|
|
24
|
+
if (packages.length === 0) {
|
|
25
|
+
return packages;
|
|
26
|
+
}
|
|
27
|
+
const maxBump = getMaxBump(packages);
|
|
28
|
+
const maxVersion = packages.reduce((max, pkg) => {
|
|
29
|
+
return semver2.gt(pkg.currentVersion, max) ? pkg.currentVersion : max;
|
|
30
|
+
}, packages[0].currentVersion);
|
|
31
|
+
const releaseType = maxBump === "auto" ? "patch" : maxBump;
|
|
32
|
+
const nextVersion = semver2.inc(maxVersion, releaseType) || maxVersion;
|
|
33
|
+
return packages.map((pkg) => ({
|
|
34
|
+
...pkg,
|
|
35
|
+
bump: maxBump,
|
|
36
|
+
nextVersion
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function applyAdaptive(packages) {
|
|
40
|
+
const hasBreaking = packages.some((pkg) => pkg.bump === "major");
|
|
41
|
+
if (hasBreaking) {
|
|
42
|
+
return applyLockstep(packages);
|
|
43
|
+
}
|
|
44
|
+
return packages;
|
|
45
|
+
}
|
|
46
|
+
function getMaxBump(packages) {
|
|
47
|
+
let maxBump = "patch";
|
|
48
|
+
for (const pkg of packages) {
|
|
49
|
+
if (pkg.bump === "major") {
|
|
50
|
+
return "major";
|
|
51
|
+
}
|
|
52
|
+
if (pkg.bump === "minor") {
|
|
53
|
+
maxBump = "minor";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return maxBump;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/planner.ts
|
|
60
|
+
async function planRelease(options) {
|
|
61
|
+
const { cwd, config, scope, bumpOverride } = options;
|
|
62
|
+
const allPackages = await discoverPackages(cwd, config);
|
|
63
|
+
let packages;
|
|
64
|
+
if (scope && scope !== "root") {
|
|
65
|
+
const isWorkspace = existsSync(join(cwd, ".gitmodules"));
|
|
66
|
+
if (isWorkspace) {
|
|
67
|
+
const matchedRoots = filterByScope(allPackages, scope, config);
|
|
68
|
+
const innerPackages = [];
|
|
69
|
+
for (const root of matchedRoots) {
|
|
70
|
+
innerPackages.push(root);
|
|
71
|
+
const inner = await discoverPackages(root.path, config);
|
|
72
|
+
for (const pkg of inner) {
|
|
73
|
+
if (!innerPackages.some((p) => p.name === pkg.name)) {
|
|
74
|
+
innerPackages.push(pkg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
packages = innerPackages;
|
|
79
|
+
} else {
|
|
80
|
+
packages = filterByScope(allPackages, scope, config);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
packages = allPackages;
|
|
84
|
+
}
|
|
85
|
+
const isWorkspaceRoot = existsSync(join(cwd, ".gitmodules")) && !scope;
|
|
86
|
+
let modifiedPackages;
|
|
87
|
+
if (isWorkspaceRoot) {
|
|
88
|
+
modifiedPackages = packages;
|
|
89
|
+
} else {
|
|
90
|
+
const git = simpleGit(cwd, { timeout: { block: 6e4 } });
|
|
91
|
+
modifiedPackages = await detectModifiedPackages(git, packages);
|
|
92
|
+
}
|
|
93
|
+
let planPackages = [];
|
|
94
|
+
for (const pkg of modifiedPackages) {
|
|
95
|
+
const bump = bumpOverride || config.bump || "auto";
|
|
96
|
+
const git = isWorkspaceRoot ? simpleGit(pkg.path, { timeout: { block: 6e4 } }) : simpleGit(cwd, { timeout: { block: 6e4 } });
|
|
97
|
+
const nextVersion = await computeNextVersion(
|
|
98
|
+
pkg.path,
|
|
99
|
+
pkg.currentVersion,
|
|
100
|
+
bump,
|
|
101
|
+
git
|
|
102
|
+
);
|
|
103
|
+
planPackages.push({
|
|
104
|
+
...pkg,
|
|
105
|
+
nextVersion,
|
|
106
|
+
bump: bump === "auto" ? detectBumpType(pkg.currentVersion, nextVersion) : bump
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const bumpStrategy = config.versioningStrategy || config.changelog?.bumpStrategy || "independent";
|
|
110
|
+
const versionStrategy = mapBumpStrategyToVersionStrategy(bumpStrategy);
|
|
111
|
+
planPackages = applyVersionStrategy(planPackages, {
|
|
112
|
+
strategy: versionStrategy});
|
|
113
|
+
return {
|
|
114
|
+
packages: planPackages,
|
|
115
|
+
strategy: config.strategy || "semver",
|
|
116
|
+
registry: config.registry || "https://registry.npmjs.org",
|
|
117
|
+
rollbackEnabled: config.rollback?.enabled ?? true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function mapBumpStrategyToVersionStrategy(bumpStrategy) {
|
|
121
|
+
if (bumpStrategy === "lockstep") {
|
|
122
|
+
return "lockstep";
|
|
123
|
+
}
|
|
124
|
+
if (bumpStrategy === "ripple" || bumpStrategy === "adaptive") {
|
|
125
|
+
return "adaptive";
|
|
126
|
+
}
|
|
127
|
+
return "independent";
|
|
128
|
+
}
|
|
129
|
+
async function discoverPackages(cwd, config) {
|
|
130
|
+
const isWorkspaceRoot = existsSync(join(cwd, ".gitmodules"));
|
|
131
|
+
if (isWorkspaceRoot) {
|
|
132
|
+
return discoverSubRepoPackages(cwd, config);
|
|
133
|
+
}
|
|
134
|
+
const packages = [];
|
|
135
|
+
const configPaths = config?.packages?.paths;
|
|
136
|
+
const pattern = configPaths?.length ? configPaths.map((p) => `${p}/package.json`) : "**/package.json";
|
|
137
|
+
const packageJsonPaths = await globby(pattern, {
|
|
138
|
+
cwd,
|
|
139
|
+
absolute: true,
|
|
140
|
+
onlyFiles: true,
|
|
141
|
+
ignore: [
|
|
142
|
+
"**/node_modules/**",
|
|
143
|
+
"**/dist/**",
|
|
144
|
+
"**/build/**",
|
|
145
|
+
"**/.git/**",
|
|
146
|
+
"**/.*/**"
|
|
147
|
+
]
|
|
148
|
+
});
|
|
149
|
+
for (let i = 0; i < packageJsonPaths.length; i++) {
|
|
150
|
+
if (i % 10 === 0) {
|
|
151
|
+
await new Promise((resolve2) => {
|
|
152
|
+
setImmediate(resolve2);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const packageJsonPath = packageJsonPaths[i];
|
|
156
|
+
const packagePath = join(packageJsonPath, "..");
|
|
157
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
158
|
+
const isRootPackageJson = packageJsonPath === join(cwd, "package.json");
|
|
159
|
+
if (packageJson.private) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (isRootPackageJson && existsSync(join(packagePath, "pnpm-workspace.yaml"))) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (isRootPackageJson && !packageJson.name) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const globalFilter = config?.packages;
|
|
169
|
+
if (globalFilter?.include?.length || globalFilter?.exclude?.length) {
|
|
170
|
+
const rel = relative(cwd, packagePath);
|
|
171
|
+
if (globalFilter.include?.length && !matchesPackagePattern(packageJson.name, rel, globalFilter.include)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (globalFilter.exclude?.length && matchesPackagePattern(packageJson.name, rel, globalFilter.exclude)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
packages.push({
|
|
179
|
+
name: packageJson.name,
|
|
180
|
+
path: packagePath,
|
|
181
|
+
currentVersion: packageJson.version,
|
|
182
|
+
nextVersion: packageJson.version,
|
|
183
|
+
bump: "auto",
|
|
184
|
+
isPublished: false
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return packages;
|
|
188
|
+
}
|
|
189
|
+
function filterByScope(packages, scope, config) {
|
|
190
|
+
const scopeFilter = config?.scopes?.[scope]?.packages;
|
|
191
|
+
const globalFilter = config?.packages;
|
|
192
|
+
const mergedInclude = [...globalFilter?.include ?? [], ...scopeFilter?.include ?? []];
|
|
193
|
+
const mergedExclude = [...globalFilter?.exclude ?? [], ...scopeFilter?.exclude ?? []];
|
|
194
|
+
const isExactName = !scope.includes("*") && (scope.startsWith("@") || !scope.includes("/"));
|
|
195
|
+
const scopeRegex = new RegExp(
|
|
196
|
+
"^" + scope.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*") + "$"
|
|
197
|
+
);
|
|
198
|
+
const result = [];
|
|
199
|
+
for (const pkg of packages) {
|
|
200
|
+
const isPathPattern2 = scope.includes("/") && !scope.startsWith("@");
|
|
201
|
+
const matchTarget = isPathPattern2 ? pkg.path : pkg.name;
|
|
202
|
+
const matches = isExactName ? pkg.name === scope : scopeRegex.test(matchTarget);
|
|
203
|
+
if (!matches) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (mergedInclude.length && !matchesPackagePattern(pkg.name, pkg.path, mergedInclude)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (mergedExclude.length && matchesPackagePattern(pkg.name, pkg.path, mergedExclude)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
result.push(pkg);
|
|
213
|
+
}
|
|
214
|
+
if (result.length === 0) {
|
|
215
|
+
const wouldMatchExcluded = packages.some((p) => {
|
|
216
|
+
const matchTarget = isPathPattern(scope) ? p.path : p.name;
|
|
217
|
+
const scopeMatches = isExactName ? p.name === scope : scopeRegex.test(matchTarget);
|
|
218
|
+
if (!scopeMatches) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return mergedExclude.length > 0 && matchesPackagePattern(p.name, p.path, mergedExclude);
|
|
222
|
+
});
|
|
223
|
+
if (wouldMatchExcluded) {
|
|
224
|
+
throw new Error(`Scope "${scope}" matches packages that are excluded by configuration`);
|
|
225
|
+
}
|
|
226
|
+
const globalInclude = globalFilter?.include;
|
|
227
|
+
if (globalInclude?.length) {
|
|
228
|
+
throw new Error(`Scope "${scope}" did not match any packages. Note: packages.include restricts discovery to: ${globalInclude.join(", ")}`);
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`Scope "${scope}" did not match any discovered packages`);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
function isPathPattern(scope) {
|
|
235
|
+
return scope.includes("/") && !scope.startsWith("@");
|
|
236
|
+
}
|
|
237
|
+
async function discoverSubRepoPackages(workspaceRoot, config) {
|
|
238
|
+
const subRepoPaths = discoverSubRepoPaths(workspaceRoot);
|
|
239
|
+
const packages = [];
|
|
240
|
+
const globalFilter = config?.packages;
|
|
241
|
+
for (const subRepoPath of subRepoPaths) {
|
|
242
|
+
try {
|
|
243
|
+
const pkgJson = JSON.parse(
|
|
244
|
+
await readFile(join(subRepoPath, "package.json"), "utf-8")
|
|
245
|
+
);
|
|
246
|
+
if (!pkgJson.name) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const rel = relative(workspaceRoot, subRepoPath);
|
|
250
|
+
if (globalFilter?.include?.length && !matchesPackagePattern(pkgJson.name, rel, globalFilter.include)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (globalFilter?.exclude?.length && matchesPackagePattern(pkgJson.name, rel, globalFilter.exclude)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
packages.push({
|
|
257
|
+
name: pkgJson.name,
|
|
258
|
+
path: subRepoPath,
|
|
259
|
+
currentVersion: pkgJson.version || "0.0.0",
|
|
260
|
+
nextVersion: pkgJson.version || "0.0.0",
|
|
261
|
+
bump: "auto",
|
|
262
|
+
isPublished: false
|
|
263
|
+
});
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return packages;
|
|
268
|
+
}
|
|
269
|
+
function shouldIgnoreFile(file) {
|
|
270
|
+
const ignoredPaths = [
|
|
271
|
+
"node_modules/",
|
|
272
|
+
".git/",
|
|
273
|
+
"dist/",
|
|
274
|
+
"build/",
|
|
275
|
+
".next/",
|
|
276
|
+
".nuxt/",
|
|
277
|
+
"coverage/",
|
|
278
|
+
".cache/",
|
|
279
|
+
"tmp/"
|
|
280
|
+
];
|
|
281
|
+
return ignoredPaths.some((path) => file.includes(path));
|
|
282
|
+
}
|
|
283
|
+
async function detectModifiedPackages(git, packages) {
|
|
284
|
+
const status = await git.status();
|
|
285
|
+
const diffSummary = await git.diffSummary(["HEAD"]);
|
|
286
|
+
const modifiedPaths = [
|
|
287
|
+
...status.files.map((f) => f.path),
|
|
288
|
+
...diffSummary.files.map((f) => f.file)
|
|
289
|
+
].filter((path) => !shouldIgnoreFile(path));
|
|
290
|
+
const modified = [];
|
|
291
|
+
for (let i = 0; i < packages.length; i++) {
|
|
292
|
+
if (i % 10 === 0) {
|
|
293
|
+
await new Promise((resolve2) => {
|
|
294
|
+
setImmediate(resolve2);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const pkg = packages[i];
|
|
298
|
+
const packageModified = modifiedPaths.some(
|
|
299
|
+
(path) => path.startsWith(pkg.path) || path.includes(pkg.name)
|
|
300
|
+
);
|
|
301
|
+
if (packageModified) {
|
|
302
|
+
modified.push(pkg);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return modified.length > 0 ? modified : packages;
|
|
306
|
+
}
|
|
307
|
+
async function computeNextVersion(packagePath, currentVersion, bump, git) {
|
|
308
|
+
if (bump === "auto") {
|
|
309
|
+
const detectedBump = await detectVersionFromCommits(git, packagePath);
|
|
310
|
+
return semver2.inc(currentVersion, detectedBump) || currentVersion;
|
|
311
|
+
}
|
|
312
|
+
return semver2.inc(currentVersion, bump) || currentVersion;
|
|
313
|
+
}
|
|
314
|
+
async function detectVersionFromCommits(git, packagePath) {
|
|
315
|
+
try {
|
|
316
|
+
const log = await git.log({
|
|
317
|
+
maxCount: 50,
|
|
318
|
+
file: packagePath
|
|
319
|
+
});
|
|
320
|
+
let hasMinor = false;
|
|
321
|
+
let hasBreaking = false;
|
|
322
|
+
for (const commit of log.all) {
|
|
323
|
+
const message = commit.message.toLowerCase();
|
|
324
|
+
if (message.includes("!:")) {
|
|
325
|
+
hasBreaking = true;
|
|
326
|
+
} else if (message.startsWith("feat") || message.startsWith("feature")) {
|
|
327
|
+
hasMinor = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (hasBreaking) {
|
|
331
|
+
return "major";
|
|
332
|
+
}
|
|
333
|
+
if (hasMinor) {
|
|
334
|
+
return "minor";
|
|
335
|
+
}
|
|
336
|
+
return "patch";
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return "patch";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function detectBumpType(currentVersion, nextVersion) {
|
|
342
|
+
if (semver2.major(currentVersion) < semver2.major(nextVersion)) {
|
|
343
|
+
return "major";
|
|
344
|
+
}
|
|
345
|
+
if (semver2.minor(currentVersion) < semver2.minor(nextVersion)) {
|
|
346
|
+
return "minor";
|
|
347
|
+
}
|
|
348
|
+
return "patch";
|
|
349
|
+
}
|
|
350
|
+
function matchesPackagePattern(pkgName, relativePath, patterns) {
|
|
351
|
+
for (const pattern of patterns) {
|
|
352
|
+
const isPathPattern2 = pattern.includes("/") && !pattern.startsWith("@");
|
|
353
|
+
const target = isPathPattern2 ? relativePath : pkgName;
|
|
354
|
+
const regex = new RegExp(
|
|
355
|
+
"^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*") + "$"
|
|
356
|
+
);
|
|
357
|
+
if (regex.test(target)) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
function createExecaShellAdapter() {
|
|
364
|
+
return {
|
|
365
|
+
async exec(command, args, options) {
|
|
366
|
+
try {
|
|
367
|
+
const result = await execa(command, args || [], {
|
|
368
|
+
cwd: options?.cwd,
|
|
369
|
+
timeout: options?.timeout,
|
|
370
|
+
preferLocal: true,
|
|
371
|
+
env: options?.env || process.env
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
ok: result.exitCode === 0,
|
|
375
|
+
code: result.exitCode,
|
|
376
|
+
stdout: result.stdout || "",
|
|
377
|
+
stderr: result.stderr || ""
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
code: error.exitCode || 1,
|
|
383
|
+
stdout: error.stdout || "",
|
|
384
|
+
stderr: error.stderr || error.message || ""
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/publisher.ts
|
|
392
|
+
async function publishPackages(options) {
|
|
393
|
+
const { plan, dryRun, shell } = options;
|
|
394
|
+
const shellApi = shell || createExecaShellAdapter();
|
|
395
|
+
const result = {
|
|
396
|
+
published: [],
|
|
397
|
+
skipped: [],
|
|
398
|
+
errors: [],
|
|
399
|
+
versionUpdates: []
|
|
400
|
+
};
|
|
401
|
+
if (dryRun) {
|
|
402
|
+
for (const pkg of plan.packages) {
|
|
403
|
+
result.skipped.push(`${pkg.name}@${pkg.nextVersion} (dry-run)`);
|
|
404
|
+
result.versionUpdates.push({
|
|
405
|
+
package: pkg.name,
|
|
406
|
+
from: pkg.currentVersion || "unknown",
|
|
407
|
+
to: pkg.nextVersion || "unknown",
|
|
408
|
+
updated: false
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
for (const pkg of plan.packages) {
|
|
414
|
+
try {
|
|
415
|
+
const registry = plan.registry || "https://registry.npmjs.org";
|
|
416
|
+
try {
|
|
417
|
+
await updatePackageVersion(pkg);
|
|
418
|
+
result.versionUpdates.push({
|
|
419
|
+
package: pkg.name,
|
|
420
|
+
from: pkg.currentVersion || "unknown",
|
|
421
|
+
to: pkg.nextVersion || "unknown",
|
|
422
|
+
updated: true
|
|
423
|
+
});
|
|
424
|
+
} catch (versionError) {
|
|
425
|
+
const msg = `Failed to update version for ${pkg.name}: ${versionError instanceof Error ? versionError.message : String(versionError)}`;
|
|
426
|
+
result.errors.push(msg);
|
|
427
|
+
result.versionUpdates.push({
|
|
428
|
+
package: pkg.name,
|
|
429
|
+
from: pkg.currentVersion || "unknown",
|
|
430
|
+
to: pkg.nextVersion || "unknown",
|
|
431
|
+
updated: false
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const pm = options.config?.publish?.packageManager ?? "pnpm";
|
|
436
|
+
const access = options.config?.publish?.access ?? "public";
|
|
437
|
+
const publishResult = await shellApi.exec(
|
|
438
|
+
pm,
|
|
439
|
+
["publish", "--access", access, "--registry", registry],
|
|
440
|
+
{
|
|
441
|
+
cwd: pkg.path,
|
|
442
|
+
timeout: 6e4
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
if (publishResult.ok) {
|
|
446
|
+
result.published.push(`${pkg.name}@${pkg.nextVersion}`);
|
|
447
|
+
} else {
|
|
448
|
+
const errorDetails = publishResult.stderr || publishResult.stdout || "Unknown error";
|
|
449
|
+
result.errors.push(`Failed to publish ${pkg.name}: ${errorDetails}`);
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const msg = `Failed to publish ${pkg.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
453
|
+
result.errors.push(msg);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
async function updatePackageVersion(pkg) {
|
|
459
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
460
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
461
|
+
packageJson.version = pkg.nextVersion;
|
|
462
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
|
|
463
|
+
}
|
|
464
|
+
async function updatePackageVersions(plan) {
|
|
465
|
+
const results = [];
|
|
466
|
+
for (const pkg of plan.packages) {
|
|
467
|
+
try {
|
|
468
|
+
await updatePackageVersion(pkg);
|
|
469
|
+
results.push({
|
|
470
|
+
package: pkg.name,
|
|
471
|
+
from: pkg.currentVersion || "unknown",
|
|
472
|
+
to: pkg.nextVersion || "unknown",
|
|
473
|
+
updated: true
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.warn(`Failed to update version for ${pkg.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
477
|
+
results.push({
|
|
478
|
+
package: pkg.name,
|
|
479
|
+
from: pkg.currentVersion || "unknown",
|
|
480
|
+
to: pkg.nextVersion || "unknown",
|
|
481
|
+
updated: false
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
487
|
+
async function generateChangelog(options) {
|
|
488
|
+
const { cwd, plan } = options;
|
|
489
|
+
const changelogPath = join(cwd, "CHANGELOG.md");
|
|
490
|
+
let existingChangelog = "";
|
|
491
|
+
try {
|
|
492
|
+
existingChangelog = await readFile(changelogPath, "utf-8");
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
496
|
+
const header = `## [${date}] Release
|
|
497
|
+
|
|
498
|
+
`;
|
|
499
|
+
const entries = [];
|
|
500
|
+
for (const pkg of plan.packages) {
|
|
501
|
+
entries.push(`- **${pkg.name}**: ${pkg.currentVersion} \u2192 ${pkg.nextVersion}`);
|
|
502
|
+
}
|
|
503
|
+
const newEntry = header + entries.join("\n") + "\n\n";
|
|
504
|
+
const updatedChangelog = newEntry + existingChangelog;
|
|
505
|
+
try {
|
|
506
|
+
await mkdir(join(cwd, ".kb", "release"), { recursive: true });
|
|
507
|
+
await writeFile(changelogPath, updatedChangelog, "utf-8");
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.warn(`Failed to write changelog: ${error instanceof Error ? error.message : String(error)}`);
|
|
510
|
+
}
|
|
511
|
+
return newEntry;
|
|
512
|
+
}
|
|
513
|
+
async function generateEnhancedChangelog(options) {
|
|
514
|
+
const simpleChangelog = await generateChangelog({
|
|
515
|
+
cwd: options.cwd,
|
|
516
|
+
plan: options.plan
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
changelog: simpleChangelog,
|
|
520
|
+
manifest: null
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function copyChangelogToPackages(options) {
|
|
524
|
+
const { plan, changelog } = options;
|
|
525
|
+
for (const pkg of plan.packages) {
|
|
526
|
+
try {
|
|
527
|
+
let packageChangelog;
|
|
528
|
+
if (plan.packages.length === 1) {
|
|
529
|
+
packageChangelog = changelog;
|
|
530
|
+
} else {
|
|
531
|
+
packageChangelog = createPackageChangelog(pkg, changelog);
|
|
532
|
+
}
|
|
533
|
+
if (!packageChangelog || packageChangelog.trim().length === 0) {
|
|
534
|
+
console.warn(`No changelog content for ${pkg.name}, skipping`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
538
|
+
let existingChangelog = "";
|
|
539
|
+
try {
|
|
540
|
+
existingChangelog = await readFile(changelogPath, "utf-8");
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
const versionPattern = new RegExp(
|
|
544
|
+
`^##\\s+${pkg.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+${pkg.nextVersion.replace(/\./g, "\\.")}`,
|
|
545
|
+
"m"
|
|
546
|
+
);
|
|
547
|
+
let updatedChangelog;
|
|
548
|
+
if (existingChangelog && versionPattern.test(existingChangelog)) {
|
|
549
|
+
const lines = existingChangelog.split("\n");
|
|
550
|
+
let startIdx = -1;
|
|
551
|
+
let endIdx = lines.length;
|
|
552
|
+
for (let i = 0; i < lines.length; i++) {
|
|
553
|
+
const line = lines[i];
|
|
554
|
+
if (line && versionPattern.test(line)) {
|
|
555
|
+
startIdx = i;
|
|
556
|
+
} else if (startIdx !== -1 && line && /^##\s+@?[\w-]+/.test(line)) {
|
|
557
|
+
endIdx = i;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (startIdx !== -1) {
|
|
562
|
+
const before = lines.slice(0, startIdx).join("\n");
|
|
563
|
+
const after = lines.slice(endIdx).join("\n");
|
|
564
|
+
updatedChangelog = (before ? before + "\n" : "") + packageChangelog + (after ? "\n" + after : "");
|
|
565
|
+
} else {
|
|
566
|
+
updatedChangelog = packageChangelog;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
updatedChangelog = packageChangelog + (existingChangelog ? "\n" + existingChangelog : "");
|
|
570
|
+
}
|
|
571
|
+
await writeFile(changelogPath, updatedChangelog.trim() + "\n", "utf-8");
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.warn(`Failed to write changelog for ${pkg.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function createPackageChangelog(pkg, changelog) {
|
|
578
|
+
const packageHeaderPattern = new RegExp(
|
|
579
|
+
`^##\\s+${pkg.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+\\d+\\.\\d+\\.\\d+`,
|
|
580
|
+
"gm"
|
|
581
|
+
);
|
|
582
|
+
const allHeaders = Array.from(changelog.matchAll(/^##\s+(@[\w-]+\/)?[\w-]+\s+\d+\.\d+\.\d+/gm));
|
|
583
|
+
let startIdx = -1;
|
|
584
|
+
let endIdx = changelog.length;
|
|
585
|
+
for (let i = 0; i < allHeaders.length; i++) {
|
|
586
|
+
const match = allHeaders[i];
|
|
587
|
+
if (!match || !match.index) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (packageHeaderPattern.test(match[0])) {
|
|
591
|
+
startIdx = match.index;
|
|
592
|
+
if (i + 1 < allHeaders.length) {
|
|
593
|
+
endIdx = allHeaders[i + 1].index;
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (startIdx === -1) {
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
601
|
+
return changelog.substring(startIdx, endIdx).trim();
|
|
602
|
+
}
|
|
603
|
+
async function commitAndTagRelease(options) {
|
|
604
|
+
const { cwd, plan, dryRun } = options;
|
|
605
|
+
const simpleGit2 = (await import('simple-git')).default;
|
|
606
|
+
const result = {
|
|
607
|
+
committed: false,
|
|
608
|
+
tagged: [],
|
|
609
|
+
pushed: false
|
|
610
|
+
};
|
|
611
|
+
if (dryRun) {
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const commitMessage = createCommitMessage(plan);
|
|
616
|
+
for (const pkg of plan.packages) {
|
|
617
|
+
const pkgGit = simpleGit2(pkg.path);
|
|
618
|
+
const filesToStage = ["package.json"];
|
|
619
|
+
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
620
|
+
if (existsSync(changelogPath)) {
|
|
621
|
+
filesToStage.push("CHANGELOG.md");
|
|
622
|
+
}
|
|
623
|
+
await pkgGit.add(filesToStage);
|
|
624
|
+
try {
|
|
625
|
+
await pkgGit.commit(commitMessage);
|
|
626
|
+
result.committed = true;
|
|
627
|
+
} catch (commitError) {
|
|
628
|
+
const msg = commitError instanceof Error ? commitError.message : String(commitError);
|
|
629
|
+
if (!msg.includes("nothing to commit") && !msg.includes("nothing added to commit")) {
|
|
630
|
+
throw commitError;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const git = simpleGit2(cwd);
|
|
635
|
+
const uniqueVersions = new Set(plan.packages.map((p) => p.nextVersion));
|
|
636
|
+
const isLockstep = plan.packages.length > 1 && uniqueVersions.size === 1;
|
|
637
|
+
if (isLockstep) {
|
|
638
|
+
const version = plan.packages[0].nextVersion;
|
|
639
|
+
const tagName = `v${version}`;
|
|
640
|
+
await git.addTag(tagName);
|
|
641
|
+
result.tagged.push(tagName);
|
|
642
|
+
} else {
|
|
643
|
+
for (const pkg of plan.packages) {
|
|
644
|
+
const pkgGit = simpleGit2(pkg.path);
|
|
645
|
+
const tagName = `${pkg.name}@${pkg.nextVersion}`;
|
|
646
|
+
await pkgGit.addTag(tagName);
|
|
647
|
+
result.tagged.push(tagName);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const pkg of plan.packages) {
|
|
651
|
+
const pkgGit = simpleGit2(pkg.path);
|
|
652
|
+
if (result.committed) {
|
|
653
|
+
await pkgGit.push(["--no-verify"]);
|
|
654
|
+
}
|
|
655
|
+
await pkgGit.pushTags("--no-verify");
|
|
656
|
+
}
|
|
657
|
+
result.pushed = true;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.error(`Git operations failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
function createCommitMessage(plan) {
|
|
665
|
+
const lines = [];
|
|
666
|
+
if (plan.packages.length === 1 && plan.packages[0]) {
|
|
667
|
+
const pkg = plan.packages[0];
|
|
668
|
+
lines.push(`chore(release): publish ${pkg.name}@${pkg.nextVersion}`);
|
|
669
|
+
} else {
|
|
670
|
+
lines.push(`chore(release): publish ${plan.packages.length} packages`);
|
|
671
|
+
}
|
|
672
|
+
lines.push("");
|
|
673
|
+
for (const pkg of plan.packages) {
|
|
674
|
+
lines.push(`- ${pkg.name}@${pkg.nextVersion}`);
|
|
675
|
+
}
|
|
676
|
+
return lines.join("\n");
|
|
677
|
+
}
|
|
678
|
+
async function saveSnapshot(options) {
|
|
679
|
+
const { cwd, plan } = options;
|
|
680
|
+
const snapshot = {
|
|
681
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
682
|
+
packages: plan.packages.map((pkg) => ({
|
|
683
|
+
...pkg,
|
|
684
|
+
nextVersion: pkg.currentVersion
|
|
685
|
+
// Store current before update
|
|
686
|
+
}))
|
|
687
|
+
};
|
|
688
|
+
const snapshotDir = join(cwd, ".kb", "release");
|
|
689
|
+
await mkdir(snapshotDir, { recursive: true });
|
|
690
|
+
const snapshotPath = join(snapshotDir, "backup.json");
|
|
691
|
+
await writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
692
|
+
await cleanupOldSnapshots();
|
|
693
|
+
}
|
|
694
|
+
async function restoreSnapshot(cwd) {
|
|
695
|
+
const snapshotPath = join(cwd, ".kb", "release", "backup.json");
|
|
696
|
+
if (!existsSync(snapshotPath)) {
|
|
697
|
+
throw new Error("No backup snapshot found");
|
|
698
|
+
}
|
|
699
|
+
const snapshotContent = await readFile(snapshotPath, "utf-8");
|
|
700
|
+
const snapshot = JSON.parse(snapshotContent);
|
|
701
|
+
for (const pkg of snapshot.packages) {
|
|
702
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
703
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
704
|
+
packageJson.version = pkg.currentVersion;
|
|
705
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function cleanupOldSnapshots(_snapshotDir) {
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/runner.ts
|
|
712
|
+
async function runRelease(options) {
|
|
713
|
+
const {
|
|
714
|
+
config,
|
|
715
|
+
runChecks,
|
|
716
|
+
executePlan,
|
|
717
|
+
onStageChange
|
|
718
|
+
} = options;
|
|
719
|
+
const startTime = Date.now();
|
|
720
|
+
const errors = [];
|
|
721
|
+
let checks;
|
|
722
|
+
try {
|
|
723
|
+
onStageChange?.("planning");
|
|
724
|
+
if (config.verify && config.verify.length > 0 && runChecks) {
|
|
725
|
+
onStageChange?.("checking");
|
|
726
|
+
checks = await runChecks("checking");
|
|
727
|
+
const failedChecks = Object.entries(checks).filter(([_, result]) => result && !result.ok).map(([id]) => id);
|
|
728
|
+
if (failedChecks.length > 0) {
|
|
729
|
+
errors.push(`Pre-release checks failed: ${failedChecks.join(", ")}`);
|
|
730
|
+
if (config.strict) {
|
|
731
|
+
return {
|
|
732
|
+
ok: false,
|
|
733
|
+
timingMs: Date.now() - startTime,
|
|
734
|
+
errors,
|
|
735
|
+
checks
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
onStageChange?.("publishing");
|
|
741
|
+
if (executePlan) {
|
|
742
|
+
await executePlan();
|
|
743
|
+
}
|
|
744
|
+
onStageChange?.("verifying");
|
|
745
|
+
return {
|
|
746
|
+
ok: errors.length === 0,
|
|
747
|
+
timingMs: Date.now() - startTime,
|
|
748
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
749
|
+
checks
|
|
750
|
+
};
|
|
751
|
+
} catch (error) {
|
|
752
|
+
onStageChange?.("rollback");
|
|
753
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
754
|
+
return {
|
|
755
|
+
ok: false,
|
|
756
|
+
timingMs: Date.now() - startTime,
|
|
757
|
+
errors,
|
|
758
|
+
checks
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/reporters/json.ts
|
|
764
|
+
function renderJson(report) {
|
|
765
|
+
return JSON.stringify(report, null, 2);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/reporters/markdown.ts
|
|
769
|
+
function renderMarkdown(report) {
|
|
770
|
+
const lines = [];
|
|
771
|
+
lines.push("# \u{1F9E9} KB Labs Release Summary");
|
|
772
|
+
lines.push("");
|
|
773
|
+
lines.push(`**Timestamp**: ${report.ts}`);
|
|
774
|
+
lines.push(`**Stage**: ${report.stage}`);
|
|
775
|
+
lines.push("");
|
|
776
|
+
if (report.result.ok) {
|
|
777
|
+
lines.push("## \u2705 Release: SUCCESS");
|
|
778
|
+
} else {
|
|
779
|
+
lines.push("## \u274C Release: FAILED");
|
|
780
|
+
}
|
|
781
|
+
lines.push("");
|
|
782
|
+
if (report.result.checks) {
|
|
783
|
+
lines.push("## Quality Checks");
|
|
784
|
+
lines.push("");
|
|
785
|
+
for (const [id, result] of Object.entries(report.result.checks)) {
|
|
786
|
+
if (!result) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const icon = result.ok ? "\u2705" : "\u274C";
|
|
790
|
+
lines.push(`- ${icon} **${id}**: ${result.ok ? "PASSED" : "FAILED"}`);
|
|
791
|
+
if (result.hint && !result.ok) {
|
|
792
|
+
lines.push(` - ${result.hint}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
lines.push("");
|
|
796
|
+
}
|
|
797
|
+
if (report.result.checksPerPackage) {
|
|
798
|
+
lines.push("## Package Quality Checks");
|
|
799
|
+
lines.push("");
|
|
800
|
+
for (const [pkgName, pkgChecks] of Object.entries(report.result.checksPerPackage)) {
|
|
801
|
+
lines.push(`### ${pkgName}`);
|
|
802
|
+
lines.push("");
|
|
803
|
+
for (const [id, result] of Object.entries(pkgChecks)) {
|
|
804
|
+
if (!result) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const icon = result.ok ? "\u2705" : "\u274C";
|
|
808
|
+
lines.push(`- ${icon} **${id}**: ${result.ok ? "PASSED" : "FAILED"}`);
|
|
809
|
+
if (result.hint && !result.ok) {
|
|
810
|
+
lines.push(` - ${result.hint}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
lines.push("");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (report.result.published && report.result.published.length > 0) {
|
|
817
|
+
lines.push("## \u{1F680} Published Packages");
|
|
818
|
+
lines.push("");
|
|
819
|
+
for (const pkg of report.result.published) {
|
|
820
|
+
lines.push(`- ${pkg}`);
|
|
821
|
+
}
|
|
822
|
+
lines.push("");
|
|
823
|
+
}
|
|
824
|
+
if (report.result.errors && report.result.errors.length > 0) {
|
|
825
|
+
lines.push("## \u274C Errors");
|
|
826
|
+
lines.push("");
|
|
827
|
+
for (const error of report.result.errors) {
|
|
828
|
+
lines.push(`- ${error}`);
|
|
829
|
+
}
|
|
830
|
+
lines.push("");
|
|
831
|
+
}
|
|
832
|
+
lines.push(`**Duration**: ${formatTiming(report.result.timingMs)}`);
|
|
833
|
+
lines.push("");
|
|
834
|
+
return lines.join("\n");
|
|
835
|
+
}
|
|
836
|
+
function formatTiming(ms) {
|
|
837
|
+
if (ms < 1e3) {
|
|
838
|
+
return `${ms}ms`;
|
|
839
|
+
}
|
|
840
|
+
if (ms < 6e4) {
|
|
841
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
842
|
+
}
|
|
843
|
+
return `${(ms / 6e4).toFixed(1)}m`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/reporters/text.ts
|
|
847
|
+
function renderText(report) {
|
|
848
|
+
const lines = [];
|
|
849
|
+
lines.push("[release] " + (report.result.ok ? "OK" : "FAILED"));
|
|
850
|
+
lines.push("");
|
|
851
|
+
if (report.result.checks) {
|
|
852
|
+
for (const [id, result] of Object.entries(report.result.checks)) {
|
|
853
|
+
if (!result) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
lines.push(`[${id}] ${result.ok ? "pass" : "fail"}`);
|
|
857
|
+
if (result.hint && !result.ok) {
|
|
858
|
+
lines.push(` ${result.hint}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
lines.push("");
|
|
862
|
+
}
|
|
863
|
+
if (report.result.checksPerPackage) {
|
|
864
|
+
for (const [pkgName, pkgChecks] of Object.entries(report.result.checksPerPackage)) {
|
|
865
|
+
lines.push(`[${pkgName}]`);
|
|
866
|
+
for (const [id, result] of Object.entries(pkgChecks)) {
|
|
867
|
+
if (!result) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
lines.push(` [${id}] ${result.ok ? "pass" : "fail"}`);
|
|
871
|
+
if (result.hint && !result.ok) {
|
|
872
|
+
lines.push(` ${result.hint}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
lines.push("");
|
|
877
|
+
}
|
|
878
|
+
if (report.result.published && report.result.published.length > 0) {
|
|
879
|
+
lines.push("[published] " + report.result.published.length + " package(s)");
|
|
880
|
+
for (const pkg of report.result.published) {
|
|
881
|
+
lines.push(` ${pkg}`);
|
|
882
|
+
}
|
|
883
|
+
lines.push("");
|
|
884
|
+
}
|
|
885
|
+
if (report.result.errors && report.result.errors.length > 0) {
|
|
886
|
+
lines.push("[errors]");
|
|
887
|
+
for (const error of report.result.errors) {
|
|
888
|
+
lines.push(` ${error}`);
|
|
889
|
+
}
|
|
890
|
+
lines.push("");
|
|
891
|
+
}
|
|
892
|
+
lines.push(`[timing] ${formatTiming2(report.result.timingMs)}`);
|
|
893
|
+
return lines.join("\n");
|
|
894
|
+
}
|
|
895
|
+
function formatTiming2(ms) {
|
|
896
|
+
if (ms < 1e3) {
|
|
897
|
+
return `${ms}ms`;
|
|
898
|
+
}
|
|
899
|
+
if (ms < 6e4) {
|
|
900
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
901
|
+
}
|
|
902
|
+
return `${(ms / 6e4).toFixed(1)}m`;
|
|
903
|
+
}
|
|
904
|
+
async function buildPackages(packages, options) {
|
|
905
|
+
const results = [];
|
|
906
|
+
for (const pkg of packages) {
|
|
907
|
+
options?.logger?.info?.(`Building ${pkg.name}...`);
|
|
908
|
+
const result = await runSafeBuild(pkg.path, pkg.name);
|
|
909
|
+
results.push({ ...result, name: pkg.name });
|
|
910
|
+
options?.onProgress?.(pkg.name, { ...result, name: pkg.name });
|
|
911
|
+
if (!result.success) {
|
|
912
|
+
options?.logger?.error?.(`Build failed for ${pkg.name}: ${result.error}`);
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
options?.logger?.info?.(`Built ${pkg.name} in ${result.durationMs}ms`);
|
|
916
|
+
}
|
|
917
|
+
return results;
|
|
918
|
+
}
|
|
919
|
+
async function runSafeBuild(packagePath, packageName) {
|
|
920
|
+
const usesTsup = existsSync(join(packagePath, "tsup.config.ts")) || existsSync(join(packagePath, "tsup.config.js"));
|
|
921
|
+
if (usesTsup) {
|
|
922
|
+
return runTsupSafeBuild(packagePath, packageName);
|
|
923
|
+
}
|
|
924
|
+
return runDirectBuild(packagePath, packageName);
|
|
925
|
+
}
|
|
926
|
+
function isBuildCommand(command, args) {
|
|
927
|
+
const full = [command, ...args ?? []].join(" ").trim();
|
|
928
|
+
return /\b(pnpm|npm|yarn)\s+(run\s+)?build\b/.test(full);
|
|
929
|
+
}
|
|
930
|
+
async function runTsupSafeBuild(packagePath, packageName) {
|
|
931
|
+
const startTime = Date.now();
|
|
932
|
+
const buildId = randomBytes(6).toString("hex");
|
|
933
|
+
const tempDir = join(tmpdir(), `kb-release-build-${buildId}`);
|
|
934
|
+
const distDir = join(packagePath, "dist");
|
|
935
|
+
const backupDir = join(packagePath, `dist.bak-${buildId}`);
|
|
936
|
+
try {
|
|
937
|
+
const buildResult = await spawnCommand(`npx tsup -d ${tempDir}`, packagePath);
|
|
938
|
+
if (!buildResult.success) {
|
|
939
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
940
|
+
});
|
|
941
|
+
return { ...buildResult, name: packageName, durationMs: Date.now() - startTime };
|
|
942
|
+
}
|
|
943
|
+
if (existsSync(distDir)) {
|
|
944
|
+
await rename(distDir, backupDir);
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
await rename(tempDir, distDir);
|
|
948
|
+
} catch {
|
|
949
|
+
await cp(tempDir, distDir, { recursive: true });
|
|
950
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
await rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
954
|
+
});
|
|
955
|
+
return { success: true, name: packageName, durationMs: Date.now() - startTime };
|
|
956
|
+
} catch (err) {
|
|
957
|
+
if (existsSync(backupDir) && !existsSync(distDir)) {
|
|
958
|
+
await rename(backupDir, distDir).catch(() => {
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
name: packageName,
|
|
966
|
+
error: err instanceof Error ? err.message : String(err),
|
|
967
|
+
durationMs: Date.now() - startTime
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function runDirectBuild(packagePath, packageName) {
|
|
972
|
+
const result = await spawnCommand("pnpm run build", packagePath);
|
|
973
|
+
return { ...result, name: packageName };
|
|
974
|
+
}
|
|
975
|
+
function spawnCommand(command, cwd, timeoutMs = 5 * 60 * 1e3) {
|
|
976
|
+
const startTime = Date.now();
|
|
977
|
+
return new Promise((resolve2) => {
|
|
978
|
+
const child = spawn(command, [], {
|
|
979
|
+
cwd,
|
|
980
|
+
stdio: "pipe",
|
|
981
|
+
shell: true,
|
|
982
|
+
env: { ...process.env }
|
|
983
|
+
});
|
|
984
|
+
let stdout = "";
|
|
985
|
+
let stderr = "";
|
|
986
|
+
child.stdout?.on("data", (data) => {
|
|
987
|
+
stdout += data.toString();
|
|
988
|
+
});
|
|
989
|
+
child.stderr?.on("data", (data) => {
|
|
990
|
+
stderr += data.toString();
|
|
991
|
+
});
|
|
992
|
+
child.on("close", (code) => {
|
|
993
|
+
const exitCode = code ?? 1;
|
|
994
|
+
const durationMs = Date.now() - startTime;
|
|
995
|
+
if (exitCode === 0) {
|
|
996
|
+
resolve2({ success: true, durationMs, stdout, stderr, exitCode });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const combined = (stderr || stdout).trim();
|
|
1000
|
+
const tail = combined.split("\n").slice(-30).join("\n");
|
|
1001
|
+
resolve2({
|
|
1002
|
+
success: false,
|
|
1003
|
+
error: tail || `Build failed with exit code ${exitCode}`,
|
|
1004
|
+
durationMs,
|
|
1005
|
+
stdout,
|
|
1006
|
+
stderr,
|
|
1007
|
+
exitCode
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
child.on("error", (err) => {
|
|
1011
|
+
resolve2({ success: false, error: err.message, durationMs: Date.now() - startTime, stdout: "", stderr: "", exitCode: 1 });
|
|
1012
|
+
});
|
|
1013
|
+
setTimeout(() => {
|
|
1014
|
+
child.kill();
|
|
1015
|
+
resolve2({ success: false, error: `Timed out after ${timeoutMs / 1e3}s`, durationMs: Date.now() - startTime, stdout: "", stderr: "", exitCode: 1 });
|
|
1016
|
+
}, timeoutMs);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
async function runReleaseChecks(checks, options) {
|
|
1020
|
+
const results = [];
|
|
1021
|
+
for (const check of checks) {
|
|
1022
|
+
const result = await runSingleCheck(check, options);
|
|
1023
|
+
results.push(result);
|
|
1024
|
+
options.logger?.info?.(`Check ${check.id}: ${result.ok ? "passed" : "failed"} (${result.timingMs}ms)`);
|
|
1025
|
+
if (!result.ok && !check.optional) {
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return results;
|
|
1030
|
+
}
|
|
1031
|
+
async function runSingleCheck(check, options) {
|
|
1032
|
+
const runIn = check.runIn ?? "perPackage";
|
|
1033
|
+
let pathsToRun;
|
|
1034
|
+
if (runIn === "repoRoot") {
|
|
1035
|
+
pathsToRun = [options.repoRoot];
|
|
1036
|
+
} else if (runIn === "scopePath") {
|
|
1037
|
+
pathsToRun = [options.scopePath ?? options.repoRoot];
|
|
1038
|
+
} else {
|
|
1039
|
+
pathsToRun = options.packagePaths.length > 0 ? options.packagePaths : [options.repoRoot];
|
|
1040
|
+
}
|
|
1041
|
+
let checkOk = true;
|
|
1042
|
+
let checkError;
|
|
1043
|
+
let totalDurationMs = 0;
|
|
1044
|
+
for (const pkgPath of pathsToRun) {
|
|
1045
|
+
const resolvedArgs = (check.args ?? []).map(
|
|
1046
|
+
(arg) => arg.match(/\.(sh|js|ts|mjs|cjs)$/) ? join(options.repoRoot, arg) : arg
|
|
1047
|
+
);
|
|
1048
|
+
const fullCommand = [check.command, ...resolvedArgs].join(" ");
|
|
1049
|
+
const timeoutMs = check.timeoutMs ?? 12e4;
|
|
1050
|
+
const result = await spawnCommand(fullCommand, pkgPath, timeoutMs);
|
|
1051
|
+
totalDurationMs += result.durationMs;
|
|
1052
|
+
const ok = evaluateParser(check, result.stdout, result.stderr, result.exitCode);
|
|
1053
|
+
if (!ok) {
|
|
1054
|
+
checkOk = false;
|
|
1055
|
+
checkError = result.error ?? (result.stderr || result.stdout || `exit code ${result.exitCode}`);
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
id: check.id,
|
|
1061
|
+
ok: checkOk,
|
|
1062
|
+
details: checkError ? { error: checkError } : void 0,
|
|
1063
|
+
hint: check.optional ? "optional" : void 0,
|
|
1064
|
+
timingMs: totalDurationMs
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function evaluateParser(check, stdout, stderr, exitCode) {
|
|
1068
|
+
const parser = check.parser ?? "exitcode";
|
|
1069
|
+
if (parser === "exitcode") {
|
|
1070
|
+
return exitCode === 0;
|
|
1071
|
+
}
|
|
1072
|
+
if (parser === "json") {
|
|
1073
|
+
try {
|
|
1074
|
+
const parsed = JSON.parse(stdout);
|
|
1075
|
+
return parsed.ok === true || parsed.success === true || parsed.status === "ok";
|
|
1076
|
+
} catch {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (typeof parser === "function") {
|
|
1081
|
+
return parser(stdout, stderr, exitCode);
|
|
1082
|
+
}
|
|
1083
|
+
return exitCode === 0;
|
|
1084
|
+
}
|
|
1085
|
+
async function verifyPackages(packages, options) {
|
|
1086
|
+
const results = [];
|
|
1087
|
+
for (const pkg of packages) {
|
|
1088
|
+
const result = verifyPackage(pkg.path, pkg.name);
|
|
1089
|
+
results.push(result);
|
|
1090
|
+
options?.onProgress?.(pkg.name, result);
|
|
1091
|
+
}
|
|
1092
|
+
return results;
|
|
1093
|
+
}
|
|
1094
|
+
function verifyPackage(packagePath, packageName) {
|
|
1095
|
+
const pkgJsonPath = join(packagePath, "package.json");
|
|
1096
|
+
if (!existsSync(pkgJsonPath)) {
|
|
1097
|
+
return { name: packageName ?? packagePath, success: true, issues: [] };
|
|
1098
|
+
}
|
|
1099
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
1100
|
+
const name = packageName ?? pkg.name ?? packagePath;
|
|
1101
|
+
if (pkg.private) {
|
|
1102
|
+
return { name, success: true, issues: [] };
|
|
1103
|
+
}
|
|
1104
|
+
if (!existsSync(join(packagePath, "dist"))) {
|
|
1105
|
+
return { name, success: true, issues: [] };
|
|
1106
|
+
}
|
|
1107
|
+
const issues = [];
|
|
1108
|
+
const tmpDir = join(tmpdir(), `kb-verify-${randomBytes(6).toString("hex")}`);
|
|
1109
|
+
try {
|
|
1110
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1111
|
+
const origPkg = readFileSync(pkgJsonPath, "utf-8");
|
|
1112
|
+
const modPkg = JSON.parse(origPkg);
|
|
1113
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
1114
|
+
const deps = modPkg[section];
|
|
1115
|
+
if (!deps) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
for (const [k, v] of Object.entries(deps)) {
|
|
1119
|
+
if (typeof v === "string" && v.startsWith("link:")) {
|
|
1120
|
+
deps[k] = "*";
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
writeFileSync(pkgJsonPath, JSON.stringify(modPkg, null, 2) + "\n");
|
|
1125
|
+
let tgzFile;
|
|
1126
|
+
try {
|
|
1127
|
+
execSync(`npm pack --pack-destination ${tmpDir}`, { cwd: packagePath, stdio: "pipe", timeout: 3e4 });
|
|
1128
|
+
const files = readdirSync(tmpDir).filter((f) => f.endsWith(".tgz"));
|
|
1129
|
+
tgzFile = files[0] ? join(tmpDir, files[0]) : void 0;
|
|
1130
|
+
} finally {
|
|
1131
|
+
writeFileSync(pkgJsonPath, origPkg);
|
|
1132
|
+
}
|
|
1133
|
+
if (!tgzFile) {
|
|
1134
|
+
issues.push("npm pack produced no tarball");
|
|
1135
|
+
return { name, success: false, issues };
|
|
1136
|
+
}
|
|
1137
|
+
execSync(`tar xzf ${tgzFile}`, { cwd: tmpDir, stdio: "pipe" });
|
|
1138
|
+
const extractedDir = join(tmpDir, "package");
|
|
1139
|
+
const testFiles = findFiles(
|
|
1140
|
+
join(extractedDir, "dist"),
|
|
1141
|
+
(f) => f.includes(".spec.") || f.includes(".test.") || f.includes("__tests__")
|
|
1142
|
+
);
|
|
1143
|
+
if (testFiles.length > 0) {
|
|
1144
|
+
issues.push(`Test files in dist/: ${testFiles.slice(0, 3).join(", ")}`);
|
|
1145
|
+
}
|
|
1146
|
+
const extractedPkg = JSON.parse(readFileSync(join(extractedDir, "package.json"), "utf-8"));
|
|
1147
|
+
for (const field of ["main", "module", "types"]) {
|
|
1148
|
+
const val = extractedPkg[field];
|
|
1149
|
+
if (val && !existsSync(join(extractedDir, val))) {
|
|
1150
|
+
issues.push(`${field}: ${val} does not exist in published package`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (extractedPkg.exports) {
|
|
1154
|
+
checkExportsExist(extractedPkg.exports, extractedDir, "exports", issues);
|
|
1155
|
+
}
|
|
1156
|
+
const esmEntry = resolveEsmEntry(extractedPkg);
|
|
1157
|
+
if (esmEntry) {
|
|
1158
|
+
const esmPath = join(extractedDir, esmEntry);
|
|
1159
|
+
if (existsSync(esmPath)) {
|
|
1160
|
+
checkDirectoryImports(esmPath, join(extractedDir, "dist"), issues);
|
|
1161
|
+
try {
|
|
1162
|
+
execSync(`node --check ${esmPath}`, { stdio: "pipe", timeout: 1e4 });
|
|
1163
|
+
} catch {
|
|
1164
|
+
issues.push(`ESM syntax error in ${esmEntry}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const cjsEntry = resolveCjsEntry(extractedPkg);
|
|
1169
|
+
if (cjsEntry) {
|
|
1170
|
+
const cjsPath = join(extractedDir, cjsEntry);
|
|
1171
|
+
if (existsSync(cjsPath)) {
|
|
1172
|
+
try {
|
|
1173
|
+
execSync(`node --check ${cjsPath}`, { stdio: "pipe", timeout: 1e4 });
|
|
1174
|
+
} catch {
|
|
1175
|
+
issues.push(`CJS syntax error in ${cjsEntry}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
issues.push(`Verification error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1181
|
+
} finally {
|
|
1182
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1183
|
+
}
|
|
1184
|
+
return { name, success: issues.length === 0, issues };
|
|
1185
|
+
}
|
|
1186
|
+
function resolveEsmEntry(pkg) {
|
|
1187
|
+
return pkg.exports?.["."]?.import ?? pkg.module ?? pkg.main;
|
|
1188
|
+
}
|
|
1189
|
+
function resolveCjsEntry(pkg) {
|
|
1190
|
+
const req = pkg.exports?.["."]?.require;
|
|
1191
|
+
if (req) {
|
|
1192
|
+
return req;
|
|
1193
|
+
}
|
|
1194
|
+
if (pkg.main?.endsWith(".cjs")) {
|
|
1195
|
+
return pkg.main;
|
|
1196
|
+
}
|
|
1197
|
+
return void 0;
|
|
1198
|
+
}
|
|
1199
|
+
function checkExportsExist(exports$1, baseDir, prefix, issues) {
|
|
1200
|
+
if (typeof exports$1 === "string") {
|
|
1201
|
+
if (exports$1.includes("*")) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (!existsSync(join(baseDir, exports$1))) {
|
|
1205
|
+
issues.push(`${prefix}: ${exports$1} missing`);
|
|
1206
|
+
}
|
|
1207
|
+
} else if (exports$1 && typeof exports$1 === "object") {
|
|
1208
|
+
for (const [k, v] of Object.entries(exports$1)) {
|
|
1209
|
+
if (k.includes("*")) {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
checkExportsExist(v, baseDir, `${prefix}.${k}`, issues);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function checkDirectoryImports(filePath, distDir, issues) {
|
|
1217
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1218
|
+
const importRegex = /(?:export|import)\s.*?from\s+['"](\.[^'"]*)['"]/g;
|
|
1219
|
+
let match;
|
|
1220
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1221
|
+
const target = match[1];
|
|
1222
|
+
if (!target || target.includes(".")) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const targetPath = resolve(dirname(filePath), target);
|
|
1226
|
+
if (existsSync(targetPath) && statSync(targetPath).isDirectory()) {
|
|
1227
|
+
issues.push(`Directory import '${target}' in ${filePath.split("/").pop()}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function findFiles(dir, predicate) {
|
|
1232
|
+
if (!existsSync(dir)) {
|
|
1233
|
+
return [];
|
|
1234
|
+
}
|
|
1235
|
+
const results = [];
|
|
1236
|
+
function walk(d) {
|
|
1237
|
+
try {
|
|
1238
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
1239
|
+
const full = join(d, entry.name);
|
|
1240
|
+
if (entry.isDirectory()) {
|
|
1241
|
+
walk(full);
|
|
1242
|
+
} else if (predicate(full)) {
|
|
1243
|
+
results.push(entry.name);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
walk(dir);
|
|
1250
|
+
return results;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/pipeline.ts
|
|
1254
|
+
async function runReleasePipeline(options) {
|
|
1255
|
+
const {
|
|
1256
|
+
cwd,
|
|
1257
|
+
repoRoot,
|
|
1258
|
+
scopeCwd,
|
|
1259
|
+
scope,
|
|
1260
|
+
config,
|
|
1261
|
+
dryRun = false,
|
|
1262
|
+
skipChecks = false,
|
|
1263
|
+
skipBuild = false,
|
|
1264
|
+
skipVerify = false,
|
|
1265
|
+
checks: checkConfigs,
|
|
1266
|
+
publisher,
|
|
1267
|
+
changelog: changelogGen,
|
|
1268
|
+
logger,
|
|
1269
|
+
onProgress
|
|
1270
|
+
} = options;
|
|
1271
|
+
const startTime = Date.now();
|
|
1272
|
+
const progress = (stage, msg) => {
|
|
1273
|
+
logger?.info?.(msg);
|
|
1274
|
+
onProgress?.(stage, msg);
|
|
1275
|
+
};
|
|
1276
|
+
progress("planning", "Discovering packages and planning release...");
|
|
1277
|
+
const plan = await planRelease({
|
|
1278
|
+
cwd: repoRoot,
|
|
1279
|
+
config,
|
|
1280
|
+
scope,
|
|
1281
|
+
bumpOverride: config.bump
|
|
1282
|
+
});
|
|
1283
|
+
if (plan.packages.length === 0) {
|
|
1284
|
+
return {
|
|
1285
|
+
success: false,
|
|
1286
|
+
plan,
|
|
1287
|
+
report: buildReport("planning", plan, repoRoot, dryRun, startTime, {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
errors: [`No packages found for scope: ${scope || "all"}`],
|
|
1290
|
+
timingMs: 0
|
|
1291
|
+
})
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
progress("planning", `Found ${plan.packages.length} package(s) to release`);
|
|
1295
|
+
await saveSnapshot({ cwd: repoRoot, plan });
|
|
1296
|
+
if (!skipChecks && checkConfigs && checkConfigs.length > 0) {
|
|
1297
|
+
progress("checking", `Running ${checkConfigs.length} pre-release check(s)...`);
|
|
1298
|
+
const packagePaths = plan.packages.map((p) => p.path);
|
|
1299
|
+
const checkResults = await runReleaseChecks(checkConfigs, {
|
|
1300
|
+
repoRoot,
|
|
1301
|
+
packagePaths,
|
|
1302
|
+
scopePath: scopeCwd,
|
|
1303
|
+
logger
|
|
1304
|
+
});
|
|
1305
|
+
const failed = checkResults.filter((r) => !r.ok && r.hint !== "optional");
|
|
1306
|
+
if (failed.length > 0) {
|
|
1307
|
+
await restoreSnapshot(repoRoot);
|
|
1308
|
+
return {
|
|
1309
|
+
success: false,
|
|
1310
|
+
plan,
|
|
1311
|
+
report: buildReport("checking", plan, repoRoot, dryRun, startTime, {
|
|
1312
|
+
ok: false,
|
|
1313
|
+
checks: Object.fromEntries(checkResults.map((r) => [r.id, r])),
|
|
1314
|
+
errors: [`Pre-release checks failed: ${failed.map((f) => f.id).join(", ")}`],
|
|
1315
|
+
timingMs: Date.now() - startTime
|
|
1316
|
+
})
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
progress("checking", "Pre-release checks passed");
|
|
1320
|
+
}
|
|
1321
|
+
if (!skipBuild && !dryRun) {
|
|
1322
|
+
progress("versioning", `Building ${plan.packages.length} package(s)...`);
|
|
1323
|
+
const buildResults = await buildPackages(plan.packages, { logger });
|
|
1324
|
+
const buildFailed = buildResults.filter((r) => !r.success);
|
|
1325
|
+
if (buildFailed.length > 0) {
|
|
1326
|
+
await restoreSnapshot(repoRoot);
|
|
1327
|
+
return {
|
|
1328
|
+
success: false,
|
|
1329
|
+
plan,
|
|
1330
|
+
report: buildReport("versioning", plan, repoRoot, dryRun, startTime, {
|
|
1331
|
+
ok: false,
|
|
1332
|
+
errors: buildFailed.map((f) => `Build failed: ${f.name} \u2014 ${f.error}`),
|
|
1333
|
+
timingMs: Date.now() - startTime
|
|
1334
|
+
})
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (!skipVerify && !dryRun) {
|
|
1339
|
+
progress("verifying", "Verifying package artifacts...");
|
|
1340
|
+
const verifyResults = await verifyPackages(plan.packages, { logger });
|
|
1341
|
+
const verifyFailed = verifyResults.filter((r) => !r.success);
|
|
1342
|
+
if (verifyFailed.length > 0) {
|
|
1343
|
+
await restoreSnapshot(repoRoot);
|
|
1344
|
+
const allIssues = verifyFailed.flatMap((r) => r.issues.map((i) => `${r.name}: ${i}`));
|
|
1345
|
+
return {
|
|
1346
|
+
success: false,
|
|
1347
|
+
plan,
|
|
1348
|
+
report: buildReport("verifying", plan, repoRoot, dryRun, startTime, {
|
|
1349
|
+
ok: false,
|
|
1350
|
+
errors: [`Package verification failed:
|
|
1351
|
+
${allIssues.join("\n ")}`],
|
|
1352
|
+
timingMs: Date.now() - startTime
|
|
1353
|
+
})
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
progress("verifying", "Package artifacts verified");
|
|
1357
|
+
}
|
|
1358
|
+
progress("versioning", "Updating package versions...");
|
|
1359
|
+
if (!dryRun) {
|
|
1360
|
+
const versionUpdates = await updatePackageVersions(plan);
|
|
1361
|
+
const failedUpdates = versionUpdates.filter((u) => !u.updated);
|
|
1362
|
+
if (failedUpdates.length > 0) {
|
|
1363
|
+
await restoreSnapshot(repoRoot);
|
|
1364
|
+
return {
|
|
1365
|
+
success: false,
|
|
1366
|
+
plan,
|
|
1367
|
+
report: buildReport("versioning", plan, repoRoot, dryRun, startTime, {
|
|
1368
|
+
ok: false,
|
|
1369
|
+
errors: failedUpdates.map((u) => `Version update failed: ${u.package}`),
|
|
1370
|
+
versionUpdates,
|
|
1371
|
+
timingMs: Date.now() - startTime
|
|
1372
|
+
})
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
let changelogMd = "";
|
|
1377
|
+
if (changelogGen) {
|
|
1378
|
+
progress("versioning", "Generating changelog...");
|
|
1379
|
+
try {
|
|
1380
|
+
changelogMd = await changelogGen.generate(plan, { repoRoot, gitCwd: scopeCwd, config });
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
logger?.warn?.(`Changelog generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (changelogMd && !dryRun) {
|
|
1386
|
+
await copyChangelogToPackages({ plan, changelog: changelogMd });
|
|
1387
|
+
const changelogPath = join(repoRoot, ".kb", "release", "CHANGELOG.md");
|
|
1388
|
+
await mkdir(join(repoRoot, ".kb", "release"), { recursive: true });
|
|
1389
|
+
await writeFile(changelogPath, changelogMd, "utf-8");
|
|
1390
|
+
}
|
|
1391
|
+
progress("publishing", dryRun ? "Simulating publish (dry-run)..." : "Publishing packages...");
|
|
1392
|
+
const packagesToPublish = plan.packages.map((pkg) => ({
|
|
1393
|
+
name: pkg.name,
|
|
1394
|
+
version: pkg.nextVersion,
|
|
1395
|
+
path: pkg.path
|
|
1396
|
+
}));
|
|
1397
|
+
const publishResult = await publisher.publish(packagesToPublish, {
|
|
1398
|
+
dryRun,
|
|
1399
|
+
access: "public"
|
|
1400
|
+
});
|
|
1401
|
+
let gitResult;
|
|
1402
|
+
if (!dryRun && publishResult.errors.length === 0) {
|
|
1403
|
+
progress("verifying", "Committing and tagging release...");
|
|
1404
|
+
gitResult = await commitAndTagRelease({ cwd: scopeCwd, plan, dryRun });
|
|
1405
|
+
}
|
|
1406
|
+
const report = buildReport("verifying", plan, repoRoot, dryRun, startTime, {
|
|
1407
|
+
ok: publishResult.errors.length === 0,
|
|
1408
|
+
published: publishResult.published,
|
|
1409
|
+
skipped: publishResult.skipped,
|
|
1410
|
+
changelog: changelogMd || void 0,
|
|
1411
|
+
git: gitResult ?? void 0,
|
|
1412
|
+
errors: publishResult.errors.length > 0 ? publishResult.errors : void 0,
|
|
1413
|
+
timingMs: Date.now() - startTime
|
|
1414
|
+
});
|
|
1415
|
+
const scopeDir = scope ? scope.replace(/[@/]/g, "-").replace(/^-/, "") : "root";
|
|
1416
|
+
const historyDir = join(repoRoot, ".kb", "release", "history", scopeDir, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"));
|
|
1417
|
+
await mkdir(historyDir, { recursive: true });
|
|
1418
|
+
await writeFile(join(historyDir, "report.json"), JSON.stringify(report, null, 2), "utf-8");
|
|
1419
|
+
return { success: report.result.ok, plan, report };
|
|
1420
|
+
}
|
|
1421
|
+
function buildReport(stage, plan, repoRoot, dryRun, startTime, result) {
|
|
1422
|
+
return {
|
|
1423
|
+
schemaVersion: "1.0",
|
|
1424
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1425
|
+
context: { repo: repoRoot, cwd: repoRoot, branch: "unknown", dryRun },
|
|
1426
|
+
stage,
|
|
1427
|
+
plan,
|
|
1428
|
+
result: { ...result, timingMs: result.timingMs ?? Date.now() - startTime }
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
async function resolveScopePath(repoRoot, scope) {
|
|
1432
|
+
if (!scope || scope === "root") {
|
|
1433
|
+
return repoRoot;
|
|
1434
|
+
}
|
|
1435
|
+
if (scope.startsWith("@")) {
|
|
1436
|
+
const packageJsonPaths = await globby("**/package.json", {
|
|
1437
|
+
cwd: repoRoot,
|
|
1438
|
+
absolute: true,
|
|
1439
|
+
onlyFiles: true,
|
|
1440
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**", "**/.*/**"]
|
|
1441
|
+
});
|
|
1442
|
+
for (const pkgJsonPath of packageJsonPaths) {
|
|
1443
|
+
try {
|
|
1444
|
+
const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
1445
|
+
if (pkg.name === scope) {
|
|
1446
|
+
return join(pkgJsonPath, "..");
|
|
1447
|
+
}
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return join(repoRoot, scope);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
export { applyVersionStrategy, buildPackages, commitAndTagRelease, copyChangelogToPackages, createExecaShellAdapter, generateChangelog, generateEnhancedChangelog, isBuildCommand, matchesPackagePattern, planRelease, publishPackages, renderJson, renderMarkdown, renderText, resolveScopePath, restoreSnapshot, runRelease, runReleaseChecks, runReleasePipeline, runSafeBuild, saveSnapshot, spawnCommand, updatePackageVersion, updatePackageVersions, verifyPackage, verifyPackages };
|
|
1456
|
+
//# sourceMappingURL=index.js.map
|
|
1457
|
+
//# sourceMappingURL=index.js.map
|