@git.zone/cli 6.8.0 → 6.10.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.
@@ -9,6 +9,7 @@ const supportedPnpmVersions = [
9
9
  "11.25.0",
10
10
  "12.1.0",
11
11
  "12.2.1",
12
+ "12.3.0",
12
13
  "12.3.4",
13
14
  ] as const;
14
15
  const pnpmHelpRequirements = {
@@ -49,6 +50,19 @@ const pnpmHelpRequirements = {
49
50
  "[possible values: public, restricted]",
50
51
  ],
51
52
  },
53
+ "12.3.0": {
54
+ pack: ["Usage: pnpm pack [OPTIONS]", "--out <OUT>", "--json"],
55
+ publish: [
56
+ "Usage: pnpm publish [OPTIONS] [PACKAGE]",
57
+ "Tarball or directory to publish",
58
+ "--no-git-checks",
59
+ "--ignore-scripts",
60
+ "--json",
61
+ "--tag <TAG>",
62
+ "--access <ACCESS>",
63
+ "[possible values: public, restricted]",
64
+ ],
65
+ },
52
66
  "12.3.4": {
53
67
  pack: ["Usage: pnpm pack [OPTIONS]", "--out <OUT>", "--json"],
54
68
  publish: [
@@ -77,9 +91,24 @@ export function assertNpmPackageName(value: unknown): asserts value is string {
77
91
  }
78
92
 
79
93
  export interface IPnpmReleaseCapability {
80
- version: (typeof supportedPnpmVersions)[number];
94
+ readonly version: (typeof supportedPnpmVersions)[number];
81
95
  }
82
96
 
97
+ export const buildQualifiedPnpmArgs = (
98
+ capabilityArg: IPnpmReleaseCapability | undefined,
99
+ argsArg: readonly string[],
100
+ ): string[] => {
101
+ if (
102
+ !capabilityArg ||
103
+ !supportedPnpmVersions.includes(capabilityArg.version)
104
+ ) {
105
+ throw new Error(
106
+ "npm release commands require a qualified pnpm invocation.",
107
+ );
108
+ }
109
+ return ["with", capabilityArg.version, ...argsArg];
110
+ };
111
+
83
112
  export type TNpmArtifactProbeStatus =
84
113
  "absent" | "exact" | "conflict" | "inconclusive";
85
114
 
@@ -169,21 +198,28 @@ export const assertPnpmReleaseCapability = async (
169
198
  );
170
199
  }
171
200
  const requirements = pnpmHelpRequirements[supportedVersion];
201
+ const capability = Object.freeze({ version: supportedVersion });
172
202
 
173
- const packHelp = await smartshellArg.execSpawn("pnpm", ["pack", "--help"], {
174
- cwd: cwdArg,
175
- silent: true,
176
- timeout: capabilityCommandTimeoutMs,
177
- timeoutKillGraceMs: 5_000,
178
- });
203
+ const packHelp = await smartshellArg.execSpawn(
204
+ "pnpm",
205
+ buildQualifiedPnpmArgs(capability, ["pack", "--help"]),
206
+ {
207
+ cwd: cwdArg,
208
+ silent: true,
209
+ timeout: capabilityCommandTimeoutMs,
210
+ timeoutKillGraceMs: 5_000,
211
+ },
212
+ );
179
213
  if (packHelp.exitCode !== 0) {
180
- throw new Error("Unable to inspect pnpm pack capabilities.");
214
+ throw new Error(
215
+ "Unable to inspect the selected pnpm pack capabilities. Exact release commands require standalone pnpm with support; Corepack launchers are unsupported.",
216
+ );
181
217
  }
182
218
  requireHelpFragments(packHelp.combinedOutput, requirements.pack, "pack");
183
219
 
184
220
  const publishHelp = await smartshellArg.execSpawn(
185
221
  "pnpm",
186
- ["publish", "--help"],
222
+ buildQualifiedPnpmArgs(capability, ["publish", "--help"]),
187
223
  {
188
224
  cwd: cwdArg,
189
225
  silent: true,
@@ -199,7 +235,7 @@ export const assertPnpmReleaseCapability = async (
199
235
  requirements.publish,
200
236
  "publish",
201
237
  );
202
- return { version: supportedVersion };
238
+ return capability;
203
239
  };
204
240
 
205
241
  /**
@@ -358,6 +394,7 @@ export const packNpmArtifact = async (
358
394
  cwdArg: string,
359
395
  destinationDirectoryArg: string,
360
396
  expectedVersionArg: string,
397
+ capabilityArg: IPnpmReleaseCapability | undefined,
361
398
  fileArg: string = releaseArtifactFileName,
362
399
  ): Promise<IReleaseArtifact> => {
363
400
  if (
@@ -375,7 +412,12 @@ export const packNpmArtifact = async (
375
412
  const artifactPath = plugins.path.join(destinationDirectoryArg, fileArg);
376
413
  const result = await smartshellArg.execSpawn(
377
414
  "pnpm",
378
- ["pack", "--out", artifactPath, "--json"],
415
+ buildQualifiedPnpmArgs(capabilityArg, [
416
+ "pack",
417
+ "--out",
418
+ artifactPath,
419
+ "--json",
420
+ ]),
379
421
  {
380
422
  cwd: cwdArg,
381
423
  timeout: packCommandTimeoutMs,
@@ -470,10 +512,14 @@ export const publishNpmArtifact = async (
470
512
  cwdArg: string,
471
513
  artifactPathArg: string,
472
514
  registryArg: string,
515
+ capabilityArg: IPnpmReleaseCapability | undefined,
473
516
  ): Promise<{ exitCode: number; output: string }> => {
474
517
  const result = await smartshellArg.execSpawn(
475
518
  "pnpm",
476
- buildPnpmPublishArgs(artifactPathArg, registryArg),
519
+ buildQualifiedPnpmArgs(
520
+ capabilityArg,
521
+ buildPnpmPublishArgs(artifactPathArg, registryArg),
522
+ ),
477
523
  {
478
524
  cwd: cwdArg,
479
525
  timeout: publishCommandTimeoutMs,
@@ -7,17 +7,27 @@ import {
7
7
  getReleaseDockerPromotionContext,
8
8
  getReleaseArtifacts,
9
9
  getReleaseNpmArtifact,
10
+ hasReleasePackages,
10
11
  type TReleaseJournal,
11
12
  type IReleaseJournalV2,
13
+ type IReleaseJournalV4,
12
14
  type IReleaseTargetStatus,
13
15
  type TReleaseErrorCode,
14
16
  type TReleaseTargetState,
15
17
  } from "./classes.releasejournal.js";
18
+ import {
19
+ GiteaAssetsClient,
20
+ GiteaAssetConflictError,
21
+ validateGiteaAssets,
22
+ } from "./helpers.giteaassets.js";
23
+ import { assertGiteaAssetsGitDestination } from "../helpers.workflow.js";
16
24
  import {
17
25
  probeAnonymousNpmArtifact,
18
26
  publishNpmArtifact,
19
27
  verifyStoredNpmArtifact,
20
28
  waitForAnonymousNpmArtifact,
29
+ buildQualifiedPnpmArgs,
30
+ type IPnpmReleaseCapability,
21
31
  type INpmArtifactProbeOptions,
22
32
  type INpmArtifactProbeResult,
23
33
  } from "./helpers.npmartifact.js";
@@ -73,6 +83,8 @@ export function buildReleaseGitPushArgs(
73
83
 
74
84
  type TReleaseTargetSelector =
75
85
  | { kind: "git" }
86
+ | { kind: "gitea-release" }
87
+ | { kind: "gitea-asset"; name: string }
76
88
  | { kind: "npm"; registry: string; packageName?: string }
77
89
  | { kind: "docker-qualification" }
78
90
  | { kind: "docker-promotion"; promotionId: string }
@@ -85,6 +97,7 @@ interface IGitProbeResult {
85
97
  }
86
98
 
87
99
  export interface IReleasePublicationOptions {
100
+ pnpmCapability?: IPnpmReleaseCapability;
88
101
  smartshell: plugins.smartshell.Smartshell;
89
102
  cwd: string;
90
103
  store: ReleaseJournalStore;
@@ -93,6 +106,15 @@ export interface IReleasePublicationOptions {
93
106
  recoveryAttemptId?: string;
94
107
  npmProbeOptions?: INpmArtifactProbeOptions;
95
108
  tsdocker?: ITsdockerReleaseClient;
109
+ giteaAssets?: Pick<
110
+ GiteaAssetsClient,
111
+ | "preflight"
112
+ | "inspectRelease"
113
+ | "createRelease"
114
+ | "inspectAssets"
115
+ | "uploadAsset"
116
+ | "verifyComplete"
117
+ >;
96
118
  ensurePublicationState: (expectedRemoteMainOidArg?: string) => Promise<void>;
97
119
  }
98
120
 
@@ -103,6 +125,20 @@ const getTargetStatus = (
103
125
  if (selectorArg.kind === "git") {
104
126
  return journalArg.git;
105
127
  }
128
+ if (
129
+ selectorArg.kind === "gitea-release" ||
130
+ selectorArg.kind === "gitea-asset"
131
+ ) {
132
+ if (journalArg.schemaVersion !== 4)
133
+ throw new Error("Release journal has no Gitea assets.");
134
+ if (selectorArg.kind === "gitea-release")
135
+ return journalArg.giteaAssets.release;
136
+ const entry = journalArg.giteaAssets.assets.find(
137
+ (entry) => entry.asset.name === selectorArg.name,
138
+ );
139
+ if (!entry) throw new Error("Release journal has no matching Gitea asset.");
140
+ return entry;
141
+ }
106
142
  if (selectorArg.kind === "docker-qualification") {
107
143
  if (journalArg.schemaVersion !== 2) {
108
144
  throw new Error("Release journal does not contain Docker qualification.");
@@ -613,7 +649,7 @@ const executeNpmRegistry = async (
613
649
  // the accepted artifact on resume. Older root/Docker journal behavior stays
614
650
  // unchanged.
615
651
  if (
616
- journal.schemaVersion === 3 &&
652
+ hasReleasePackages(journal) &&
617
653
  status.state === "failed" &&
618
654
  status.attempts > 0 &&
619
655
  status.error === "verification-inconclusive" &&
@@ -689,7 +725,7 @@ const executeNpmRegistry = async (
689
725
  }
690
726
  if (probe.status !== "absent") {
691
727
  if (
692
- journal.schemaVersion === 3 &&
728
+ hasReleasePackages(journal) &&
693
729
  status.state === "failed" &&
694
730
  probe.status === "inconclusive"
695
731
  ) {
@@ -713,12 +749,15 @@ const executeNpmRegistry = async (
713
749
  artifact.file,
714
750
  );
715
751
  await verifyStoredNpmArtifact(artifactPath, artifact);
752
+ // Reject a missing invocation before claiming a potentially publishing target.
753
+ buildQualifiedPnpmArgs(optionsArg.pnpmCapability, []);
716
754
  journal = await claimTarget(optionsArg.store, journal, selector);
717
755
  const publishResult = await publishNpmArtifact(
718
756
  optionsArg.smartshell,
719
757
  optionsArg.cwd,
720
758
  artifactPath,
721
759
  registryArg,
760
+ optionsArg.pnpmCapability,
722
761
  );
723
762
  probe = await probeNpm(
724
763
  journal,
@@ -1426,6 +1465,165 @@ const executeDockerPromotion = async (
1426
1465
  );
1427
1466
  };
1428
1467
 
1468
+ const executeGiteaTarget = async (
1469
+ options: IReleasePublicationOptions,
1470
+ journalArg: TReleaseJournal,
1471
+ selector: TReleaseTargetSelector,
1472
+ inspect: () => Promise<boolean>,
1473
+ publish: () => Promise<void>,
1474
+ onVerified?: (target: IReleaseTargetStatus) => void,
1475
+ ): Promise<TReleaseJournal> => {
1476
+ let journal = journalArg;
1477
+ let status = getTargetStatus(journal, selector);
1478
+ if (status.state === "conflict")
1479
+ throw new Error("Gitea publication has a recorded destination conflict.");
1480
+ const mark = async (
1481
+ state: "verified" | "failed" | "conflict",
1482
+ error: TReleaseErrorCode | null,
1483
+ ) => {
1484
+ status = getTargetStatus(journal, selector);
1485
+ journal = await updateTarget(
1486
+ options.store,
1487
+ journal,
1488
+ selector,
1489
+ status.state,
1490
+ status.attempt?.id || null,
1491
+ (target) => {
1492
+ target.state = state;
1493
+ target.error = error;
1494
+ target.attempt = null;
1495
+ if (state === "verified") onVerified?.(target);
1496
+ },
1497
+ );
1498
+ };
1499
+ let present: boolean;
1500
+ try {
1501
+ present = await inspect();
1502
+ } catch (error) {
1503
+ if (error instanceof GiteaAssetConflictError)
1504
+ await mark("conflict", "destination-conflict");
1505
+ throw error;
1506
+ }
1507
+ if (present) {
1508
+ if (status.state !== "verified") await mark("verified", null);
1509
+ return journal;
1510
+ }
1511
+ if (status.state === "verified") {
1512
+ await mark("conflict", "destination-conflict");
1513
+ throw new Error(
1514
+ "A previously verified Gitea release or attachment is missing.",
1515
+ );
1516
+ }
1517
+ if (status.state === "publishing") {
1518
+ journal = await recoverPublishingTarget(
1519
+ options.store,
1520
+ journal,
1521
+ selector,
1522
+ options.recoveryAttemptId,
1523
+ );
1524
+ }
1525
+ await options.ensurePublicationState(journal.release.mainOid);
1526
+ journal = await claimTarget(options.store, journal, selector);
1527
+ // One mutation per claimed attempt. Reconcile an accepted request even when
1528
+ // its response was lost; never overwrite an attachment or recreate a tag.
1529
+ let publicationError: unknown;
1530
+ try {
1531
+ await publish();
1532
+ } catch (error) {
1533
+ publicationError = error;
1534
+ }
1535
+ try {
1536
+ present = await inspect();
1537
+ } catch (error) {
1538
+ await mark(
1539
+ error instanceof GiteaAssetConflictError ? "conflict" : "failed",
1540
+ error instanceof GiteaAssetConflictError
1541
+ ? "destination-conflict"
1542
+ : "verification-inconclusive",
1543
+ );
1544
+ throw error;
1545
+ }
1546
+ if (present) {
1547
+ await mark("verified", null);
1548
+ return journal;
1549
+ }
1550
+ await mark(
1551
+ publicationError instanceof GiteaAssetConflictError ? "conflict" : "failed",
1552
+ publicationError instanceof GiteaAssetConflictError
1553
+ ? "destination-conflict"
1554
+ : publicationError
1555
+ ? "command-failed"
1556
+ : "verification-inconclusive",
1557
+ );
1558
+ throw new Error(
1559
+ "Gitea publication could not be verified; npm publication remains blocked. Inspect the retained journal before resuming.",
1560
+ );
1561
+ };
1562
+
1563
+ const executeGiteaAssets = async (
1564
+ options: IReleasePublicationOptions,
1565
+ journalArg: IReleaseJournalV4,
1566
+ client: NonNullable<IReleasePublicationOptions["giteaAssets"]>,
1567
+ ): Promise<IReleaseJournalV4> => {
1568
+ let releaseId = journalArg.giteaAssets.release.id;
1569
+ let journal = (await executeGiteaTarget(
1570
+ options,
1571
+ journalArg,
1572
+ { kind: "gitea-release" },
1573
+ async () => {
1574
+ const release = await client.inspectRelease();
1575
+ if (!release) return false;
1576
+ if (releaseId !== null && release.id !== releaseId)
1577
+ throw new GiteaAssetConflictError("Gitea release identity changed.");
1578
+ releaseId = release.id;
1579
+ return true;
1580
+ },
1581
+ async () => {
1582
+ await client.createRelease();
1583
+ },
1584
+ (target) => {
1585
+ (target as IReleaseJournalV4["giteaAssets"]["release"]).id = releaseId;
1586
+ },
1587
+ )) as IReleaseJournalV4;
1588
+ if (releaseId === null)
1589
+ throw new Error("Gitea release has no verified identity.");
1590
+ const expected = journal.giteaAssets.assets.map((entry) => entry.asset);
1591
+ const directory = options.store.getReleaseDirectory(journal.release.version);
1592
+ for (const { asset } of journal.giteaAssets.assets) {
1593
+ journal = (await executeGiteaTarget(
1594
+ options,
1595
+ journal,
1596
+ { kind: "gitea-asset", name: asset.name },
1597
+ async () => {
1598
+ const inspection = await client.inspectAssets(releaseId!, expected);
1599
+ return inspection.present.some((entry) => entry.name === asset.name);
1600
+ },
1601
+ async () => {
1602
+ await validateGiteaAssets(directory, expected);
1603
+ await client.uploadAsset(releaseId!, asset, directory);
1604
+ },
1605
+ )) as IReleaseJournalV4;
1606
+ }
1607
+ // One full anonymous verification immediately before entering the npm phase.
1608
+ // Resume always repeats it, including an otherwise completed journal.
1609
+ try {
1610
+ await client.verifyComplete(releaseId, expected);
1611
+ } catch (error) {
1612
+ if (error instanceof GiteaAssetConflictError) {
1613
+ await markUnclaimedTarget(
1614
+ options.store,
1615
+ journal,
1616
+ { kind: "gitea-release" },
1617
+ ["verified"],
1618
+ "conflict",
1619
+ "destination-conflict",
1620
+ );
1621
+ }
1622
+ throw error;
1623
+ }
1624
+ return journal;
1625
+ };
1626
+
1429
1627
  export const executeReleasePublication = async (
1430
1628
  optionsArg: IReleasePublicationOptions,
1431
1629
  ): Promise<TReleaseJournal> => {
@@ -1437,6 +1635,32 @@ export const executeReleasePublication = async (
1437
1635
  artifact,
1438
1636
  );
1439
1637
  }
1638
+ let giteaClient = optionsArg.giteaAssets;
1639
+ if (journal.schemaVersion === 4) {
1640
+ const { destination, tokenEnv, manifestPath, assets } = journal.giteaAssets;
1641
+ assertGiteaAssetsGitDestination(
1642
+ { ...destination, tokenEnv, manifestPath },
1643
+ optionsArg.pushUrl || "",
1644
+ );
1645
+ await validateGiteaAssets(
1646
+ optionsArg.store.getReleaseDirectory(journal.release.version),
1647
+ assets.map((entry) => entry.asset),
1648
+ );
1649
+ if (!giteaClient) {
1650
+ const token = process.env[tokenEnv];
1651
+ if (!token)
1652
+ throw new Error(
1653
+ `Gitea assets require the configured credential in ${tokenEnv}.`,
1654
+ );
1655
+ giteaClient = new GiteaAssetsClient({
1656
+ destination,
1657
+ token,
1658
+ tag: journal.release.tag,
1659
+ commit: journal.release.mainOid,
1660
+ });
1661
+ }
1662
+ await giteaClient.preflight();
1663
+ }
1440
1664
  if (journal.schemaVersion === 2) {
1441
1665
  journal = await executeDockerQualification(optionsArg, journal);
1442
1666
  if (
@@ -1447,6 +1671,8 @@ export const executeReleasePublication = async (
1447
1671
  }
1448
1672
  }
1449
1673
  journal = await executeGitTarget(optionsArg, journal);
1674
+ if (journal.schemaVersion === 4)
1675
+ journal = await executeGiteaAssets(optionsArg, journal, giteaClient!);
1450
1676
  const expectedRemoteMainOid =
1451
1677
  journal.git.state === "verified"
1452
1678
  ? journal.release.mainOid
@@ -1458,12 +1684,12 @@ export const executeReleasePublication = async (
1458
1684
  journal,
1459
1685
  registry.registry,
1460
1686
  registry.packageName,
1461
- journal.schemaVersion === 3,
1687
+ hasReleasePackages(journal),
1462
1688
  );
1463
1689
  }
1464
1690
  // Submit in dependency order before waiting for npm's publish-time scans.
1465
1691
  // Success still requires every package/registry cell to verify exactly.
1466
- if (journal.schemaVersion === 3) {
1692
+ if (hasReleasePackages(journal)) {
1467
1693
  for (const registry of journal.npm.registries) {
1468
1694
  if (
1469
1695
  registry.state !== "failed" ||