@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.
@@ -1,5 +1,12 @@
1
1
  import * as plugins from "./mod.plugins.js";
2
2
  import { InterProcessLock } from "../mod_services/classes.interprocesslock.js";
3
+ import { resolveGiteaAssetsConfiguration } from "../helpers.workflow.js";
4
+ import {
5
+ assertGiteaFrozenAssets,
6
+ validateGiteaAssets,
7
+ type IGiteaAssetDestination,
8
+ type IGiteaFrozenAsset,
9
+ } from "./helpers.giteaassets.js";
3
10
  import {
4
11
  assertReleaseCleanupResult,
5
12
  assertReleasePromotionEvidence,
@@ -151,8 +158,35 @@ export interface IReleaseJournalV3 extends Omit<
151
158
  packages: IReleasePackage[];
152
159
  }
153
160
 
161
+ export interface IReleaseGiteaAssetJournal extends IReleaseTargetStatus {
162
+ asset: IGiteaFrozenAsset;
163
+ }
164
+
165
+ export interface IReleaseGiteaAssetsJournal {
166
+ destination: IGiteaAssetDestination;
167
+ tokenEnv: string;
168
+ manifestPath: string;
169
+ release: IReleaseTargetStatus & { id: number | null };
170
+ assets: IReleaseGiteaAssetJournal[];
171
+ }
172
+
173
+ export interface IReleaseJournalV4 extends Omit<
174
+ IReleaseJournalV1,
175
+ "schemaVersion"
176
+ > {
177
+ schemaVersion: 4;
178
+ packages: IReleasePackage[];
179
+ giteaAssets: IReleaseGiteaAssetsJournal;
180
+ }
181
+
154
182
  export type TReleaseJournal =
155
- IReleaseJournalV1 | IReleaseJournalV2 | IReleaseJournalV3;
183
+ IReleaseJournalV1 | IReleaseJournalV2 | IReleaseJournalV3 | IReleaseJournalV4;
184
+
185
+ export const hasReleasePackages = (
186
+ journalArg: TReleaseJournal,
187
+ ): journalArg is IReleaseJournalV3 | IReleaseJournalV4 =>
188
+ journalArg.schemaVersion === 3 ||
189
+ (journalArg.schemaVersion === 4 && journalArg.packages.length > 0);
156
190
 
157
191
  export const releasePackageArtifactFile = (packageNameArg: string): string =>
158
192
  `package.${plugins.crypto.createHash("sha256").update(packageNameArg).digest("hex")}.tgz`;
@@ -160,7 +194,7 @@ export const releasePackageArtifactFile = (packageNameArg: string): string =>
160
194
  export const getReleaseArtifacts = (
161
195
  journalArg: TReleaseJournal,
162
196
  ): IReleaseArtifact[] =>
163
- journalArg.schemaVersion === 3
197
+ hasReleasePackages(journalArg)
164
198
  ? journalArg.packages.map((entryArg) => entryArg.artifact)
165
199
  : journalArg.artifact
166
200
  ? [journalArg.artifact]
@@ -170,12 +204,11 @@ export const getReleaseNpmArtifact = (
170
204
  journalArg: TReleaseJournal,
171
205
  packageNameArg?: string,
172
206
  ): IReleaseArtifact => {
173
- const artifact =
174
- journalArg.schemaVersion === 3
175
- ? journalArg.packages.find(
176
- (entryArg) => entryArg.artifact.packageName === packageNameArg,
177
- )?.artifact
178
- : journalArg.artifact;
207
+ const artifact = hasReleasePackages(journalArg)
208
+ ? journalArg.packages.find(
209
+ (entryArg) => entryArg.artifact.packageName === packageNameArg,
210
+ )?.artifact
211
+ : journalArg.artifact;
179
212
  if (!artifact) throw new Error("npm release target has no exact artifact.");
180
213
  return artifact;
181
214
  };
@@ -396,6 +429,12 @@ const isComplete = (journalArg: TReleaseJournal): boolean => {
396
429
  const commonComplete = [
397
430
  journalArg.git.state,
398
431
  ...journalArg.npm.registries.map((registryArg) => registryArg.state),
432
+ ...(journalArg.schemaVersion === 4
433
+ ? [
434
+ journalArg.giteaAssets.release.state,
435
+ ...journalArg.giteaAssets.assets.map((entry) => entry.state),
436
+ ]
437
+ : []),
399
438
  ].every((stateArg) => stateArg === "verified" || stateArg === "skipped");
400
439
  if (
401
440
  !commonComplete ||
@@ -418,7 +457,7 @@ const immutableJournalIdentity = (journalArg: TReleaseJournal): string =>
418
457
  schemaVersion: journalArg.schemaVersion,
419
458
  release: journalArg.release,
420
459
  artifact: journalArg.artifact,
421
- ...(journalArg.schemaVersion === 3
460
+ ...(hasReleasePackages(journalArg)
422
461
  ? { packages: journalArg.packages }
423
462
  : {}),
424
463
  git: {
@@ -431,11 +470,20 @@ const immutableJournalIdentity = (journalArg: TReleaseJournal): string =>
431
470
  tag: journalArg.npm.tag,
432
471
  alreadyPublished: journalArg.npm.alreadyPublished,
433
472
  registries: journalArg.npm.registries.map((registryArg) =>
434
- journalArg.schemaVersion === 3
473
+ hasReleasePackages(journalArg)
435
474
  ? [registryArg.packageName, registryArg.registry]
436
475
  : registryArg.registry,
437
476
  ),
438
477
  },
478
+ giteaAssets:
479
+ journalArg.schemaVersion === 4
480
+ ? {
481
+ destination: journalArg.giteaAssets.destination,
482
+ tokenEnv: journalArg.giteaAssets.tokenEnv,
483
+ manifestPath: journalArg.giteaAssets.manifestPath,
484
+ assets: journalArg.giteaAssets.assets.map((entry) => entry.asset),
485
+ }
486
+ : undefined,
439
487
  docker:
440
488
  journalArg.schemaVersion === releaseJournalSchemaVersion
441
489
  ? {
@@ -1174,6 +1222,160 @@ const assertReleaseJournalV3 = (
1174
1222
  return journal;
1175
1223
  };
1176
1224
 
1225
+ const assertReleaseJournalV4 = (
1226
+ valueArg: Record<string, unknown>,
1227
+ ): IReleaseJournalV4 => {
1228
+ assertExactKeys(
1229
+ valueArg,
1230
+ [
1231
+ "kind",
1232
+ "schemaVersion",
1233
+ "revision",
1234
+ "release",
1235
+ "artifact",
1236
+ "packages",
1237
+ "git",
1238
+ "npm",
1239
+ "giteaAssets",
1240
+ "createdAt",
1241
+ "updatedAt",
1242
+ "completedAt",
1243
+ ],
1244
+ "Release journal",
1245
+ );
1246
+ if (
1247
+ !Array.isArray(valueArg.packages) ||
1248
+ !isPlainObject(valueArg.giteaAssets) ||
1249
+ !isPlainObject(valueArg.git) ||
1250
+ !isPlainObject(valueArg.npm) ||
1251
+ !Array.isArray(valueArg.npm.registries)
1252
+ ) {
1253
+ throw new Error("Schema 4 requires an exact Gitea asset publication set.");
1254
+ }
1255
+ const assets = valueArg.giteaAssets;
1256
+ assertExactKeys(
1257
+ assets,
1258
+ ["destination", "tokenEnv", "manifestPath", "release", "assets"],
1259
+ "Gitea assets",
1260
+ );
1261
+ if (
1262
+ !isPlainObject(assets.destination) ||
1263
+ !isPlainObject(assets.release) ||
1264
+ !Array.isArray(assets.assets)
1265
+ )
1266
+ throw new Error("Invalid Gitea asset publication intent.");
1267
+ assertExactKeys(
1268
+ assets.destination,
1269
+ ["apiOrigin", "owner", "repository"],
1270
+ "Gitea destination",
1271
+ );
1272
+ resolveGiteaAssetsConfiguration({
1273
+ ...assets.destination,
1274
+ tokenEnv: assets.tokenEnv,
1275
+ manifestPath: assets.manifestPath,
1276
+ } as Parameters<typeof resolveGiteaAssetsConfiguration>[0]);
1277
+ assertExactKeys(
1278
+ assets.release,
1279
+ ["state", "attempts", "attempt", "error", "id"],
1280
+ "Gitea release target",
1281
+ );
1282
+ assertTargetStatus(assets.release, "Gitea release target");
1283
+ if (
1284
+ assets.release.state === "skipped" ||
1285
+ (assets.release.id !== null &&
1286
+ (!Number.isSafeInteger(assets.release.id) ||
1287
+ (assets.release.id as number) <= 0)) ||
1288
+ (assets.release.state === "verified" && assets.release.id === null)
1289
+ ) {
1290
+ throw new Error("Invalid Gitea release identity or status.");
1291
+ }
1292
+ const frozen: unknown[] = [];
1293
+ for (const entry of assets.assets) {
1294
+ if (!isPlainObject(entry)) throw new Error("Invalid Gitea asset target.");
1295
+ assertExactKeys(
1296
+ entry,
1297
+ ["state", "attempts", "attempt", "error", "asset"],
1298
+ "Gitea asset target",
1299
+ );
1300
+ assertTargetStatus(entry, "Gitea asset target");
1301
+ if (entry.state === "skipped")
1302
+ throw new Error("Gitea asset targets cannot be skipped.");
1303
+ frozen.push(entry.asset);
1304
+ }
1305
+ assertGiteaFrozenAssets(
1306
+ frozen,
1307
+ assets.destination as unknown as IGiteaAssetDestination,
1308
+ (valueArg.release as IReleaseJournalV1["release"])?.tag,
1309
+ );
1310
+ const commonComplete = [valueArg.git, ...valueArg.npm.registries].every(
1311
+ (entry) => entry?.state === "verified" || entry?.state === "skipped",
1312
+ );
1313
+ const common = {
1314
+ kind: valueArg.kind,
1315
+ schemaVersion: valueArg.packages.length ? 3 : 1,
1316
+ revision: valueArg.revision,
1317
+ release: valueArg.release,
1318
+ artifact: valueArg.artifact,
1319
+ ...(valueArg.packages.length ? { packages: valueArg.packages } : {}),
1320
+ git: valueArg.git,
1321
+ npm: valueArg.npm,
1322
+ createdAt: valueArg.createdAt,
1323
+ updatedAt: valueArg.updatedAt,
1324
+ completedAt: commonComplete ? valueArg.updatedAt : null,
1325
+ };
1326
+ if (valueArg.packages.length) assertReleaseJournalV3(common);
1327
+ else assertReleaseJournalV1(common);
1328
+ const journal = valueArg as unknown as IReleaseJournalV4;
1329
+ const statuses = [
1330
+ journal.git,
1331
+ journal.giteaAssets.release,
1332
+ ...journal.giteaAssets.assets,
1333
+ ...journal.npm.registries,
1334
+ ];
1335
+ if (
1336
+ journal.git.state === "skipped" ||
1337
+ statuses.filter((entry) => entry.state === "publishing").length > 1
1338
+ ) {
1339
+ throw new Error(
1340
+ "Gitea assets require an active Git target and one publication owner.",
1341
+ );
1342
+ }
1343
+ const assetStates = journal.giteaAssets.assets;
1344
+ const npmStarted = journal.npm.registries.some(
1345
+ (entry) => entry.state !== "pending",
1346
+ );
1347
+ if (
1348
+ (journal.git.state !== "verified" &&
1349
+ journal.git.state !== "conflict" &&
1350
+ statuses.slice(1).some((entry) => entry.state !== "pending")) ||
1351
+ (journal.giteaAssets.release.state !== "verified" &&
1352
+ journal.giteaAssets.release.state !== "conflict" &&
1353
+ assetStates.some((entry) => entry.state !== "pending")) ||
1354
+ (npmStarted &&
1355
+ (journal.giteaAssets.release.id === null ||
1356
+ assetStates.some(
1357
+ (entry) => !["verified", "conflict"].includes(entry.state),
1358
+ )))
1359
+ )
1360
+ throw new Error(
1361
+ "Release targets violate Git, Gitea assets, npm publication order.",
1362
+ );
1363
+ if (journal.completedAt !== null) {
1364
+ const completed = assertIsoTimestamp(
1365
+ journal.completedAt,
1366
+ "Release journal completedAt",
1367
+ );
1368
+ if (
1369
+ !isComplete(journal) ||
1370
+ completed < journal.createdAt ||
1371
+ completed > journal.updatedAt
1372
+ )
1373
+ throw new Error("Release journal completion state is inconsistent.");
1374
+ } else if (isComplete(journal))
1375
+ throw new Error("Complete release journal must record completedAt.");
1376
+ return journal;
1377
+ };
1378
+
1177
1379
  export const assertReleaseJournal = (valueArg: unknown): TReleaseJournal => {
1178
1380
  if (!isPlainObject(valueArg)) {
1179
1381
  throw new Error("Release journal root must be a JSON object.");
@@ -1185,6 +1387,7 @@ export const assertReleaseJournal = (valueArg: unknown): TReleaseJournal => {
1185
1387
  return assertReleaseJournalV2(valueArg);
1186
1388
  }
1187
1389
  if (valueArg.schemaVersion === 3) return assertReleaseJournalV3(valueArg);
1390
+ if (valueArg.schemaVersion === 4) return assertReleaseJournalV4(valueArg);
1188
1391
  throw new Error("Release journal header is invalid or unsupported.");
1189
1392
  };
1190
1393
 
@@ -1409,6 +1612,68 @@ const assertSchema2AppendOnlyTransition = (
1409
1612
  }
1410
1613
  };
1411
1614
 
1615
+ const assertSchema4AppendOnlyTransition = (
1616
+ current: IReleaseJournalV4,
1617
+ next: IReleaseJournalV4,
1618
+ ): void => {
1619
+ if (
1620
+ current.giteaAssets.release.id !== null &&
1621
+ current.giteaAssets.release.id !== next.giteaAssets.release.id
1622
+ ) {
1623
+ throw new Error("The recorded Gitea release identity cannot be replaced.");
1624
+ }
1625
+ if (
1626
+ current.giteaAssets.release.id === null &&
1627
+ next.giteaAssets.release.id !== null &&
1628
+ next.giteaAssets.release.state !== "verified"
1629
+ ) {
1630
+ throw new Error(
1631
+ "A Gitea release identity must be verified before recording it.",
1632
+ );
1633
+ }
1634
+ const statuses = (journal: IReleaseJournalV4) => [
1635
+ journal.git,
1636
+ journal.giteaAssets.release,
1637
+ ...journal.giteaAssets.assets,
1638
+ ...journal.npm.registries,
1639
+ ];
1640
+ const nextStatuses = statuses(next);
1641
+ for (const [index, before] of statuses(current).entries()) {
1642
+ const after = nextStatuses[index];
1643
+ if (before.state === "publishing" && after.state === "publishing") {
1644
+ if (
1645
+ JSON.stringify(before.attempt) !== JSON.stringify(after.attempt) ||
1646
+ before.attempts !== after.attempts
1647
+ )
1648
+ throw new Error("Publication attempt ownership cannot change.");
1649
+ continue;
1650
+ }
1651
+ if (
1652
+ (before.state === "pending" || before.state === "failed") &&
1653
+ after.state === "publishing"
1654
+ ) {
1655
+ if (after.attempts !== before.attempts + 1)
1656
+ throw new Error(
1657
+ "Publication claims must increment the attempt counter once.",
1658
+ );
1659
+ continue;
1660
+ }
1661
+ if (
1662
+ after.attempts !== before.attempts ||
1663
+ (after.state === "pending" && before.state !== "pending") ||
1664
+ (before.state === "conflict" && after.state !== "conflict") ||
1665
+ (before.state === "verified" &&
1666
+ !["verified", "conflict"].includes(after.state)) ||
1667
+ (before.state === "publishing" &&
1668
+ !["verified", "failed", "conflict"].includes(after.state))
1669
+ ) {
1670
+ throw new Error(
1671
+ "Release publication status cannot regress or rewrite attempt history.",
1672
+ );
1673
+ }
1674
+ }
1675
+ };
1676
+
1412
1677
  export const createReleaseAttempt = (): IReleaseAttempt => ({
1413
1678
  id: plugins.crypto.randomBytes(16).toString("hex"),
1414
1679
  pid: process.pid,
@@ -1479,7 +1744,8 @@ export class ReleaseJournalStore {
1479
1744
  ): string {
1480
1745
  if (
1481
1746
  fileArg !== releaseArtifactFileName &&
1482
- !/^package\.[0-9a-f]{64}\.tgz$/.test(fileArg)
1747
+ !/^package\.[0-9a-f]{64}\.tgz$/.test(fileArg) &&
1748
+ !/^asset\.[0-9a-f]{64}\.bin$/.test(fileArg)
1483
1749
  ) {
1484
1750
  throw new Error("Invalid release artifact filename.");
1485
1751
  }
@@ -1567,9 +1833,15 @@ export class ReleaseJournalStore {
1567
1833
  throw new Error("New release journals must start at revision 1.");
1568
1834
  }
1569
1835
  if (
1570
- journal.schemaVersion === 3 &&
1836
+ (journal.schemaVersion === 3 || journal.schemaVersion === 4) &&
1571
1837
  (journal.completedAt !== null ||
1572
- [journal.git, ...journal.npm.registries].some(
1838
+ [
1839
+ journal.git,
1840
+ ...journal.npm.registries,
1841
+ ...(journal.schemaVersion === 4
1842
+ ? [journal.giteaAssets.release, ...journal.giteaAssets.assets]
1843
+ : []),
1844
+ ].some(
1573
1845
  (target) =>
1574
1846
  !["pending", "skipped"].includes(target.state) ||
1575
1847
  target.attempts !== 0 ||
@@ -1578,7 +1850,7 @@ export class ReleaseJournalStore {
1578
1850
  ))
1579
1851
  ) {
1580
1852
  throw new Error(
1581
- "New schema 3 journals require unclaimed publication targets.",
1853
+ "New package and asset journals require unclaimed publication targets.",
1582
1854
  );
1583
1855
  }
1584
1856
  if (journal.schemaVersion === releaseJournalSchemaVersion) {
@@ -1646,6 +1918,16 @@ export class ReleaseJournalStore {
1646
1918
  );
1647
1919
  }
1648
1920
  }
1921
+ if (journal.schemaVersion === 4) {
1922
+ if (journal.giteaAssets.release.id !== null)
1923
+ throw new Error(
1924
+ "New Gitea asset journals must not preclaim a release identity.",
1925
+ );
1926
+ await validateGiteaAssets(
1927
+ temporaryDirectory,
1928
+ journal.giteaAssets.assets.map((entry) => entry.asset),
1929
+ );
1930
+ }
1649
1931
 
1650
1932
  const releaseDirectory = this.getReleaseDirectory(journal.release.version);
1651
1933
  const lock = new InterProcessLock({
@@ -1721,6 +2003,8 @@ export class ReleaseJournalStore {
1721
2003
  ) {
1722
2004
  assertSchema2AppendOnlyTransition(current, next);
1723
2005
  }
2006
+ if (current.schemaVersion === 4 && next.schemaVersion === 4)
2007
+ assertSchema4AppendOnlyTransition(current, next);
1724
2008
  await this.writeAtomic(version, next);
1725
2009
  return next;
1726
2010
  });