@kb-labs/release-manager-core 2.85.0 → 2.87.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 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 pm = options.config?.publish?.packageManager ?? "pnpm";
613
+ const restoreDeps = rewriteWorkspaceDeps(pkg.path, versionMap, pm);
474
614
  const access = options.config?.publish?.access ?? "public";
475
- const publishResult = await shellApi.exec(
476
- pm,
477
- ["publish", "--access", access, "--registry", registry],
478
- {
479
- cwd: pkg.path,
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
- try {
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
- const filesToStage = [];
673
- for (const pkg of pkgs) {
674
- const rel = (p) => p.startsWith(root + "/") ? p.slice(root.length + 1) : p;
675
- filesToStage.push(rel(join(pkg.path, "package.json")));
676
- const changelogPath = join(pkg.path, "CHANGELOG.md");
677
- if (existsSync(changelogPath)) {
678
- filesToStage.push(rel(changelogPath));
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
- await rootGit.add(filesToStage);
682
- try {
683
- await rootGit.commit(commitMessage);
684
- result.committed = true;
685
- } catch (commitError) {
686
- const msg = commitError instanceof Error ? commitError.message : String(commitError);
687
- if (!msg.includes("nothing to commit") && !msg.includes("nothing added to commit")) {
688
- throw commitError;
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
- const uniqueVersions = new Set(plan.packages.map((p) => p.nextVersion));
693
- const isLockstep = plan.packages.length > 1 && uniqueVersions.size === 1;
694
- if (isLockstep) {
695
- const version = plan.packages[0].nextVersion;
696
- const tagName = `v${version}`;
697
- for (const root of rootToPkgs.keys()) {
698
- await simpleGit2(root).addTag(tagName);
699
- }
700
- result.tagged.push(tagName);
701
- } else {
702
- for (const pkg of plan.packages) {
703
- const root = pkgToRoot.get(pkg.path);
704
- const tagName = `${pkg.name}@${pkg.nextVersion}`;
705
- await simpleGit2(root).addTag(tagName);
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",