@git.zone/cli 6.6.1 → 6.7.1

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.
Files changed (51) hide show
  1. package/.smartconfig.json +2 -1
  2. package/assets/templates/asset_tspack_release_gitea/.gitea/workflows/release.yml +4 -3
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/gitzone.cli.js +2 -2
  5. package/dist_ts/helpers.workflow.d.ts +3 -0
  6. package/dist_ts/helpers.workflow.js +14 -1
  7. package/dist_ts/mod_config/index.js +4 -1
  8. package/dist_ts/mod_deprecate/helpers.deprecation.d.ts +12 -0
  9. package/dist_ts/mod_deprecate/helpers.deprecation.js +87 -0
  10. package/dist_ts/mod_deprecate/index.d.ts +1 -1
  11. package/dist_ts/mod_deprecate/index.js +77 -40
  12. package/dist_ts/mod_format/classes.baseformatter.d.ts +6 -0
  13. package/dist_ts/mod_format/classes.baseformatter.js +61 -1
  14. package/dist_ts/mod_format/classes.formatplanner.js +14 -3
  15. package/dist_ts/mod_format/classes.formatstats.d.ts +4 -1
  16. package/dist_ts/mod_format/classes.formatstats.js +11 -1
  17. package/dist_ts/mod_format/formatters/readme.formatter.d.ts +2 -1
  18. package/dist_ts/mod_format/formatters/readme.formatter.js +80 -14
  19. package/dist_ts/mod_format/index.js +2 -1
  20. package/dist_ts/mod_format/interfaces.format.d.ts +6 -2
  21. package/dist_ts/mod_format/interfaces.format.js +1 -1
  22. package/dist_ts/mod_release/classes.releasejournal.d.ts +17 -3
  23. package/dist_ts/mod_release/classes.releasejournal.js +144 -12
  24. package/dist_ts/mod_release/helpers.npmartifact.d.ts +3 -1
  25. package/dist_ts/mod_release/helpers.npmartifact.js +64 -5
  26. package/dist_ts/mod_release/helpers.releasepublication.js +21 -19
  27. package/dist_ts/mod_release/index.d.ts +24 -0
  28. package/dist_ts/mod_release/index.js +82 -22
  29. package/dist_ts/plugins.d.ts +2 -1
  30. package/dist_ts/plugins.js +3 -2
  31. package/package.json +3 -3
  32. package/readme.md +80 -4
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/gitzone.cli.ts +1 -1
  35. package/ts/helpers.workflow.ts +17 -0
  36. package/ts/mod_config/index.ts +3 -0
  37. package/ts/mod_deprecate/helpers.deprecation.ts +151 -0
  38. package/ts/mod_deprecate/index.ts +92 -41
  39. package/ts/mod_format/classes.baseformatter.ts +71 -0
  40. package/ts/mod_format/classes.formatplanner.ts +16 -3
  41. package/ts/mod_format/classes.formatstats.ts +14 -1
  42. package/ts/mod_format/formatters/readme.formatter.ts +89 -14
  43. package/ts/mod_format/index.ts +1 -0
  44. package/ts/mod_format/interfaces.format.ts +7 -2
  45. package/ts/mod_release/classes.releasejournal.ts +219 -20
  46. package/ts/mod_release/helpers.npmartifact.ts +120 -23
  47. package/ts/mod_release/helpers.releasepublication.ts +49 -15
  48. package/ts/mod_release/index.ts +127 -30
  49. package/ts/plugins.ts +2 -0
  50. package/readme.hints.md +0 -596
  51. package/readme.plan.md +0 -176
@@ -5,7 +5,12 @@ import {
5
5
  } from "./classes.releasejournal.js";
6
6
  import { assertCredentialFreeGitPushDestination } from "./helpers.releasebranch.js";
7
7
 
8
- const supportedPnpmVersions = ["11.25.0", "12.1.0", "12.2.1", "12.3.4"] as const;
8
+ const supportedPnpmVersions = [
9
+ "11.25.0",
10
+ "12.1.0",
11
+ "12.2.1",
12
+ "12.3.4",
13
+ ] as const;
9
14
  const pnpmHelpRequirements = {
10
15
  "11.25.0": {
11
16
  pack: ["--out <path>", "--json"],
@@ -62,17 +67,15 @@ const maximumMetadataResponseBytes = 8 * 1024 * 1024;
62
67
  const capabilityCommandTimeoutMs = 30_000;
63
68
  const packCommandTimeoutMs = 10 * 60_000;
64
69
  const publishCommandTimeoutMs = 5 * 60_000;
65
- const packageNameRegex = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
70
+ const packageNameRegex =
71
+ /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
66
72
 
67
73
  export interface IPnpmReleaseCapability {
68
74
  version: (typeof supportedPnpmVersions)[number];
69
75
  }
70
76
 
71
77
  export type TNpmArtifactProbeStatus =
72
- | "absent"
73
- | "exact"
74
- | "conflict"
75
- | "inconclusive";
78
+ "absent" | "exact" | "conflict" | "inconclusive";
76
79
 
77
80
  export interface INpmArtifactProbeResult {
78
81
  status: TNpmArtifactProbeStatus;
@@ -99,7 +102,10 @@ const readPackageIdentity = async (
99
102
  let packageJson: unknown;
100
103
  try {
101
104
  packageJson = JSON.parse(
102
- await plugins.fs.readFile(plugins.path.join(cwdArg, "package.json"), "utf8"),
105
+ await plugins.fs.readFile(
106
+ plugins.path.join(cwdArg, "package.json"),
107
+ "utf8",
108
+ ),
103
109
  );
104
110
  } catch (error) {
105
111
  throw new Error("Unable to read package.json for npm release packaging.", {
@@ -190,12 +196,67 @@ export const assertPnpmReleaseCapability = async (
190
196
  return { version: supportedVersion };
191
197
  };
192
198
 
199
+ /**
200
+ * Development documents must never reach a published tarball.
201
+ *
202
+ * npm-packlist force-includes every root level file whose name starts with
203
+ * "readme" (case insensitive), so neither the package.json files array nor
204
+ * .npmignore can keep readme.plan.md or readme.hints.md out of the package.
205
+ * plan.md and hints.md are not force-included but a broad files array can still
206
+ * capture them, so both spellings are refused here.
207
+ */
208
+ const developmentDocumentPatterns = [
209
+ /^readme\.plan(?:\..+)?$/i,
210
+ /^readme\.hints(?:\..+)?$/i,
211
+ /^plan\.md$/i,
212
+ /^hints\.md$/i,
213
+ /^readme\..+\.md$/i,
214
+ ] as const;
215
+
216
+ export const findDevelopmentDocumentsInPackage = (
217
+ filePathsArg: readonly string[],
218
+ ): string[] => {
219
+ const offenders = new Set<string>();
220
+ for (const filePath of filePathsArg) {
221
+ if (typeof filePath !== "string" || !filePath) continue;
222
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
223
+ // Only root level files matter, npm force-inclusion is a root level rule.
224
+ if (normalized.includes("/")) continue;
225
+ if (
226
+ developmentDocumentPatterns.some((patternArg) =>
227
+ patternArg.test(normalized),
228
+ )
229
+ ) {
230
+ offenders.add(normalized);
231
+ }
232
+ }
233
+ return [...offenders].sort();
234
+ };
235
+
236
+ export const assertNoDevelopmentDocumentsInPackage = (
237
+ filePathsArg: readonly string[],
238
+ ): void => {
239
+ const offenders = findDevelopmentDocumentsInPackage(filePathsArg);
240
+ if (offenders.length === 0) {
241
+ return;
242
+ }
243
+ throw new Error(
244
+ `The npm package would publish development documents: ${offenders.join(", ")}. ` +
245
+ 'npm force-includes every root file whose name starts with "readme", so neither the ' +
246
+ "package.json files array nor .npmignore can exclude them. Rename them to names that are " +
247
+ "not published (readme.plan.md -> plan.md, readme.hints.md -> hints.md; `gitzone format` " +
248
+ "performs this rename), or drop them from the files array, then release again.",
249
+ );
250
+ };
251
+
193
252
  const legacyPublishWorkflowPaths = [
194
253
  ".gitea/workflows/default_tags.yaml",
195
254
  ".gitlab-ci.yml",
196
255
  ] as const;
197
256
 
198
- export const assertNoLegacyNpmPublisher = async (cwdArg: string): Promise<void> => {
257
+ export const assertNoLegacyNpmPublisher = async (
258
+ cwdArg: string,
259
+ ): Promise<void> => {
199
260
  for (const relativePath of legacyPublishWorkflowPaths) {
200
261
  const filePath = plugins.path.join(cwdArg, relativePath);
201
262
  let content: string;
@@ -205,9 +266,12 @@ export const assertNoLegacyNpmPublisher = async (cwdArg: string): Promise<void>
205
266
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
206
267
  continue;
207
268
  }
208
- throw new Error(`Unable to inspect ${relativePath} for legacy npm publication.`, {
209
- cause: error,
210
- });
269
+ throw new Error(
270
+ `Unable to inspect ${relativePath} for legacy npm publication.`,
271
+ {
272
+ cause: error,
273
+ },
274
+ );
211
275
  }
212
276
  const activeContent = content
213
277
  .split("\n")
@@ -288,17 +352,21 @@ export const packNpmArtifact = async (
288
352
  cwdArg: string,
289
353
  destinationDirectoryArg: string,
290
354
  expectedVersionArg: string,
355
+ fileArg: string = releaseArtifactFileName,
291
356
  ): Promise<IReleaseArtifact> => {
357
+ if (
358
+ fileArg !== releaseArtifactFileName &&
359
+ !/^package\.[0-9a-f]{64}\.tgz$/.test(fileArg)
360
+ ) {
361
+ throw new Error("Invalid release artifact filename.");
362
+ }
292
363
  const identity = await readPackageIdentity(cwdArg);
293
364
  if (identity.version !== expectedVersionArg) {
294
365
  throw new Error(
295
366
  `package.json version ${identity.version} does not match release ${expectedVersionArg}.`,
296
367
  );
297
368
  }
298
- const artifactPath = plugins.path.join(
299
- destinationDirectoryArg,
300
- releaseArtifactFileName,
301
- );
369
+ const artifactPath = plugins.path.join(destinationDirectoryArg, fileArg);
302
370
  const result = await smartshellArg.execSpawn(
303
371
  "pnpm",
304
372
  ["pack", "--out", artifactPath, "--json"],
@@ -309,13 +377,17 @@ export const packNpmArtifact = async (
309
377
  },
310
378
  );
311
379
  if (result.exitCode !== 0) {
312
- throw new Error("pnpm pack failed while creating the exact release artifact.");
380
+ throw new Error(
381
+ "pnpm pack failed while creating the exact release artifact.",
382
+ );
313
383
  }
314
384
  let report: unknown;
315
385
  try {
316
386
  const stdout = result.stdout.trim();
317
387
  const reportStart = stdout.lastIndexOf("\n{");
318
- report = JSON.parse(reportStart === -1 ? stdout : stdout.slice(reportStart + 1));
388
+ report = JSON.parse(
389
+ reportStart === -1 ? stdout : stdout.slice(reportStart + 1),
390
+ );
319
391
  } catch (error) {
320
392
  throw new Error("pnpm pack did not return its required JSON report.", {
321
393
  cause: error,
@@ -330,12 +402,26 @@ export const packNpmArtifact = async (
330
402
  ) {
331
403
  throw new Error("pnpm pack reported a different package identity.");
332
404
  }
405
+ const reportedFiles = (report as { files?: unknown }).files;
406
+ if (!Array.isArray(reportedFiles)) {
407
+ throw new Error(
408
+ "pnpm pack did not report the packed file list, so the package contents cannot be verified.",
409
+ );
410
+ }
411
+ assertNoDevelopmentDocumentsInPackage(
412
+ reportedFiles.map((entryArg) =>
413
+ typeof entryArg === "string"
414
+ ? entryArg
415
+ : String((entryArg as { path?: unknown })?.path ?? ""),
416
+ ),
417
+ );
333
418
  await syncFile(artifactPath);
334
- return calculateNpmArtifact(
419
+ const artifact = await calculateNpmArtifact(
335
420
  artifactPath,
336
421
  identity.packageName,
337
422
  identity.version,
338
423
  );
424
+ return { ...artifact, file: fileArg };
339
425
  };
340
426
 
341
427
  export const verifyStoredNpmArtifact = async (
@@ -353,7 +439,9 @@ export const verifyStoredNpmArtifact = async (
353
439
  actual.sha256 !== expectedArtifactArg.sha256 ||
354
440
  actual.integrity !== expectedArtifactArg.integrity
355
441
  ) {
356
- throw new Error("Stored npm artifact no longer matches its release journal identity.");
442
+ throw new Error(
443
+ "Stored npm artifact no longer matches its release journal identity.",
444
+ );
357
445
  }
358
446
  };
359
447
 
@@ -451,10 +539,15 @@ const readBoundedResponse = async (
451
539
  }
452
540
  reader.releaseLock();
453
541
  }
454
- return Buffer.concat(chunks.map((chunkArg) => Buffer.from(chunkArg)), size);
542
+ return Buffer.concat(
543
+ chunks.map((chunkArg) => Buffer.from(chunkArg)),
544
+ size,
545
+ );
455
546
  };
456
547
 
457
- const readBoundedJsonResponse = async (responseArg: Response): Promise<unknown> =>
548
+ const readBoundedJsonResponse = async (
549
+ responseArg: Response,
550
+ ): Promise<unknown> =>
458
551
  JSON.parse(
459
552
  (
460
553
  await readBoundedResponse(
@@ -561,7 +654,9 @@ export const probeAnonymousNpmArtifact = async (
561
654
  cancelResponseBody(packageResponse);
562
655
  return { status: "inconclusive", code: "registry-response" };
563
656
  }
564
- const packageMetadata = (await readBoundedJsonResponse(packageResponse)) as {
657
+ const packageMetadata = (await readBoundedJsonResponse(
658
+ packageResponse,
659
+ )) as {
565
660
  ["dist-tags"]?: Record<string, unknown>;
566
661
  };
567
662
  if (packageMetadata["dist-tags"]?.[tagArg] !== artifactArg.version) {
@@ -582,7 +677,9 @@ export const waitForAnonymousNpmArtifact = async (
582
677
  const attempts = optionsArg.attempts ?? 10;
583
678
  const delayMs = optionsArg.delayMs ?? 1_000;
584
679
  if (!Number.isSafeInteger(attempts) || attempts < 1) {
585
- throw new Error("npm verification attempts must be a positive safe integer.");
680
+ throw new Error(
681
+ "npm verification attempts must be a positive safe integer.",
682
+ );
586
683
  }
587
684
  let lastResult: INpmArtifactProbeResult = {
588
685
  status: "inconclusive",
@@ -5,6 +5,8 @@ import {
5
5
  createReleaseDockerPromotions,
6
6
  finalizeJournalCompletion,
7
7
  getReleaseDockerPromotionContext,
8
+ getReleaseArtifacts,
9
+ getReleaseNpmArtifact,
8
10
  type TReleaseJournal,
9
11
  type IReleaseJournalV2,
10
12
  type IReleaseTargetStatus,
@@ -71,7 +73,7 @@ export function buildReleaseGitPushArgs(
71
73
 
72
74
  type TReleaseTargetSelector =
73
75
  | { kind: "git" }
74
- | { kind: "npm"; registry: string }
76
+ | { kind: "npm"; registry: string; packageName?: string }
75
77
  | { kind: "docker-qualification" }
76
78
  | { kind: "docker-promotion"; promotionId: string }
77
79
  | { kind: "docker-cleanup" };
@@ -128,7 +130,9 @@ const getTargetStatus = (
128
130
  return promotion;
129
131
  }
130
132
  const registry = journalArg.npm.registries.find(
131
- (registryArg) => registryArg.registry === selectorArg.registry,
133
+ (registryArg) =>
134
+ registryArg.registry === selectorArg.registry &&
135
+ registryArg.packageName === selectorArg.packageName,
132
136
  );
133
137
  if (!registry) {
134
138
  throw new Error(
@@ -486,20 +490,19 @@ const probeNpm = async (
486
490
  registryArg: string,
487
491
  optionsArg: IReleasePublicationOptions,
488
492
  waitArg = false,
493
+ packageNameArg?: string,
489
494
  ): Promise<INpmArtifactProbeResult> => {
490
- if (!journalArg.artifact) {
491
- throw new Error("npm release target has no exact artifact.");
492
- }
495
+ const artifact = getReleaseNpmArtifact(journalArg, packageNameArg);
493
496
  return waitArg
494
497
  ? waitForAnonymousNpmArtifact(
495
498
  registryArg,
496
- journalArg.artifact,
499
+ artifact,
497
500
  journalArg.npm.tag,
498
501
  optionsArg.npmProbeOptions,
499
502
  )
500
503
  : probeAnonymousNpmArtifact(
501
504
  registryArg,
502
- journalArg.artifact,
505
+ artifact,
503
506
  journalArg.npm.tag,
504
507
  optionsArg.npmProbeOptions,
505
508
  );
@@ -527,14 +530,22 @@ const executeNpmRegistry = async (
527
530
  optionsArg: IReleasePublicationOptions,
528
531
  journalArg: TReleaseJournal,
529
532
  registryArg: string,
533
+ packageNameArg?: string,
530
534
  ): Promise<TReleaseJournal> => {
531
535
  const selector: TReleaseTargetSelector = {
532
536
  kind: "npm",
533
537
  registry: registryArg,
538
+ packageName: packageNameArg,
534
539
  };
535
540
  let journal = journalArg;
536
541
  let status = getTargetStatus(journal, selector);
537
- let probe = await probeNpm(journal, registryArg, optionsArg);
542
+ let probe = await probeNpm(
543
+ journal,
544
+ registryArg,
545
+ optionsArg,
546
+ false,
547
+ packageNameArg,
548
+ );
538
549
 
539
550
  if (status.state === "verified") {
540
551
  if (probe.status === "exact") {
@@ -596,7 +607,13 @@ const executeNpmRegistry = async (
596
607
  throw new Error(`npm registry target is in conflict: ${registryArg}`);
597
608
  }
598
609
 
599
- probe = await probeNpm(journal, registryArg, optionsArg);
610
+ probe = await probeNpm(
611
+ journal,
612
+ registryArg,
613
+ optionsArg,
614
+ false,
615
+ packageNameArg,
616
+ );
600
617
  if (probe.status === "exact") {
601
618
  if (status.attempts === 0 && journal.npm.alreadyPublished === "error") {
602
619
  await markUnclaimedTarget(
@@ -629,13 +646,12 @@ const executeNpmRegistry = async (
629
646
  throw new Error(`npm registry cannot be verified safely: ${registryArg}`);
630
647
  }
631
648
 
649
+ const artifact = getReleaseNpmArtifact(journal, packageNameArg);
632
650
  const artifactPath = optionsArg.store.getArtifactPath(
633
651
  journal.release.version,
652
+ artifact.file,
634
653
  );
635
- if (!journal.artifact) {
636
- throw new Error("npm release journal has no artifact.");
637
- }
638
- await verifyStoredNpmArtifact(artifactPath, journal.artifact);
654
+ await verifyStoredNpmArtifact(artifactPath, artifact);
639
655
  journal = await claimTarget(optionsArg.store, journal, selector);
640
656
  const publishResult = await publishNpmArtifact(
641
657
  optionsArg.smartshell,
@@ -643,7 +659,13 @@ const executeNpmRegistry = async (
643
659
  artifactPath,
644
660
  registryArg,
645
661
  );
646
- probe = await probeNpm(journal, registryArg, optionsArg, true);
662
+ probe = await probeNpm(
663
+ journal,
664
+ registryArg,
665
+ optionsArg,
666
+ true,
667
+ packageNameArg,
668
+ );
647
669
  if (probe.status === "exact") {
648
670
  return finishPublishingTarget(
649
671
  optionsArg.store,
@@ -1346,6 +1368,13 @@ export const executeReleasePublication = async (
1346
1368
  optionsArg: IReleasePublicationOptions,
1347
1369
  ): Promise<TReleaseJournal> => {
1348
1370
  let journal = optionsArg.journal;
1371
+ // Validate the whole stored set before Git or any registry can change.
1372
+ for (const artifact of getReleaseArtifacts(journal)) {
1373
+ await verifyStoredNpmArtifact(
1374
+ optionsArg.store.getArtifactPath(journal.release.version, artifact.file),
1375
+ artifact,
1376
+ );
1377
+ }
1349
1378
  if (journal.schemaVersion === 2) {
1350
1379
  journal = await executeDockerQualification(optionsArg, journal);
1351
1380
  if (
@@ -1362,7 +1391,12 @@ export const executeReleasePublication = async (
1362
1391
  : journal.git.expectedRemoteMainOid || undefined;
1363
1392
  for (const registry of journal.npm.registries) {
1364
1393
  await optionsArg.ensurePublicationState(expectedRemoteMainOid);
1365
- journal = await executeNpmRegistry(optionsArg, journal, registry.registry);
1394
+ journal = await executeNpmRegistry(
1395
+ optionsArg,
1396
+ journal,
1397
+ registry.registry,
1398
+ registry.packageName,
1399
+ );
1366
1400
  }
1367
1401
  if (
1368
1402
  journal.schemaVersion === 2 &&
@@ -34,6 +34,9 @@ import {
34
34
  normalizeReleaseGitRemoteName,
35
35
  normalizeReleaseVersion,
36
36
  resolveGitCommonDirectory,
37
+ getReleaseArtifacts,
38
+ releasePackageArtifactFile,
39
+ type IReleasePackage,
37
40
  type IReleaseArtifact,
38
41
  type TReleaseJournal,
39
42
  type IReleaseJournalV1,
@@ -115,6 +118,15 @@ const runInternal = async (argvArg: any): Promise<void> => {
115
118
  planMode: workflow.confirmation === "plan",
116
119
  });
117
120
  printReleasePlan(workflow, branchContext);
121
+ if (
122
+ workflow.targets.includes("npm") &&
123
+ workflow.npmPackageSource === "tspublish"
124
+ ) {
125
+ const packagePlan = await planReleasePackages(branchContext.sourceCwd);
126
+ console.log("npm packages (dependency order):");
127
+ for (const module of packagePlan.modules)
128
+ console.log(` ${module.name} (${module.folder})`);
129
+ }
118
130
  if (workflow.confirmation === "plan") {
119
131
  return;
120
132
  }
@@ -346,6 +358,14 @@ const assertReleaseConfiguration = (
346
358
  if (!workflowArg.targets.includes("npm")) {
347
359
  return;
348
360
  }
361
+ if (
362
+ workflowArg.npmPackageSource === "tspublish" &&
363
+ workflowArg.targets.includes("docker")
364
+ ) {
365
+ throw new Error(
366
+ "tspublish package releases support Git and npm targets; use a separate Docker release repository.",
367
+ );
368
+ }
349
369
  if (workflowArg.npmRegistries.length === 0) {
350
370
  throw new Error("The npm release target requires at least one registry.");
351
371
  }
@@ -410,7 +430,21 @@ const createDockerQualificationRequest = (
410
430
  version: versionArg,
411
431
  });
412
432
 
413
- const createReleaseJournal = async (
433
+ export const planReleasePackages = async (
434
+ cwdArg: string,
435
+ ): Promise<plugins.tspublish.ITsPublishPlan> => {
436
+ const plan = await new plugins.tspublish.TsPublish().plan(cwdArg);
437
+ for (const module of plan.modules) {
438
+ if (module.registries.length !== 1 || module.registries[0] !== "useBase") {
439
+ throw new Error(
440
+ `${module.name}: GitZone-managed packages must declare registries: ["useBase"]. Configure destinations in release.targets.npm.`,
441
+ );
442
+ }
443
+ }
444
+ return plan;
445
+ };
446
+
447
+ export const createReleaseJournal = async (
414
448
  optionsArg: ICreateReleaseJournalOptions,
415
449
  ): Promise<{ store: ReleaseJournalStore; journal: TReleaseJournal }> => {
416
450
  const gitCommonDirectory = await resolveGitCommonDirectory(
@@ -437,13 +471,49 @@ const createReleaseJournal = async (
437
471
  );
438
472
  try {
439
473
  let artifact: IReleaseArtifact | null = null;
474
+ const packages: IReleasePackage[] = [];
440
475
  if (optionsArg.workflow.targets.includes("npm")) {
441
- artifact = await packNpmArtifact(
442
- optionsArg.smartshell,
443
- optionsArg.releaseCwd,
444
- temporaryDirectory,
445
- optionsArg.version,
446
- );
476
+ if (optionsArg.workflow.npmPackageSource === "tspublish") {
477
+ const plan = await planReleasePackages(optionsArg.releaseCwd);
478
+ if (plan.version !== optionsArg.version)
479
+ throw new Error("tspublish plan version differs from the release.");
480
+ const outputDirectory = plugins.path.join(
481
+ temporaryDirectory,
482
+ "prepared",
483
+ );
484
+ const prepared = await new plugins.tspublish.TsPublish().prepare(
485
+ optionsArg.releaseCwd,
486
+ outputDirectory,
487
+ );
488
+ if (
489
+ JSON.stringify(prepared.map(({ directory, ...module }) => module)) !==
490
+ JSON.stringify(plan.modules)
491
+ ) {
492
+ throw new Error("tspublish package plan changed during preparation.");
493
+ }
494
+ for (const module of prepared) {
495
+ const packed = await packNpmArtifact(
496
+ optionsArg.smartshell,
497
+ module.directory,
498
+ temporaryDirectory,
499
+ optionsArg.version,
500
+ releasePackageArtifactFile(module.name),
501
+ );
502
+ if (packed.packageName !== module.name)
503
+ throw new Error(
504
+ "Prepared package identity changed during packing.",
505
+ );
506
+ packages.push({ folder: module.folder, artifact: packed });
507
+ }
508
+ await plugins.fs.rm(outputDirectory, { recursive: true, force: true });
509
+ } else {
510
+ artifact = await packNpmArtifact(
511
+ optionsArg.smartshell,
512
+ optionsArg.releaseCwd,
513
+ temporaryDirectory,
514
+ optionsArg.version,
515
+ );
516
+ }
447
517
  await verifyCleanTree(
448
518
  optionsArg.smartshell,
449
519
  optionsArg.releaseCwd,
@@ -464,7 +534,7 @@ const createReleaseJournal = async (
464
534
  const journal = finalizeJournalCompletion(
465
535
  {
466
536
  kind: "gitzone-release-journal",
467
- schemaVersion: optionsArg.dockerRequest ? 2 : 1,
537
+ schemaVersion: packages.length ? 3 : optionsArg.dockerRequest ? 2 : 1,
468
538
  revision: 1,
469
539
  release: {
470
540
  version: optionsArg.version,
@@ -473,6 +543,7 @@ const createReleaseJournal = async (
473
543
  tagOid: optionsArg.refs.tagOid,
474
544
  },
475
545
  artifact,
546
+ ...(packages.length ? { packages } : {}),
476
547
  git: {
477
548
  state: gitStatus.state,
478
549
  attempts: gitStatus.attempts,
@@ -492,18 +563,26 @@ const createReleaseJournal = async (
492
563
  alreadyPublished: optionsArg.workflow.targets.includes("npm")
493
564
  ? optionsArg.workflow.npmAlreadyPublished
494
565
  : "success",
495
- registries: optionsArg.workflow.targets.includes("npm")
496
- ? optionsArg.workflow.npmRegistries.map((registryArg) => {
497
- const status = createInitialTargetStatus(true);
498
- return {
499
- state: status.state,
500
- attempts: status.attempts,
501
- attempt: status.attempt,
502
- error: status.error,
503
- registry: registryArg,
504
- };
505
- })
506
- : [],
566
+ registries: packages.length
567
+ ? packages.flatMap((entry) =>
568
+ optionsArg.workflow.npmRegistries.map((registry) => ({
569
+ ...createInitialTargetStatus(true),
570
+ registry,
571
+ packageName: entry.artifact.packageName,
572
+ })),
573
+ )
574
+ : optionsArg.workflow.targets.includes("npm")
575
+ ? optionsArg.workflow.npmRegistries.map((registryArg) => {
576
+ const status = createInitialTargetStatus(true);
577
+ return {
578
+ state: status.state,
579
+ attempts: status.attempts,
580
+ attempt: status.attempt,
581
+ error: status.error,
582
+ registry: registryArg,
583
+ };
584
+ })
585
+ : [],
507
586
  },
508
587
  ...(optionsArg.dockerRequest
509
588
  ? {
@@ -675,13 +754,15 @@ const runReleaseResume = async (
675
754
  await tsdocker.assertCapabilities();
676
755
  await tsdocker.validate(journal.docker.request);
677
756
  }
678
- if (journal.artifact) {
757
+ if (getReleaseArtifacts(journal).length) {
679
758
  await assertPnpmReleaseCapability(smartshellInstance, paths.cwd);
680
759
  await assertNoLegacyNpmPublisher(paths.cwd);
681
- await verifyStoredNpmArtifact(
682
- store.getArtifactPath(version),
683
- journal.artifact,
684
- );
760
+ for (const artifact of getReleaseArtifacts(journal)) {
761
+ await verifyStoredNpmArtifact(
762
+ store.getArtifactPath(version, artifact.file),
763
+ artifact,
764
+ );
765
+ }
685
766
  }
686
767
 
687
768
  const branchContext = await inspectReleaseBranch({
@@ -773,17 +854,24 @@ const assertResumeConfiguration = async (
773
854
  if (
774
855
  journalArg.npm.registries.length > 0 &&
775
856
  (JSON.stringify(currentRegistries) !==
776
- JSON.stringify(
777
- journalArg.npm.registries.map((registryArg) => registryArg.registry),
778
- ) ||
857
+ JSON.stringify([
858
+ ...new Set(
859
+ journalArg.npm.registries.map((registryArg) => registryArg.registry),
860
+ ),
861
+ ]) ||
779
862
  workflowArg.npmAccessLevel !== journalArg.npm.access ||
863
+ workflowArg.npmPackageSource !==
864
+ (journalArg.schemaVersion === 3 ? "tspublish" : "root") ||
780
865
  workflowArg.npmAlreadyPublished !== journalArg.npm.alreadyPublished)
781
866
  ) {
782
867
  throw new Error(
783
868
  "Current npm release configuration does not match the journal.",
784
869
  );
785
870
  }
786
- if (journalArg.git.state !== "skipped" || journalArg.artifact) {
871
+ if (
872
+ journalArg.git.state !== "skipped" ||
873
+ getReleaseArtifacts(journalArg).length
874
+ ) {
787
875
  await assertNoLegacyNpmPublisher(cwdArg);
788
876
  }
789
877
  };
@@ -797,7 +885,15 @@ const printReleaseJournalSummary = (journalArg: TReleaseJournal): void => {
797
885
  );
798
886
  console.log(`git: ${journalArg.git.state}`);
799
887
  for (const registry of journalArg.npm.registries) {
800
- console.log(`npm ${registry.registry}: ${registry.state}`);
888
+ console.log(
889
+ `npm ${registry.packageName ? `${registry.packageName} ` : ""}${registry.registry}: ${registry.state}`,
890
+ );
891
+ }
892
+ if (journalArg.schemaVersion === 3) {
893
+ for (const { artifact } of journalArg.packages)
894
+ console.log(
895
+ `artifact ${artifact.packageName}: ${artifact.integrity} (${artifact.sha256})`,
896
+ );
801
897
  }
802
898
  if (journalArg.schemaVersion === 2) {
803
899
  console.log(
@@ -1143,6 +1239,7 @@ function printReleasePlan(
1143
1239
  `changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`,
1144
1240
  );
1145
1241
  if (workflow.targets.includes("npm")) {
1242
+ console.log(`npm package source: ${workflow.npmPackageSource}`);
1146
1243
  console.log(
1147
1244
  `npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`,
1148
1245
  );
package/ts/plugins.ts CHANGED
@@ -20,6 +20,7 @@ import * as smartdelay from '@push.rocks/smartdelay';
20
20
  import * as smartbucket from '@push.rocks/smartbucket';
21
21
  import * as s3 from '@aws-sdk/client-s3';
22
22
  import * as tsdockerReleaseProtocol from '@git.zone/tsdocker/dist_ts/classes.releaseprotocol.js';
23
+ import * as tspublish from '@git.zone/tspublish';
23
24
 
24
25
  export type {
25
26
  IReleaseCapabilitiesResult,
@@ -60,4 +61,5 @@ export {
60
61
  smartbucket,
61
62
  s3,
62
63
  tsdockerReleaseProtocol,
64
+ tspublish,
63
65
  };