@git.zone/cli 6.7.3 → 6.9.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.
@@ -49,8 +49,14 @@ import {
49
49
  normalizeNpmRegistryUrl,
50
50
  packNpmArtifact,
51
51
  verifyStoredNpmArtifact,
52
+ type IPnpmReleaseCapability,
52
53
  } from "./helpers.npmartifact.js";
53
54
  import { executeReleasePublication } from "./helpers.releasepublication.js";
55
+ import {
56
+ assertPackageReleaseRecovery,
57
+ assertRecoveryUnpublished,
58
+ withReleasePreparationRecovery,
59
+ } from "./helpers.releasepreparation.js";
54
60
  import {
55
61
  TsdockerReleaseClient,
56
62
  canonicalReleaseJson,
@@ -87,6 +93,10 @@ const runInternal = async (argvArg: any): Promise<void> => {
87
93
  await runReleaseResume(argvArg, mode);
88
94
  return;
89
95
  }
96
+ if (subcommand === "recover") {
97
+ await runReleaseRecover(argvArg, mode);
98
+ return;
99
+ }
90
100
  if (subcommand !== undefined) {
91
101
  throw new Error(`Unknown release subcommand: ${subcommand}`);
92
102
  }
@@ -134,7 +144,7 @@ const runInternal = async (argvArg: any): Promise<void> => {
134
144
  const preflightTsdocker = workflow.targets.includes("docker")
135
145
  ? new TsdockerReleaseClient(smartshellInstance, branchContext.sourceCwd)
136
146
  : undefined;
137
- await assertFreshReleaseCapabilities(
147
+ const pnpmCapability = await assertFreshReleaseCapabilities(
138
148
  smartshellInstance,
139
149
  workflow,
140
150
  branchContext.sourceCwd,
@@ -284,6 +294,7 @@ const runInternal = async (argvArg: any): Promise<void> => {
284
294
  )
285
295
  : undefined;
286
296
  const journalContext = await createReleaseJournal({
297
+ pnpmCapability,
287
298
  smartshell: smartshellInstance,
288
299
  workflow,
289
300
  releaseCwd,
@@ -296,6 +307,7 @@ const runInternal = async (argvArg: any): Promise<void> => {
296
307
  ensurePublicationState,
297
308
  });
298
309
  const finalJournal = await executeReleasePublication({
310
+ pnpmCapability,
299
311
  smartshell: smartshellInstance,
300
312
  cwd: releaseCwd,
301
313
  store: journalContext.store,
@@ -313,7 +325,7 @@ const assertFreshReleaseCapabilities = async (
313
325
  cwdArg: string,
314
326
  pushUrlArg?: string,
315
327
  tsdockerArg?: ITsdockerReleaseClient,
316
- ): Promise<void> => {
328
+ ): Promise<IPnpmReleaseCapability | undefined> => {
317
329
  if (workflowArg.targets.includes("docker")) {
318
330
  if (!tsdockerArg) {
319
331
  throw new Error("Docker release target requires project-local tSDocker.");
@@ -332,7 +344,7 @@ const assertFreshReleaseCapabilities = async (
332
344
  await assertNoLegacyNpmPublisher(cwdArg);
333
345
  }
334
346
  if (workflowArg.targets.includes("npm")) {
335
- await assertPnpmReleaseCapability(smartshellArg, cwdArg);
347
+ return assertPnpmReleaseCapability(smartshellArg, cwdArg);
336
348
  }
337
349
  };
338
350
 
@@ -399,9 +411,12 @@ const assertReleaseConfiguration = (
399
411
  };
400
412
 
401
413
  interface ICreateReleaseJournalOptions {
414
+ pnpmCapability?: IPnpmReleaseCapability;
402
415
  smartshell: plugins.smartshell.Smartshell;
403
416
  workflow: IResolvedReleaseWorkflow;
404
417
  releaseCwd: string;
418
+ /** Exact detached checkout used only while recovering pre-journal packaging. */
419
+ npmSourceCwd?: string;
405
420
  releasePushUrl?: string;
406
421
  expectedRemoteMainOid?: string;
407
422
  version: string;
@@ -447,6 +462,7 @@ export const planReleasePackages = async (
447
462
  export const createReleaseJournal = async (
448
463
  optionsArg: ICreateReleaseJournalOptions,
449
464
  ): Promise<{ store: ReleaseJournalStore; journal: TReleaseJournal }> => {
465
+ const npmSourceCwd = optionsArg.npmSourceCwd ?? optionsArg.releaseCwd;
450
466
  const gitCommonDirectory = await resolveGitCommonDirectory(
451
467
  optionsArg.smartshell,
452
468
  optionsArg.releaseCwd,
@@ -474,7 +490,7 @@ export const createReleaseJournal = async (
474
490
  const packages: IReleasePackage[] = [];
475
491
  if (optionsArg.workflow.targets.includes("npm")) {
476
492
  if (optionsArg.workflow.npmPackageSource === "tspublish") {
477
- const plan = await planReleasePackages(optionsArg.releaseCwd);
493
+ const plan = await planReleasePackages(npmSourceCwd);
478
494
  if (plan.version !== optionsArg.version)
479
495
  throw new Error("tspublish plan version differs from the release.");
480
496
  const outputDirectory = plugins.path.join(
@@ -482,7 +498,7 @@ export const createReleaseJournal = async (
482
498
  "prepared",
483
499
  );
484
500
  const prepared = await new plugins.tspublish.TsPublish().prepare(
485
- optionsArg.releaseCwd,
501
+ npmSourceCwd,
486
502
  outputDirectory,
487
503
  );
488
504
  if (
@@ -497,6 +513,7 @@ export const createReleaseJournal = async (
497
513
  module.directory,
498
514
  temporaryDirectory,
499
515
  optionsArg.version,
516
+ optionsArg.pnpmCapability,
500
517
  releasePackageArtifactFile(module.name),
501
518
  );
502
519
  if (packed.packageName !== module.name)
@@ -509,14 +526,15 @@ export const createReleaseJournal = async (
509
526
  } else {
510
527
  artifact = await packNpmArtifact(
511
528
  optionsArg.smartshell,
512
- optionsArg.releaseCwd,
529
+ npmSourceCwd,
513
530
  temporaryDirectory,
514
531
  optionsArg.version,
532
+ optionsArg.pnpmCapability,
515
533
  );
516
534
  }
517
535
  await verifyCleanTree(
518
536
  optionsArg.smartshell,
519
- optionsArg.releaseCwd,
537
+ npmSourceCwd,
520
538
  "npm pack lifecycle scripts changed the release worktree. Aborting release.",
521
539
  );
522
540
  await optionsArg.ensurePublicationState();
@@ -650,6 +668,155 @@ const runReleaseInspect = async (
650
668
  }
651
669
  };
652
670
 
671
+ const runReleaseRecover = async (
672
+ argvArg: any,
673
+ mode: ICliMode,
674
+ ): Promise<void> => {
675
+ const allowed = new Set([
676
+ "_",
677
+ "$0",
678
+ "y",
679
+ "yes",
680
+ "plan",
681
+ "h",
682
+ "help",
683
+ "plain",
684
+ "quiet",
685
+ "agent",
686
+ "interactive",
687
+ "check-updates",
688
+ "checkUpdates",
689
+ ]);
690
+ if (
691
+ argvArg._?.length !== 3 ||
692
+ Object.keys(argvArg).some((key) => !allowed.has(key)) ||
693
+ (argvArg.plan !== undefined && typeof argvArg.plan !== "boolean")
694
+ ) {
695
+ throw new Error(
696
+ "Usage: gitzone release recover <version> [-y|--plan]. Target, version, build and test overrides are not allowed.",
697
+ );
698
+ }
699
+ const version = normalizeReleaseVersion(argvArg._[2]);
700
+ const shell = new plugins.smartshell.Smartshell({
701
+ executor: "bash",
702
+ sourceFilePaths: [],
703
+ });
704
+ const store = new ReleaseJournalStore(
705
+ await resolveGitCommonDirectory(shell, paths.cwd),
706
+ );
707
+ const existing = await plugins.fs
708
+ .lstat(store.getReleaseDirectory(version))
709
+ .then(() => true)
710
+ .catch((error: NodeJS.ErrnoException) => {
711
+ if (error.code === "ENOENT") return false;
712
+ throw error;
713
+ });
714
+ if (existing) {
715
+ if (argvArg.plan) {
716
+ printReleaseJournalSummary(await store.read(version));
717
+ return;
718
+ }
719
+ await runReleaseResume(
720
+ { _: ["release", "resume", version], yes: mode.yes },
721
+ mode,
722
+ );
723
+ return;
724
+ }
725
+ const workflow = await resolveReleaseWorkflow({ yes: true });
726
+ assertReleaseConfiguration(workflow, false);
727
+ let branch = await inspectReleaseBranch({
728
+ smartshell: shell,
729
+ cwd: paths.cwd,
730
+ gitRemote: workflow.gitRemote,
731
+ gitTargetActive: true,
732
+ pushBranch: workflow.pushBranch,
733
+ pushTags: workflow.pushTags,
734
+ mergeRequested: false,
735
+ planMode: argvArg.plan === true,
736
+ });
737
+ const refs = await captureReleaseRefs(
738
+ shell,
739
+ paths.cwd,
740
+ version,
741
+ branch.sourceOid,
742
+ );
743
+ const options = { smartshell: shell, workflow, branch, refs, version, store };
744
+ const packageName = await assertPackageReleaseRecovery(options);
745
+ if (!branch.planAncestryVerified) {
746
+ console.log(
747
+ "Remote ancestry is deferred until execution fetches the missing commit.",
748
+ );
749
+ }
750
+ console.log(
751
+ `Recover existing ${packageName}@${version}\ncommit: ${refs.mainOid}\ntag object: ${refs.tagOid}\nGit destination: ${branch.gitRemote}\nnpm registries: ${workflow.npmRegistries.join(", ")}\nBuild and pack an isolated checkout, install its journal, then publish configured targets.`,
752
+ );
753
+ if (argvArg.plan) return;
754
+ const pnpmCapability = await assertFreshReleaseCapabilities(
755
+ shell,
756
+ workflow,
757
+ paths.cwd,
758
+ branch.pushUrl,
759
+ );
760
+ if (!mode.yes) {
761
+ if (!mode.interactive)
762
+ throw new Error(
763
+ "Release recovery requires an interactive terminal or -y.",
764
+ );
765
+ if (
766
+ !(await plugins.smartinteract.SmartInteract.getCliConfirmation(
767
+ `Recover and publish existing v${version}?`,
768
+ false,
769
+ ))
770
+ )
771
+ return;
772
+ }
773
+ branch = await prepareReleaseIntegration(shell, branch);
774
+ options.branch = branch;
775
+ const ensureState = async (remote = branch.remoteMainOid) => {
776
+ await verifyReleaseRefs(shell, paths.cwd, version, refs);
777
+ await revalidateReleasePublicationState(
778
+ shell,
779
+ branch,
780
+ refs.mainOid,
781
+ remote,
782
+ );
783
+ };
784
+ const ensureUnpublished = async () => {
785
+ await ensureState();
786
+ await assertRecoveryUnpublished(options, packageName);
787
+ };
788
+ const context = await withReleasePreparationRecovery(
789
+ options,
790
+ ensureUnpublished,
791
+ async (checkout, ensureCheckout) =>
792
+ createReleaseJournal({
793
+ pnpmCapability,
794
+ smartshell: shell,
795
+ workflow,
796
+ releaseCwd: paths.cwd,
797
+ npmSourceCwd: checkout,
798
+ releasePushUrl: branch.pushUrl,
799
+ expectedRemoteMainOid: branch.remoteMainOid,
800
+ version,
801
+ refs,
802
+ ensurePublicationState: async () => {
803
+ await ensureCheckout();
804
+ await ensureUnpublished();
805
+ },
806
+ }),
807
+ );
808
+ const result = await executeReleasePublication({
809
+ pnpmCapability,
810
+ smartshell: shell,
811
+ cwd: paths.cwd,
812
+ store: context.store,
813
+ journal: context.journal,
814
+ pushUrl: branch.pushUrl,
815
+ ensurePublicationState: ensureState,
816
+ });
817
+ printReleaseJournalSummary(result);
818
+ };
819
+
653
820
  const assertResumeArguments = (argvArg: any): void => {
654
821
  const forbiddenKeys = [
655
822
  "allow-empty",
@@ -754,8 +921,12 @@ const runReleaseResume = async (
754
921
  await tsdocker.assertCapabilities();
755
922
  await tsdocker.validate(journal.docker.request);
756
923
  }
924
+ let pnpmCapability: IPnpmReleaseCapability | undefined;
757
925
  if (getReleaseArtifacts(journal).length) {
758
- await assertPnpmReleaseCapability(smartshellInstance, paths.cwd);
926
+ pnpmCapability = await assertPnpmReleaseCapability(
927
+ smartshellInstance,
928
+ paths.cwd,
929
+ );
759
930
  await assertNoLegacyNpmPublisher(paths.cwd);
760
931
  for (const artifact of getReleaseArtifacts(journal)) {
761
932
  await verifyStoredNpmArtifact(
@@ -825,6 +996,7 @@ const runReleaseResume = async (
825
996
  );
826
997
  };
827
998
  const finalJournal = await executeReleasePublication({
999
+ pnpmCapability,
828
1000
  smartshell: smartshellInstance,
829
1001
  cwd: paths.cwd,
830
1002
  store,
@@ -1284,7 +1456,8 @@ export function showHelp(mode?: ICliMode): void {
1284
1456
  if (mode?.json) {
1285
1457
  printJson({
1286
1458
  command: "release",
1287
- usage: "gitzone release [inspect [version]|resume <version>] [options]",
1459
+ usage:
1460
+ "gitzone release [inspect [version]|resume <version>|recover <version>] [options]",
1288
1461
  description:
1289
1462
  "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
1290
1463
  flags: [
@@ -1338,6 +1511,11 @@ export function showHelp(mode?: ICliMode): void {
1338
1511
  description:
1339
1512
  "Resume exact journaled publication without rebuilding or repacking npm artifacts",
1340
1513
  },
1514
+ {
1515
+ flag: "recover <version>",
1516
+ description:
1517
+ "Recover a local root npm release tag after build or pack failure before journal creation",
1518
+ },
1341
1519
  {
1342
1520
  flag: "--recover-attempt <id>",
1343
1521
  description:
@@ -1350,7 +1528,7 @@ export function showHelp(mode?: ICliMode): void {
1350
1528
 
1351
1529
  console.log("");
1352
1530
  console.log(
1353
- "Usage: gitzone release [inspect [version]|resume <version>] [options]",
1531
+ "Usage: gitzone release [inspect [version]|resume <version>|recover <version>] [options]",
1354
1532
  );
1355
1533
  console.log("");
1356
1534
  console.log("Creates a versioned release from changelog Pending entries.");
@@ -1386,6 +1564,9 @@ export function showHelp(mode?: ICliMode): void {
1386
1564
  console.log(
1387
1565
  " resume <version> Resume exact journaled publication without rebuilding or repacking npm artifacts",
1388
1566
  );
1567
+ console.log(
1568
+ " recover <version> Recover an unpublished root npm release from its existing local tag",
1569
+ );
1389
1570
  console.log(
1390
1571
  " --recover-attempt Recover one exact 32-character hexadecimal attempt ID after proving its publisher stopped",
1391
1572
  );