@kb-labs/release-manager-core 2.83.0 → 2.86.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/index.d.ts +10 -0
- package/dist/index.js +371 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ interface ReleaseContext {
|
|
|
15
15
|
interface PackageVersion {
|
|
16
16
|
name: string;
|
|
17
17
|
path: string;
|
|
18
|
+
/** Absolute path to the git repository root for this package. Populated during planning. */
|
|
19
|
+
gitRoot: string;
|
|
18
20
|
currentVersion: string;
|
|
19
21
|
nextVersion: string;
|
|
20
22
|
bump: VersionBump;
|
|
@@ -408,6 +410,14 @@ declare function commitAndTagRelease(options: {
|
|
|
408
410
|
dryRun?: boolean;
|
|
409
411
|
/** Pass --no-verify to git push and pushTags. Default: false — hooks run normally. */
|
|
410
412
|
noVerify?: boolean;
|
|
413
|
+
/** repoRoot for checkpoint updates. If omitted, checkpoint updates are skipped. */
|
|
414
|
+
repoRoot?: string;
|
|
415
|
+
/** Per-root state from checkpoint — skip roots already fully pushed. */
|
|
416
|
+
checkpointGitRoots?: Record<string, {
|
|
417
|
+
committed: boolean;
|
|
418
|
+
tagged: string[];
|
|
419
|
+
pushed: boolean;
|
|
420
|
+
}>;
|
|
411
421
|
}): Promise<{
|
|
412
422
|
committed: boolean;
|
|
413
423
|
tagged: string[];
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rm, rename, cp } from 'fs/promises';
|
|
2
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, rmSync, statSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, rmSync, statSync, unlinkSync } from 'fs';
|
|
3
3
|
import { join, relative, resolve, dirname } from 'path';
|
|
4
4
|
import simpleGit from 'simple-git';
|
|
5
5
|
import semver2 from 'semver';
|
|
6
6
|
import globby from 'globby';
|
|
7
|
-
import { discoverSubRepoPaths } from '@kb-labs/sdk';
|
|
7
|
+
import { discoverSubRepoPaths, useEnv } from '@kb-labs/sdk';
|
|
8
8
|
import { execa } from 'execa';
|
|
9
9
|
import { spawn, execSync } from 'child_process';
|
|
10
10
|
import { tmpdir } from 'os';
|
|
@@ -104,6 +104,16 @@ function mergeConfigWithFlow(config, flowName) {
|
|
|
104
104
|
...flow.checks !== void 0 && { checks: flow.checks }
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
async function isVersionPublished(name, version, registry) {
|
|
108
|
+
try {
|
|
109
|
+
const encoded = name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name;
|
|
110
|
+
const url = `${registry.replace(/\/$/, "")}/${encoded}/${version}`;
|
|
111
|
+
const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(5e3) });
|
|
112
|
+
return res.status === 200;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
107
117
|
async function planRelease(options) {
|
|
108
118
|
const { cwd, scope, bumpOverride } = options;
|
|
109
119
|
const config = options.flow ? mergeConfigWithFlow(options.config, options.flow) : options.config;
|
|
@@ -140,11 +150,38 @@ async function planRelease(options) {
|
|
|
140
150
|
const git = simpleGit(cwd, { timeout: { block: 6e4 } });
|
|
141
151
|
modifiedPackages = await detectModifiedPackages(git, packages, cwd);
|
|
142
152
|
}
|
|
153
|
+
const registry = config.registry ?? "https://registry.npmjs.org";
|
|
143
154
|
let planPackages = [];
|
|
144
155
|
for (const pkg of modifiedPackages) {
|
|
145
156
|
const bump = bumpOverride || config.bump || "auto";
|
|
146
157
|
const gitCwd = isWorkspaceRoot ? pkg.path : cwd;
|
|
147
158
|
const git = simpleGit(gitCwd, { timeout: { block: 6e4 } });
|
|
159
|
+
let gitRoot = pkg.gitRoot;
|
|
160
|
+
if (!gitRoot) {
|
|
161
|
+
gitRoot = await git.revparse(["--show-toplevel"]).catch(() => pkg.path);
|
|
162
|
+
}
|
|
163
|
+
const pkgRelPath = relative(gitRoot, join(pkg.path, "package.json"));
|
|
164
|
+
const headPkgRaw = await git.raw(["show", `HEAD:${pkgRelPath}`]).catch(() => null);
|
|
165
|
+
const headVersion = headPkgRaw ? (() => {
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(headPkgRaw).version ?? null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
})() : null;
|
|
172
|
+
if (headVersion && headVersion !== pkg.currentVersion) {
|
|
173
|
+
const alreadyPublished = await isVersionPublished(pkg.name, pkg.currentVersion, registry);
|
|
174
|
+
if (alreadyPublished) {
|
|
175
|
+
planPackages.push({
|
|
176
|
+
...pkg,
|
|
177
|
+
gitRoot,
|
|
178
|
+
nextVersion: pkg.currentVersion,
|
|
179
|
+
bump: detectBumpType(headVersion, pkg.currentVersion),
|
|
180
|
+
isPublished: true
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
148
185
|
const nextVersion = await computeNextVersion(
|
|
149
186
|
pkg.path,
|
|
150
187
|
pkg.currentVersion,
|
|
@@ -155,6 +192,7 @@ async function planRelease(options) {
|
|
|
155
192
|
);
|
|
156
193
|
planPackages.push({
|
|
157
194
|
...pkg,
|
|
195
|
+
gitRoot,
|
|
158
196
|
nextVersion,
|
|
159
197
|
bump: bump === "auto" ? detectBumpType(pkg.currentVersion, nextVersion) : bump
|
|
160
198
|
});
|
|
@@ -232,6 +270,7 @@ async function discoverPackages(cwd, config) {
|
|
|
232
270
|
packages.push({
|
|
233
271
|
name: packageJson.name,
|
|
234
272
|
path: packagePath,
|
|
273
|
+
gitRoot: "",
|
|
235
274
|
currentVersion: packageJson.version,
|
|
236
275
|
nextVersion: packageJson.version,
|
|
237
276
|
bump: "auto",
|
|
@@ -317,6 +356,7 @@ async function discoverSubRepoPackages(workspaceRoot, config) {
|
|
|
317
356
|
packages.push({
|
|
318
357
|
name: pkgJson.name,
|
|
319
358
|
path: subRepoPath,
|
|
359
|
+
gitRoot: subRepoPath,
|
|
320
360
|
currentVersion: pkgJson.version || "0.0.0",
|
|
321
361
|
nextVersion: pkgJson.version || "0.0.0",
|
|
322
362
|
bump: "auto",
|
|
@@ -425,6 +465,104 @@ function createExecaShellAdapter() {
|
|
|
425
465
|
}
|
|
426
466
|
};
|
|
427
467
|
}
|
|
468
|
+
function rewriteLinkDep(deps, depName, val, pkgPath, versionMap) {
|
|
469
|
+
const pinned = versionMap.get(depName);
|
|
470
|
+
if (pinned) {
|
|
471
|
+
deps[depName] = `^${pinned}`;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const linkPath = val.slice("link:".length);
|
|
476
|
+
const linked = JSON.parse(readFileSync(join(pkgPath, linkPath, "package.json"), "utf-8"));
|
|
477
|
+
deps[depName] = `^${linked.version ?? "*"}`;
|
|
478
|
+
} catch {
|
|
479
|
+
deps[depName] = "*";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function rewriteWorkspaceDep(deps, depName, val, versionMap) {
|
|
483
|
+
const pinned = versionMap.get(depName);
|
|
484
|
+
if (!pinned) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
deps[depName] = val === "workspace:*" ? `^${pinned}` : val.replace("workspace:", "");
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
function rewriteDepsSection(deps, pkgPath, versionMap, packageManager) {
|
|
491
|
+
let modified = false;
|
|
492
|
+
for (const depName of Object.keys(deps)) {
|
|
493
|
+
const val = deps[depName];
|
|
494
|
+
if (typeof val !== "string") {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (val.startsWith("link:")) {
|
|
498
|
+
rewriteLinkDep(deps, depName, val, pkgPath, versionMap);
|
|
499
|
+
modified = true;
|
|
500
|
+
} else if (val.startsWith("workspace:") && packageManager !== "pnpm" && rewriteWorkspaceDep(deps, depName, val, versionMap)) {
|
|
501
|
+
modified = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return modified;
|
|
505
|
+
}
|
|
506
|
+
function rewriteWorkspaceDeps(pkgPath, versionMap, packageManager) {
|
|
507
|
+
const pkgJsonPath = join(pkgPath, "package.json");
|
|
508
|
+
const original = readFileSync(pkgJsonPath, "utf-8");
|
|
509
|
+
const pkgJson = JSON.parse(original);
|
|
510
|
+
let modified = false;
|
|
511
|
+
for (const section of ["dependencies", "peerDependencies"]) {
|
|
512
|
+
const deps = pkgJson[section];
|
|
513
|
+
if (!deps) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (rewriteDepsSection(deps, pkgPath, versionMap, packageManager)) {
|
|
517
|
+
modified = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (modified) {
|
|
521
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
522
|
+
}
|
|
523
|
+
return () => writeFileSync(pkgJsonPath, original);
|
|
524
|
+
}
|
|
525
|
+
function checkpointPath(repoRoot) {
|
|
526
|
+
return join(repoRoot, ".kb", "release", "checkpoint.json");
|
|
527
|
+
}
|
|
528
|
+
function loadCheckpoint(repoRoot) {
|
|
529
|
+
const path = checkpointPath(repoRoot);
|
|
530
|
+
if (!existsSync(path)) return null;
|
|
531
|
+
try {
|
|
532
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function writeCheckpoint(repoRoot, checkpoint) {
|
|
538
|
+
mkdirSync(join(repoRoot, ".kb", "release"), { recursive: true });
|
|
539
|
+
const data = { ...checkpoint, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
540
|
+
writeFileSync(checkpointPath(repoRoot), JSON.stringify(data, null, 2));
|
|
541
|
+
}
|
|
542
|
+
function updateCheckpointGitRoot(repoRoot, gitRoot, state) {
|
|
543
|
+
const checkpoint = loadCheckpoint(repoRoot);
|
|
544
|
+
if (!checkpoint) return;
|
|
545
|
+
checkpoint.gitRoots[gitRoot] = state;
|
|
546
|
+
writeFileSync(checkpointPath(repoRoot), JSON.stringify(checkpoint, null, 2));
|
|
547
|
+
}
|
|
548
|
+
function markCheckpointComplete(repoRoot) {
|
|
549
|
+
const checkpoint = loadCheckpoint(repoRoot);
|
|
550
|
+
if (!checkpoint) return;
|
|
551
|
+
checkpoint.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
552
|
+
writeFileSync(checkpointPath(repoRoot), JSON.stringify(checkpoint, null, 2));
|
|
553
|
+
}
|
|
554
|
+
function deleteCheckpoint(repoRoot) {
|
|
555
|
+
try {
|
|
556
|
+
unlinkSync(checkpointPath(repoRoot));
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function isCheckpointResumable(checkpoint, flow, version) {
|
|
561
|
+
if (checkpoint.completedAt) return false;
|
|
562
|
+
if (checkpoint.flow !== flow) return false;
|
|
563
|
+
if (checkpoint.version !== version && version !== "independent") return false;
|
|
564
|
+
return checkpoint.publishedPackages.length > 0 && Object.values(checkpoint.gitRoots).some((s) => !s.pushed);
|
|
565
|
+
}
|
|
428
566
|
|
|
429
567
|
// src/publisher.ts
|
|
430
568
|
async function publishPackages(options) {
|
|
@@ -448,6 +586,8 @@ async function publishPackages(options) {
|
|
|
448
586
|
}
|
|
449
587
|
return result;
|
|
450
588
|
}
|
|
589
|
+
const pm = options.config?.publish?.packageManager ?? "pnpm";
|
|
590
|
+
const versionMap = new Map(plan.packages.map((p) => [p.name, p.nextVersion]));
|
|
451
591
|
for (const pkg of plan.packages) {
|
|
452
592
|
try {
|
|
453
593
|
const registry = plan.registry || "https://registry.npmjs.org";
|
|
@@ -470,16 +610,18 @@ async function publishPackages(options) {
|
|
|
470
610
|
});
|
|
471
611
|
continue;
|
|
472
612
|
}
|
|
473
|
-
const
|
|
613
|
+
const restoreDeps = rewriteWorkspaceDeps(pkg.path, versionMap, pm);
|
|
474
614
|
const access = options.config?.publish?.access ?? "public";
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
timeout: 6e4
|
|
481
|
-
|
|
482
|
-
|
|
615
|
+
let publishResult;
|
|
616
|
+
try {
|
|
617
|
+
publishResult = await shellApi.exec(
|
|
618
|
+
pm,
|
|
619
|
+
["publish", "--access", access, "--registry", registry],
|
|
620
|
+
{ cwd: pkg.path, timeout: 6e4 }
|
|
621
|
+
);
|
|
622
|
+
} finally {
|
|
623
|
+
restoreDeps();
|
|
624
|
+
}
|
|
483
625
|
if (publishResult.ok) {
|
|
484
626
|
result.published.push(`${pkg.name}@${pkg.nextVersion}`);
|
|
485
627
|
} else {
|
|
@@ -639,7 +781,7 @@ function createPackageChangelog(pkg, changelog) {
|
|
|
639
781
|
return changelog.substring(startIdx, endIdx).trim();
|
|
640
782
|
}
|
|
641
783
|
async function commitAndTagRelease(options) {
|
|
642
|
-
const { cwd, plan, dryRun, noVerify = false } = options;
|
|
784
|
+
const { cwd, plan, dryRun, noVerify = false, repoRoot, checkpointGitRoots } = options;
|
|
643
785
|
const simpleGit2 = (await import('simple-git')).default;
|
|
644
786
|
const result = {
|
|
645
787
|
committed: false,
|
|
@@ -653,12 +795,7 @@ async function commitAndTagRelease(options) {
|
|
|
653
795
|
const commitMessage = createCommitMessage(plan);
|
|
654
796
|
const pkgToRoot = /* @__PURE__ */ new Map();
|
|
655
797
|
for (const pkg of plan.packages) {
|
|
656
|
-
|
|
657
|
-
const root = (await simpleGit2(pkg.path).revparse(["--show-toplevel"])).trim();
|
|
658
|
-
pkgToRoot.set(pkg.path, root);
|
|
659
|
-
} catch {
|
|
660
|
-
pkgToRoot.set(pkg.path, pkg.path);
|
|
661
|
-
}
|
|
798
|
+
pkgToRoot.set(pkg.path, pkg.gitRoot || pkg.path);
|
|
662
799
|
}
|
|
663
800
|
const rootToPkgs = /* @__PURE__ */ new Map();
|
|
664
801
|
for (const pkg of plan.packages) {
|
|
@@ -667,55 +804,77 @@ async function commitAndTagRelease(options) {
|
|
|
667
804
|
list.push(pkg);
|
|
668
805
|
rootToPkgs.set(root, list);
|
|
669
806
|
}
|
|
807
|
+
const uniqueVersions = new Set(plan.packages.map((p) => p.nextVersion));
|
|
808
|
+
const isLockstep = plan.packages.length > 1 && uniqueVersions.size === 1;
|
|
809
|
+
const pushFlags = noVerify ? ["--no-verify"] : [];
|
|
810
|
+
const pushTagsOptions = noVerify ? ["--no-verify"] : void 0;
|
|
670
811
|
for (const [root, pkgs] of rootToPkgs) {
|
|
812
|
+
const prior = checkpointGitRoots?.[root];
|
|
813
|
+
if (prior?.pushed) {
|
|
814
|
+
result.committed = result.committed || prior.committed;
|
|
815
|
+
result.tagged.push(...prior.tagged.filter((t) => !result.tagged.includes(t)));
|
|
816
|
+
result.pushed = true;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
671
819
|
const rootGit = simpleGit2(root);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
filesToStage
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
filesToStage.push(rel(
|
|
820
|
+
let rootCommitted = prior?.committed ?? false;
|
|
821
|
+
let rootTagged = prior?.tagged ?? [];
|
|
822
|
+
if (!rootCommitted) {
|
|
823
|
+
const filesToStage = [];
|
|
824
|
+
for (const pkg of pkgs) {
|
|
825
|
+
const rel = (p) => p.startsWith(root + "/") ? p.slice(root.length + 1) : p;
|
|
826
|
+
filesToStage.push(rel(join(pkg.path, "package.json")));
|
|
827
|
+
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
828
|
+
if (existsSync(changelogPath)) {
|
|
829
|
+
filesToStage.push(rel(changelogPath));
|
|
830
|
+
}
|
|
679
831
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
832
|
+
await rootGit.add(filesToStage);
|
|
833
|
+
try {
|
|
834
|
+
await rootGit.commit(commitMessage);
|
|
835
|
+
rootCommitted = true;
|
|
836
|
+
result.committed = true;
|
|
837
|
+
} catch (commitError) {
|
|
838
|
+
const msg = commitError instanceof Error ? commitError.message : String(commitError);
|
|
839
|
+
if (!msg.includes("nothing to commit") && !msg.includes("nothing added to commit")) {
|
|
840
|
+
throw commitError;
|
|
841
|
+
}
|
|
689
842
|
}
|
|
843
|
+
} else {
|
|
844
|
+
result.committed = true;
|
|
690
845
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
result.tagged.push(tagName);
|
|
846
|
+
if (rootTagged.length === 0) {
|
|
847
|
+
if (isLockstep) {
|
|
848
|
+
const tagName = `v${plan.packages[0].nextVersion}`;
|
|
849
|
+
await rootGit.addTag(tagName);
|
|
850
|
+
rootTagged = [tagName];
|
|
851
|
+
} else {
|
|
852
|
+
for (const pkg of pkgs) {
|
|
853
|
+
const tagName = `${pkg.name}@${pkg.nextVersion}`;
|
|
854
|
+
await rootGit.addTag(tagName);
|
|
855
|
+
rootTagged.push(tagName);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
result.tagged.push(...rootTagged);
|
|
859
|
+
} else {
|
|
860
|
+
result.tagged.push(...rootTagged.filter((t) => !result.tagged.includes(t)));
|
|
707
861
|
}
|
|
708
|
-
|
|
709
|
-
const pushFlags = noVerify ? ["--no-verify"] : [];
|
|
710
|
-
const pushTagsOptions = noVerify ? ["--no-verify"] : void 0;
|
|
711
|
-
for (const root of rootToPkgs.keys()) {
|
|
712
|
-
const rootGit = simpleGit2(root);
|
|
713
|
-
if (result.committed) {
|
|
862
|
+
if (rootCommitted) {
|
|
714
863
|
await rootGit.push(pushFlags);
|
|
715
864
|
}
|
|
716
865
|
await rootGit.pushTags(pushTagsOptions);
|
|
866
|
+
if (repoRoot) {
|
|
867
|
+
updateCheckpointGitRoot(repoRoot, root, {
|
|
868
|
+
committed: rootCommitted,
|
|
869
|
+
tagged: rootTagged,
|
|
870
|
+
pushed: true
|
|
871
|
+
});
|
|
872
|
+
}
|
|
717
873
|
}
|
|
718
874
|
result.pushed = true;
|
|
875
|
+
if (repoRoot) {
|
|
876
|
+
markCheckpointComplete(repoRoot);
|
|
877
|
+
}
|
|
719
878
|
} catch (error) {
|
|
720
879
|
console.error(`Git operations failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
721
880
|
throw error;
|
|
@@ -1329,6 +1488,38 @@ function findFiles(dir, predicate) {
|
|
|
1329
1488
|
walk(dir);
|
|
1330
1489
|
return results;
|
|
1331
1490
|
}
|
|
1491
|
+
function lockPath(repoRoot) {
|
|
1492
|
+
return join(repoRoot, ".kb", "release", "release.lock");
|
|
1493
|
+
}
|
|
1494
|
+
function isProcessAlive(pid) {
|
|
1495
|
+
try {
|
|
1496
|
+
process.kill(pid, 0);
|
|
1497
|
+
return true;
|
|
1498
|
+
} catch {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function acquireLock(repoRoot, flow) {
|
|
1503
|
+
const path = lockPath(repoRoot);
|
|
1504
|
+
try {
|
|
1505
|
+
const existing = JSON.parse(readFileSync(path, "utf-8"));
|
|
1506
|
+
if (isProcessAlive(existing.pid)) {
|
|
1507
|
+
throw new Error(
|
|
1508
|
+
`Another release is already running (PID ${existing.pid}, started ${existing.startedAt}, flow: ${existing.flow ?? "unknown"}). If this is stale, delete: ${path}`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
if (err instanceof Error && err.message.includes("Another release")) throw err;
|
|
1513
|
+
}
|
|
1514
|
+
mkdirSync(join(repoRoot, ".kb", "release"), { recursive: true });
|
|
1515
|
+
writeFileSync(path, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), flow }));
|
|
1516
|
+
return () => {
|
|
1517
|
+
try {
|
|
1518
|
+
unlinkSync(path);
|
|
1519
|
+
} catch {
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1332
1523
|
|
|
1333
1524
|
// src/pipeline.ts
|
|
1334
1525
|
async function runReleasePipeline(options) {
|
|
@@ -1355,6 +1546,65 @@ async function runReleasePipeline(options) {
|
|
|
1355
1546
|
logger?.info?.(msg);
|
|
1356
1547
|
onProgress?.(stage, msg);
|
|
1357
1548
|
};
|
|
1549
|
+
const releaseLock = acquireLock(repoRoot, flow);
|
|
1550
|
+
try {
|
|
1551
|
+
return await _runPipeline({
|
|
1552
|
+
repoRoot,
|
|
1553
|
+
scopeCwd,
|
|
1554
|
+
scope,
|
|
1555
|
+
flow,
|
|
1556
|
+
config,
|
|
1557
|
+
dryRun,
|
|
1558
|
+
skipChecks,
|
|
1559
|
+
skipBuild,
|
|
1560
|
+
skipVerify,
|
|
1561
|
+
noVerify,
|
|
1562
|
+
checkConfigs,
|
|
1563
|
+
publisher,
|
|
1564
|
+
changelogGen,
|
|
1565
|
+
logger,
|
|
1566
|
+
startTime,
|
|
1567
|
+
progress,
|
|
1568
|
+
options
|
|
1569
|
+
});
|
|
1570
|
+
} finally {
|
|
1571
|
+
releaseLock();
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
async function _runPipeline(ctx) {
|
|
1575
|
+
const {
|
|
1576
|
+
repoRoot,
|
|
1577
|
+
scopeCwd,
|
|
1578
|
+
scope,
|
|
1579
|
+
flow,
|
|
1580
|
+
config,
|
|
1581
|
+
dryRun,
|
|
1582
|
+
skipChecks,
|
|
1583
|
+
skipBuild,
|
|
1584
|
+
skipVerify,
|
|
1585
|
+
noVerify,
|
|
1586
|
+
checkConfigs,
|
|
1587
|
+
publisher,
|
|
1588
|
+
changelogGen,
|
|
1589
|
+
logger,
|
|
1590
|
+
startTime,
|
|
1591
|
+
progress
|
|
1592
|
+
} = ctx;
|
|
1593
|
+
if (!dryRun) {
|
|
1594
|
+
const registry = config.registry ?? "https://registry.npmjs.org";
|
|
1595
|
+
const authError = await verifyNpmAuth(registry);
|
|
1596
|
+
if (authError) {
|
|
1597
|
+
return {
|
|
1598
|
+
success: false,
|
|
1599
|
+
plan: { packages: [], strategy: "semver", registry, rollbackEnabled: false },
|
|
1600
|
+
report: buildReport("planning", { packages: [], strategy: "semver", registry, rollbackEnabled: false }, repoRoot, dryRun, startTime, {
|
|
1601
|
+
ok: false,
|
|
1602
|
+
errors: [`npm auth check failed: ${authError}`],
|
|
1603
|
+
timingMs: Date.now() - startTime
|
|
1604
|
+
})
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1358
1608
|
progress("planning", "Discovering packages and planning release...");
|
|
1359
1609
|
const plan = await planRelease({
|
|
1360
1610
|
cwd: repoRoot,
|
|
@@ -1376,6 +1626,36 @@ async function runReleasePipeline(options) {
|
|
|
1376
1626
|
};
|
|
1377
1627
|
}
|
|
1378
1628
|
progress("planning", `Found ${plan.packages.length} package(s) to release`);
|
|
1629
|
+
if (!dryRun) {
|
|
1630
|
+
const existingCheckpoint = loadCheckpoint(repoRoot);
|
|
1631
|
+
if (existingCheckpoint) {
|
|
1632
|
+
const uniqueVersions = new Set(plan.packages.map((p) => p.nextVersion));
|
|
1633
|
+
const planVersion = uniqueVersions.size === 1 ? plan.packages[0].nextVersion : "independent";
|
|
1634
|
+
if (isCheckpointResumable(existingCheckpoint, flow ?? "default", planVersion)) {
|
|
1635
|
+
progress("publishing", "Resuming from checkpoint \u2014 publish already done, running git step...");
|
|
1636
|
+
const resumeGit = await commitAndTagRelease({
|
|
1637
|
+
cwd: scopeCwd,
|
|
1638
|
+
plan,
|
|
1639
|
+
dryRun: false,
|
|
1640
|
+
noVerify,
|
|
1641
|
+
repoRoot,
|
|
1642
|
+
checkpointGitRoots: existingCheckpoint.gitRoots
|
|
1643
|
+
});
|
|
1644
|
+
deleteCheckpoint(repoRoot);
|
|
1645
|
+
const resumeReport = buildReport("verifying", plan, repoRoot, dryRun, startTime, {
|
|
1646
|
+
ok: resumeGit.committed,
|
|
1647
|
+
published: existingCheckpoint.publishedPackages.map((p) => `${p.name}@${p.version}`),
|
|
1648
|
+
git: resumeGit,
|
|
1649
|
+
timingMs: Date.now() - startTime
|
|
1650
|
+
});
|
|
1651
|
+
const scopeDir2 = scope ? scope.replace(/[@/]/g, "-").replace(/^-/, "") : "root";
|
|
1652
|
+
const historyDir2 = join(repoRoot, ".kb", "release", "history", scopeDir2, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"));
|
|
1653
|
+
await mkdir(historyDir2, { recursive: true });
|
|
1654
|
+
await writeFile(join(historyDir2, "report.json"), JSON.stringify(resumeReport, null, 2), "utf-8");
|
|
1655
|
+
return { success: resumeReport.result.ok, plan, report: resumeReport };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1379
1659
|
await saveSnapshot({ cwd: repoRoot, plan });
|
|
1380
1660
|
if (!skipChecks && checkConfigs && checkConfigs.length > 0) {
|
|
1381
1661
|
progress("checking", `Running ${checkConfigs.length} pre-release check(s)...`);
|
|
@@ -1522,10 +1802,26 @@ async function runReleasePipeline(options) {
|
|
|
1522
1802
|
})
|
|
1523
1803
|
};
|
|
1524
1804
|
}
|
|
1805
|
+
if (!dryRun) {
|
|
1806
|
+
const uniqueVersions = new Set(plan.packages.map((p) => p.nextVersion));
|
|
1807
|
+
const cpVersion = uniqueVersions.size === 1 ? plan.packages[0].nextVersion : "independent";
|
|
1808
|
+
writeCheckpoint(repoRoot, {
|
|
1809
|
+
flow: flow ?? "default",
|
|
1810
|
+
version: cpVersion,
|
|
1811
|
+
publishedPackages: plan.packages.map((p) => ({
|
|
1812
|
+
name: p.name,
|
|
1813
|
+
version: p.nextVersion,
|
|
1814
|
+
path: p.path,
|
|
1815
|
+
gitRoot: p.gitRoot
|
|
1816
|
+
})),
|
|
1817
|
+
gitRoots: {}
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1525
1820
|
let gitResult;
|
|
1526
1821
|
if (!dryRun) {
|
|
1527
1822
|
progress("verifying", "Committing and tagging release...");
|
|
1528
|
-
gitResult = await commitAndTagRelease({ cwd: scopeCwd, plan, dryRun, noVerify });
|
|
1823
|
+
gitResult = await commitAndTagRelease({ cwd: scopeCwd, plan, dryRun, noVerify, repoRoot });
|
|
1824
|
+
deleteCheckpoint(repoRoot);
|
|
1529
1825
|
}
|
|
1530
1826
|
const report = buildReport("verifying", plan, repoRoot, dryRun, startTime, {
|
|
1531
1827
|
ok: !gitResult || gitResult.committed,
|
|
@@ -1542,6 +1838,24 @@ async function runReleasePipeline(options) {
|
|
|
1542
1838
|
await writeFile(join(historyDir, "report.json"), JSON.stringify(report, null, 2), "utf-8");
|
|
1543
1839
|
return { success: report.result.ok, plan, report };
|
|
1544
1840
|
}
|
|
1841
|
+
async function verifyNpmAuth(registry) {
|
|
1842
|
+
const token = useEnv("NPM_TOKEN") ?? useEnv("NODE_AUTH_TOKEN");
|
|
1843
|
+
if (!token) {
|
|
1844
|
+
return "NPM_TOKEN or NODE_AUTH_TOKEN environment variable is not set";
|
|
1845
|
+
}
|
|
1846
|
+
try {
|
|
1847
|
+
const res = await fetch(`${registry}/-/whoami`, {
|
|
1848
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1849
|
+
signal: AbortSignal.timeout(8e3)
|
|
1850
|
+
});
|
|
1851
|
+
if (!res.ok) {
|
|
1852
|
+
return `npm token invalid or expired (HTTP ${res.status} from ${registry})`;
|
|
1853
|
+
}
|
|
1854
|
+
return null;
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
return `npm registry unreachable: ${e instanceof Error ? e.message : String(e)}`;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1545
1859
|
function buildReport(stage, plan, repoRoot, dryRun, startTime, result) {
|
|
1546
1860
|
return {
|
|
1547
1861
|
schemaVersion: "1.0",
|