@smoothbricks/cli 0.10.2 → 0.10.4

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 (133) hide show
  1. package/README.md +31 -23
  2. package/dist/bun/preload.d.ts +13 -0
  3. package/dist/bun/preload.d.ts.map +1 -0
  4. package/dist/bun/preload.js +12 -0
  5. package/dist/cli.js +36 -4
  6. package/dist/generate/index.d.ts.map +1 -1
  7. package/dist/generate/index.js +0 -6
  8. package/dist/github-ci/index.d.ts +17 -2
  9. package/dist/github-ci/index.d.ts.map +1 -1
  10. package/dist/github-ci/index.js +192 -26
  11. package/dist/github-ci/outputs.d.ts +22 -0
  12. package/dist/github-ci/outputs.d.ts.map +1 -0
  13. package/dist/github-ci/outputs.js +648 -0
  14. package/dist/lib/cli-package.js +2 -2
  15. package/dist/lib/json.d.ts +118 -11
  16. package/dist/lib/json.d.ts.map +1 -1
  17. package/dist/lib/json.js +317 -29
  18. package/dist/lib/workspace.d.ts +7 -14
  19. package/dist/lib/workspace.d.ts.map +1 -1
  20. package/dist/lib/workspace.js +76 -82
  21. package/dist/monorepo/ci-workflow.d.ts +2 -0
  22. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  23. package/dist/monorepo/ci-workflow.js +28 -2
  24. package/dist/monorepo/git-config.js +2 -2
  25. package/dist/monorepo/index.d.ts +3 -1
  26. package/dist/monorepo/index.d.ts.map +1 -1
  27. package/dist/monorepo/index.js +6 -2
  28. package/dist/monorepo/lockfile.d.ts +13 -1
  29. package/dist/monorepo/lockfile.d.ts.map +1 -1
  30. package/dist/monorepo/lockfile.js +33 -21
  31. package/dist/monorepo/managed-files.d.ts +12 -0
  32. package/dist/monorepo/managed-files.d.ts.map +1 -1
  33. package/dist/monorepo/managed-files.js +86 -28
  34. package/dist/monorepo/package-policy.d.ts.map +1 -1
  35. package/dist/monorepo/package-policy.js +78 -45
  36. package/dist/monorepo/packed-manifest.d.ts +5 -2
  37. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  38. package/dist/monorepo/packed-manifest.js +16 -47
  39. package/dist/monorepo/packed-package.d.ts.map +1 -1
  40. package/dist/monorepo/packed-package.js +28 -16
  41. package/dist/monorepo/packs/index.d.ts +3 -0
  42. package/dist/monorepo/packs/index.d.ts.map +1 -1
  43. package/dist/monorepo/packs/index.js +18 -6
  44. package/dist/monorepo/publish-workflow.d.ts +1 -0
  45. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  46. package/dist/monorepo/publish-workflow.js +242 -3
  47. package/dist/monorepo/runtime.d.ts +15 -0
  48. package/dist/monorepo/runtime.d.ts.map +1 -1
  49. package/dist/monorepo/runtime.js +66 -12
  50. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  51. package/dist/monorepo/tool-validation.js +32 -23
  52. package/dist/monorepo/wrangler.d.ts.map +1 -1
  53. package/dist/monorepo/wrangler.js +13 -13
  54. package/dist/nx/index.d.ts +12 -4
  55. package/dist/nx/index.d.ts.map +1 -1
  56. package/dist/nx/index.js +86 -19
  57. package/dist/pr/index.d.ts.map +1 -1
  58. package/dist/pr/index.js +28 -32
  59. package/dist/release/candidates.d.ts +3 -2
  60. package/dist/release/candidates.d.ts.map +1 -1
  61. package/dist/release/candidates.js +4 -5
  62. package/dist/release/github-release.d.ts +3 -0
  63. package/dist/release/github-release.d.ts.map +1 -1
  64. package/dist/release/github-release.js +11 -0
  65. package/dist/release/index.d.ts +20 -1
  66. package/dist/release/index.d.ts.map +1 -1
  67. package/dist/release/index.js +221 -89
  68. package/dist/release/orchestration.d.ts +8 -1
  69. package/dist/release/orchestration.d.ts.map +1 -1
  70. package/dist/release/orchestration.js +38 -1
  71. package/dist/wrangler/prepare-env.d.ts +0 -1
  72. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  73. package/dist/wrangler/prepare-env.js +100 -51
  74. package/dist/wrangler/scaffold.d.ts.map +1 -1
  75. package/dist/wrangler/scaffold.js +12 -12
  76. package/managed/raw/git-format-staged.yml +19 -1
  77. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
  78. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  79. package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
  80. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  81. package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
  82. package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
  83. package/managed/templates/github/actions/cache-nx/action.yml +2 -2
  84. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  85. package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
  86. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  87. package/managed/templates/github/workflows/managed-files.yml +80 -0
  88. package/package.json +18 -4
  89. package/src/bun/preload.ts +12 -0
  90. package/src/cli.ts +49 -11
  91. package/src/generate/index.ts +0 -6
  92. package/src/github-ci/index.test.ts +486 -0
  93. package/src/github-ci/index.ts +220 -30
  94. package/src/github-ci/outputs.ts +505 -0
  95. package/src/lib/cli-package.ts +2 -2
  96. package/src/lib/json.ts +291 -32
  97. package/src/lib/workspace.ts +88 -100
  98. package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
  99. package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
  100. package/src/monorepo/ci-workflow.ts +28 -2
  101. package/src/monorepo/git-config.ts +2 -2
  102. package/src/monorepo/index.ts +6 -2
  103. package/src/monorepo/lockfile.test.ts +35 -36
  104. package/src/monorepo/lockfile.ts +48 -24
  105. package/src/monorepo/managed-files.test.ts +63 -0
  106. package/src/monorepo/managed-files.ts +94 -34
  107. package/src/monorepo/package-policy.test.ts +70 -51
  108. package/src/monorepo/package-policy.ts +104 -59
  109. package/src/monorepo/packed-manifest.ts +18 -52
  110. package/src/monorepo/packed-package.ts +43 -22
  111. package/src/monorepo/packs/index.test.ts +18 -1
  112. package/src/monorepo/packs/index.ts +20 -7
  113. package/src/monorepo/publish-workflow.ts +482 -3
  114. package/src/monorepo/runtime.test.ts +42 -3
  115. package/src/monorepo/runtime.ts +90 -12
  116. package/src/monorepo/tool-validation.ts +35 -25
  117. package/src/monorepo/wrangler.test.ts +11 -15
  118. package/src/monorepo/wrangler.ts +15 -15
  119. package/src/nx/index.test.ts +43 -0
  120. package/src/nx/index.ts +110 -23
  121. package/src/pr/index.ts +42 -36
  122. package/src/release/__tests__/candidates.test.ts +5 -4
  123. package/src/release/__tests__/fixture-repo.test.ts +18 -16
  124. package/src/release/__tests__/github-release.test.ts +11 -0
  125. package/src/release/__tests__/orchestration.test.ts +57 -2
  126. package/src/release/__tests__/trust-publisher.test.ts +98 -2
  127. package/src/release/candidates.ts +10 -17
  128. package/src/release/github-release.ts +12 -0
  129. package/src/release/index.ts +321 -112
  130. package/src/release/orchestration.ts +55 -3
  131. package/src/wrangler/prepare-env.ts +75 -54
  132. package/src/wrangler/scaffold.test.ts +9 -12
  133. package/src/wrangler/scaffold.ts +13 -13
@@ -1,13 +1,24 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
1
2
  import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
3
  import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
+ import { join, resolve } from 'node:path';
4
5
  import { createInterface } from 'node:readline/promises';
5
6
  import { Writable } from 'node:stream';
6
7
  import { $ } from 'bun';
8
+ import typia from 'typia';
9
+ import { githubCiApplyOutputs, githubCiNxRunMany } from '../github-ci/index.js';
7
10
  import { assertNoConflictMarkers } from '../lib/conflict-markers.js';
8
11
  import { withDevenvEnv } from '../lib/devenv.js';
9
- import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
10
- import { decode, run, runInteractiveStatus, runResult, runStatus } from '../lib/run.js';
12
+ import {
13
+ type NxJson,
14
+ type NxProjectJson,
15
+ type NxTargetConfig,
16
+ type PackageJson,
17
+ parseNxJsonText,
18
+ parseNxProjectJsonText,
19
+ parsePackageJsonText,
20
+ } from '../lib/json.js';
21
+ import { decode, run, runInteractiveStatus, runResult } from '../lib/run.js';
11
22
  import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
12
23
  import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
13
24
  import {
@@ -29,14 +40,18 @@ import {
29
40
  } from './core.js';
30
41
  import {
31
42
  createOrUpdateGithubRelease,
43
+ githubReleaseLookupExists,
32
44
  renderNxProjectChangelogContents,
33
45
  withNxWorkspaceRoot,
34
46
  } from './github-release.js';
35
47
  import { publishWithAuthDiagnostics } from './npm-auth.js';
36
48
  import {
37
49
  bumpStableReleaseToNext,
50
+ collectRepairPlatformOutputs,
38
51
  completeReleaseAtHead as completeReleaseAtHeadWithShell,
39
52
  type ReleaseCompletionShell,
53
+ type ReleaseRepairOutputsShell,
54
+ type ReleaseRepairShell,
40
55
  type ReleaseSummary,
41
56
  type ReleaseVersionMode,
42
57
  repairPendingTargets,
@@ -57,6 +72,15 @@ export interface ReleasePublishOptions {
57
72
 
58
73
  export interface ReleaseRepairPendingOptions {
59
74
  dryRun?: boolean;
75
+ platformOutputs?: string;
76
+ ref?: string;
77
+ }
78
+
79
+ export interface ReleasePlatformOutputsOptions {
80
+ bump: string;
81
+ output: string;
82
+ ref?: string;
83
+ targets: string;
60
84
  }
61
85
 
62
86
  export interface ReleaseTrustPublisherOptions {
@@ -162,26 +186,56 @@ export async function releasePublish(root: string, options: ReleasePublishOption
162
186
  }
163
187
 
164
188
  export async function releaseRepairPending(root: string, options: ReleaseRepairPendingOptions): Promise<void> {
165
- const branch = await releaseBranch(root);
166
- const remote = await releaseRemote(root, branch);
167
- console.log(`Repair pending releases: fetching ${remote}/${branch} and tags.`);
168
- await fetchReleaseRefs(root, remote, branch);
169
- const remoteRef = `${remote}/${branch}`;
170
- const restoreRef = (await gitRefExists(root, remoteRef)) ? remoteRef : await gitHead(root);
171
- console.log(`Repair pending releases: planning from ${restoreRef}.`);
172
- const targets = await listPendingReleaseTargets(root, restoreRef);
173
- console.log(
174
- targets.length === 0
175
- ? 'Repair pending releases: no pending durable state repairs found.'
176
- : `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
189
+ const repairRef = await fetchReleaseRepairRef(root, options.ref);
190
+ const restoreRef = options.ref ? await gitHead(root) : repairRef;
191
+ console.log(`Repair pending releases: planning from ${repairRef}.`);
192
+ const targets = await listPendingReleaseTargets(root, repairRef);
193
+ logPendingReleaseTargets(targets);
194
+ const platformOutputs = options.platformOutputs ? resolve(root, options.platformOutputs) : undefined;
195
+ const summaries = await repairPendingTargets(
196
+ releaseRepairShell(root, platformOutputs),
197
+ targets,
198
+ restoreRef,
199
+ options.dryRun === true,
177
200
  );
178
- for (const target of targets) {
179
- console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
180
- }
181
- const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
182
201
  await writeRepairSummary(summaries, options.dryRun === true);
183
202
  }
184
203
 
204
+ export async function releaseCollectPlatformOutputs(
205
+ root: string,
206
+ options: ReleasePlatformOutputsOptions,
207
+ ): Promise<void> {
208
+ const restoreRef = await gitHead(root);
209
+ const releaseRef = await fetchReleaseRepairRef(root, options.ref);
210
+ const packages = releasePackages(root);
211
+ const packagesAtHead = await releasePackagesAtHead(root, packages);
212
+ const currentPackages =
213
+ packagesAtHead.length > 0
214
+ ? packagesAtHead
215
+ : await releasePlatformPackages(root, packages, releaseBumpArg(options.bump));
216
+ console.log(
217
+ currentPackages.length === 0
218
+ ? 'Release platform outputs: no current release packages selected.'
219
+ : `Release platform outputs: building current outputs for ${packageSummary(currentPackages)}.`,
220
+ );
221
+ const outputRoot = resolve(root, options.output);
222
+ await githubCiNxRunMany(root, {
223
+ targets: options.targets,
224
+ projects: releasePackageProjects(currentPackages),
225
+ collectOutputs: join(outputRoot, 'current'),
226
+ allowEmptyProjects: true,
227
+ });
228
+
229
+ console.log(`Repair platform outputs: planning from ${releaseRef}.`);
230
+ const targets = await listPendingReleaseTargets(root, releaseRef);
231
+ logPendingReleaseTargets(targets);
232
+ await collectRepairPlatformOutputs(
233
+ releaseRepairOutputsShell(root, options.targets, join(outputRoot, 'repairs')),
234
+ targets,
235
+ restoreRef,
236
+ );
237
+ }
238
+
185
239
  export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
186
240
  const repository = githubRepositoryFromRootPackage(root);
187
241
  const workflow = 'publish.yml';
@@ -203,14 +257,10 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
203
257
  },
204
258
  bootstrapOptions,
205
259
  ),
206
- trustPublisher: (pkg, dryRun, env) => {
207
- const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
208
- if (dryRun) {
209
- args.push('--dry-run');
210
- }
211
- return runLatestNpmTrust(root, args, env);
212
- },
260
+ trustPublisher: (pkg, dryRun, env) =>
261
+ runLatestNpmTrust(root, npmTrustGithubArgs(pkg.name, repository, workflow, dryRun), env),
213
262
  trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
263
+ login: () => runLatestNpm(root, ['login', '--auth-type=web']),
214
264
  log: (message) => console.log(message),
215
265
  error: (message) => console.error(message),
216
266
  },
@@ -218,6 +268,19 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
218
268
  );
219
269
  }
220
270
 
271
+ export function npmTrustGithubArgs(
272
+ packageName: string,
273
+ repository: string,
274
+ workflow: string,
275
+ dryRun: boolean,
276
+ ): string[] {
277
+ const args = ['trust', 'github', packageName, '--file', workflow, '--repo', repository, '--allow-publish', '--yes'];
278
+ if (dryRun) {
279
+ args.push('--dry-run');
280
+ }
281
+ return args;
282
+ }
283
+
221
284
  export interface TrustPublisherShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
222
285
  repository: string;
223
286
  workflow: string;
@@ -225,7 +288,8 @@ export interface TrustPublisherShell<Package extends ReleasePackageInfo = Releas
225
288
  packageExists(name: string): Promise<boolean>;
226
289
  bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
227
290
  trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
228
- trustedPublishers(pkg: Package): Promise<TrustedPublisher[]>;
291
+ trustedPublishers(pkg: Package): Promise<TrustedPublisherLookup>;
292
+ login(): Promise<void>;
229
293
  log(message: string): void;
230
294
  error(message: string): void;
231
295
  }
@@ -239,6 +303,14 @@ export interface TrustedPublisher {
239
303
  repository?: string;
240
304
  }
241
305
 
306
+ export interface NpmTrustAccessDenied {
307
+ readonly status: 'access-denied';
308
+ readonly identity: string;
309
+ readonly owners: string;
310
+ }
311
+
312
+ export type TrustedPublisherLookup = TrustedPublisher[] | NpmTrustAccessDenied;
313
+
242
314
  export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
243
315
  shell: TrustPublisherShell<Package>,
244
316
  options: ReleaseTrustPublisherOptions,
@@ -275,7 +347,7 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
275
347
  const failedPackages: string[] = [];
276
348
  for (const pkg of selectedPackages) {
277
349
  if (!options.dryRun) {
278
- const trustedPublishers = await shell.trustedPublishers(pkg);
350
+ const trustedPublishers = await trustedPublishersWithOwnerLogin(shell, pkg);
279
351
  if (hasMatchingGithubTrustedPublisher(trustedPublishers, shell.repository, shell.workflow)) {
280
352
  shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
281
353
  continue;
@@ -315,6 +387,33 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
315
387
  }
316
388
  }
317
389
 
390
+ async function trustedPublishersWithOwnerLogin<Package extends ReleasePackageInfo>(
391
+ shell: TrustPublisherShell<Package>,
392
+ pkg: Package,
393
+ ): Promise<TrustedPublisher[]> {
394
+ let result = await shell.trustedPublishers(pkg);
395
+ if (Array.isArray(result)) {
396
+ return result;
397
+ }
398
+
399
+ shell.error(
400
+ `${pkg.name}: npm trust access denied for npm account "${result.identity}". ` +
401
+ `npm trust requires package write access. Package owners:\n${result.owners}`,
402
+ );
403
+ shell.log(`${pkg.name}: opening npm browser login. Sign in as a listed package owner, then return here.`);
404
+ await shell.login();
405
+
406
+ result = await shell.trustedPublishers(pkg);
407
+ if (Array.isArray(result)) {
408
+ return result;
409
+ }
410
+ throw new Error(
411
+ `${pkg.name}: npm trust access denied for npm account "${result.identity}". ` +
412
+ `npm trust requires package write access. Package owners:\n${result.owners}\n` +
413
+ 'npm browser login completed, but the selected account still cannot manage this package.',
414
+ );
415
+ }
416
+
318
417
  function selectedTrustPublisherPackages<Package extends ReleasePackageInfo>(
319
418
  packages: Package[],
320
419
  selections: string[],
@@ -483,7 +582,7 @@ async function publishPlaceholderPackage(
483
582
  name: pkg.name,
484
583
  version: NPM_BOOTSTRAP_VERSION,
485
584
  description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
486
- license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
585
+ license: pkg.json.license ?? 'UNLICENSED',
487
586
  repository: pkg.json.repository,
488
587
  publishConfig: { access: 'public' },
489
588
  },
@@ -507,7 +606,7 @@ function safeTarballPrefix(name: string): string {
507
606
 
508
607
  async function assertPackedWorkspaceDependencies(root: string, tarball: string, pkg: ReleasePackage): Promise<void> {
509
608
  const manifest = await readPackedPackageJson(root, tarball, pkg.name);
510
- const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
609
+ const failures = validatePackedWorkspaceDependencies(root, pkg, manifest, { mode: 'publish' });
511
610
  if (failures.length > 0) {
512
611
  throw new Error(failures.join('\n'));
513
612
  }
@@ -548,6 +647,19 @@ async function releaseVersionPackages(
548
647
  );
549
648
  }
550
649
 
650
+ async function releasePlatformPackages(
651
+ root: string,
652
+ packages: ReleasePackage[],
653
+ bump: string,
654
+ ): Promise<ReleasePackage[]> {
655
+ const candidates = await releaseVersionPackages(root, packages, bump);
656
+ if (candidates.length === 0) {
657
+ return [];
658
+ }
659
+ // Nx may version additional reverse dependents, so use its dry-run result rather than only the direct candidates.
660
+ return runNxReleaseVersionPreview(root, releasePackageProjects(candidates), bump);
661
+ }
662
+
551
663
  async function runNxReleaseVersion(
552
664
  root: string,
553
665
  projects: string,
@@ -603,6 +715,7 @@ async function runNxReleaseVersionPreview(root: string, projects: string, bump:
603
715
  gitPush: false,
604
716
  stageChanges: false,
605
717
  createRelease: false,
718
+ forceChangelogGeneration: true,
606
719
  dryRun: true,
607
720
  });
608
721
  releasePreviewChangelogs.clear();
@@ -680,8 +793,7 @@ async function packageVersionAtRef(root: string, packagePath: string, ref: strin
680
793
  return null;
681
794
  }
682
795
  try {
683
- const parsed = JSON.parse(decode(result.stdout));
684
- return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
796
+ return parsePackageJsonText(decode(result.stdout))?.version ?? null;
685
797
  } catch {
686
798
  return null;
687
799
  }
@@ -808,17 +920,11 @@ function releaseNextShell(root: string): { bumpStablePackagesToNext(packages: Re
808
920
  };
809
921
  }
810
922
 
811
- function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage> & {
812
- checkout(ref: string): Promise<void>;
813
- withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
814
- beforeRepairTarget(target: ReleaseTarget): void;
815
- afterRepairTarget(target: ReleaseTarget): void;
816
- } {
923
+ function releaseTargetCheckoutShell(root: string) {
817
924
  return {
818
- ...releaseCompletionShell(root),
819
- checkout: (ref) => run('git', ['switch', '--detach', ref], root),
820
- withDevenvEnv: (runWithEnv) => withDevenvEnv(root, runWithEnv),
821
- beforeRepairTarget: (target) => {
925
+ checkout: (ref: string) => run('git', ['switch', '--detach', ref], root),
926
+ withDevenvEnv: <T>(runWithEnv: () => Promise<T>) => withDevenvEnv(root, runWithEnv),
927
+ beforeRepairTarget: (target: ReleaseTarget) => {
822
928
  console.log(`::group::Repair pending release ${target.sha.slice(0, 12)} (${packageSummary(target.packages)})`);
823
929
  },
824
930
  afterRepairTarget: () => {
@@ -827,6 +933,37 @@ function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage
827
933
  };
828
934
  }
829
935
 
936
+ function releaseRepairShell(root: string, platformOutputs?: string): ReleaseRepairShell<ReleasePackage> {
937
+ return {
938
+ ...releaseCompletionShell(root),
939
+ ...releaseTargetCheckoutShell(root),
940
+ prepareRepairTarget: async (target) => {
941
+ if (!platformOutputs || target.npmPackages.length === 0) {
942
+ return;
943
+ }
944
+ const output = join(platformOutputs, target.sha);
945
+ console.log(`Repair pending releases: applying cross-platform outputs from ${output}.`);
946
+ await githubCiApplyOutputs(root, [output], target.sha);
947
+ },
948
+ };
949
+ }
950
+
951
+ function releaseRepairOutputsShell(
952
+ root: string,
953
+ targetGlobs: string,
954
+ outputRoot: string,
955
+ ): ReleaseRepairOutputsShell<ReleasePackage> {
956
+ return {
957
+ ...releaseTargetCheckoutShell(root),
958
+ collectRepairTargetOutputs: (target) =>
959
+ githubCiNxRunMany(root, {
960
+ targets: targetGlobs,
961
+ projects: releasePackageProjects(target.npmPackages),
962
+ collectOutputs: join(outputRoot, target.sha),
963
+ }),
964
+ };
965
+ }
966
+
830
967
  function releaseRetagShell(root: string, remote: string) {
831
968
  return {
832
969
  listReleasePackages: () => releasePackages(root),
@@ -844,6 +981,29 @@ function releaseRetagShell(root: string, remote: string) {
844
981
  };
845
982
  }
846
983
 
984
+ async function fetchReleaseRepairRef(root: string, requestedRef?: string): Promise<string> {
985
+ const branch = await releaseBranch(root);
986
+ const remote = await releaseRemote(root, branch);
987
+ console.log(`Repair pending releases: fetching ${remote}/${branch} and tags.`);
988
+ await fetchReleaseRefs(root, remote, branch);
989
+ if (requestedRef) {
990
+ return requestedRef;
991
+ }
992
+ const remoteRef = `${remote}/${branch}`;
993
+ return (await gitRefExists(root, remoteRef)) ? remoteRef : gitHead(root);
994
+ }
995
+
996
+ function logPendingReleaseTargets(targets: ReleaseTarget[]): void {
997
+ console.log(
998
+ targets.length === 0
999
+ ? 'Repair pending releases: no pending durable state repairs found.'
1000
+ : `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
1001
+ );
1002
+ for (const target of targets) {
1003
+ console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
1004
+ }
1005
+ }
1006
+
847
1007
  async function listPendingReleaseTargets(root: string, ref: string): Promise<ReleaseTarget[]> {
848
1008
  const head = await gitHead(root);
849
1009
  return pendingReleaseTargets(await listOwnedReleaseTagRecords(root, ref), head);
@@ -1069,7 +1229,8 @@ function repairReasons(target: ReleaseTarget, pkg: ReleasePackage): string[] {
1069
1229
  }
1070
1230
 
1071
1231
  async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
1072
- await run('git', ['fetch', '--tags', remote, branch], root);
1232
+ // `retag-unpublished` intentionally moves repairable tags, so the remote tag is authoritative.
1233
+ await run('git', ['fetch', '--force', '--tags', remote, branch], root);
1073
1234
  }
1074
1235
 
1075
1236
  async function gitRefExists(root: string, ref: string): Promise<boolean> {
@@ -1112,74 +1273,83 @@ async function packageChangedFilesSince(root: string, ref: string, packagePath:
1112
1273
  .map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
1113
1274
  }
1114
1275
 
1115
- async function packageJsonAtRef(
1116
- root: string,
1117
- ref: string,
1118
- packagePath: string,
1119
- ): Promise<Record<string, unknown> | null> {
1276
+ async function packageJsonAtRef(root: string, ref: string, packagePath: string): Promise<PackageJson | null> {
1120
1277
  const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
1121
1278
  if (result.exitCode !== 0) {
1122
1279
  return null;
1123
1280
  }
1124
1281
  try {
1125
- const parsed = JSON.parse(decode(result.stdout));
1126
- return isRecord(parsed) ? parsed : null;
1282
+ return parsePackageJsonText(decode(result.stdout));
1127
1283
  } catch {
1128
1284
  return null;
1129
1285
  }
1130
1286
  }
1131
1287
 
1132
- async function currentPackageJson(root: string, packagePath: string): Promise<Record<string, unknown> | null> {
1288
+ async function currentPackageJson(root: string, packagePath: string): Promise<PackageJson | null> {
1133
1289
  return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
1134
1290
  }
1135
1291
 
1136
1292
  async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
1137
1293
  const project = await nxProjectJson(root, projectName);
1138
- const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
1139
- return resolveBuildInputPatterns(project, nxJson);
1294
+ return resolveBuildInputPatterns(project, readNxJson(join(root, 'nx.json')));
1140
1295
  }
1141
1296
 
1142
- async function nxProjectJson(root: string, projectName: string): Promise<Record<string, unknown>> {
1297
+ async function nxProjectJson(root: string, projectName: string): Promise<NxProjectJson> {
1143
1298
  const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
1144
- const parsed = JSON.parse(decode(result.stdout));
1145
- if (!isRecord(parsed)) {
1299
+ const parsed = parseNxProjectJsonText(decode(result.stdout));
1300
+ if (!parsed) {
1146
1301
  throw new Error(`Unable to inspect Nx project ${projectName}.`);
1147
1302
  }
1148
1303
  return parsed;
1149
1304
  }
1150
1305
 
1151
- function resolveBuildInputPatterns(project: Record<string, unknown>, nxJson: Record<string, unknown>): string[] {
1152
- const targets = recordProperty(project, 'targets');
1306
+ function readNxJson(path: string): NxJson {
1307
+ if (!existsSync(path)) {
1308
+ return {};
1309
+ }
1310
+ try {
1311
+ return parseNxJsonText(readFileSync(path, 'utf8')) ?? {};
1312
+ } catch {
1313
+ return {};
1314
+ }
1315
+ }
1316
+
1317
+ function resolveBuildInputPatterns(project: NxProjectJson, nxJson: NxJson): string[] {
1318
+ const targets = project.targets;
1153
1319
  if (!targets) {
1154
1320
  return [];
1155
1321
  }
1156
1322
  return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
1157
1323
  }
1158
1324
 
1159
- function collectBuildInputs(targets: Record<string, unknown>): string[] {
1160
- const build = recordProperty(targets, 'build');
1325
+ function collectBuildInputs(targets: Record<string, NxTargetConfig>): string[] {
1326
+ const build = targets.build;
1161
1327
  if (!build) {
1162
1328
  return ['production'];
1163
1329
  }
1164
- const directInputs = stringArrayProperty(build, 'inputs');
1330
+ const directInputs = stringInputs(build.inputs);
1165
1331
  if (directInputs.length > 0) {
1166
1332
  return directInputs;
1167
1333
  }
1168
1334
  const inputs: string[] = [];
1169
- for (const dependency of stringArrayProperty(build, 'dependsOn')) {
1170
- if (dependency.startsWith('^')) {
1335
+ for (const dependency of build.dependsOn ?? []) {
1336
+ if (typeof dependency !== 'string' || dependency.startsWith('^')) {
1171
1337
  continue;
1172
1338
  }
1173
1339
  const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
1174
1340
  if (!targetName) {
1175
1341
  continue;
1176
1342
  }
1177
- inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
1343
+ inputs.push(...stringInputs(targets[targetName]?.inputs));
1178
1344
  }
1179
1345
  return inputs.length > 0 ? inputs : ['production'];
1180
1346
  }
1181
1347
 
1182
- function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown>): string[] {
1348
+ function stringInputs(inputs: NxTargetConfig['inputs'] | undefined): string[] {
1349
+ return Array.isArray(inputs) ? inputs.filter((entry): entry is string => typeof entry === 'string') : [];
1350
+ }
1351
+
1352
+ function normalizeInputPatterns(inputs: string[], nxJson: NxJson): string[] {
1183
1353
  const patterns: string[] = [];
1184
1354
  const seen = new Set<string>();
1185
1355
  for (const input of inputs) {
@@ -1190,14 +1360,13 @@ function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown
1190
1360
  return patterns;
1191
1361
  }
1192
1362
 
1193
- function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen: Set<string>): string[] {
1363
+ function expandInputPattern(input: string, nxJson: NxJson, seen: Set<string>): string[] {
1194
1364
  if (seen.has(input)) {
1195
1365
  return [];
1196
1366
  }
1197
1367
  seen.add(input);
1198
1368
  if (!input.includes('{')) {
1199
- const namedInputs = recordProperty(nxJson, 'namedInputs');
1200
- const namedInput = namedInputs?.[input];
1369
+ const namedInput = nxJson.namedInputs?.[input];
1201
1370
  if (Array.isArray(namedInput)) {
1202
1371
  return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
1203
1372
  }
@@ -1212,19 +1381,6 @@ function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen
1212
1381
  return [`${excluded ? '!' : ''}${packageRelative}`];
1213
1382
  }
1214
1383
 
1215
- function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
1216
- if (!record) {
1217
- return null;
1218
- }
1219
- const value = record[key];
1220
- return isRecord(value) ? value : null;
1221
- }
1222
-
1223
- function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
1224
- const value = record?.[key];
1225
- return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
1226
- }
1227
-
1228
1384
  async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
1229
1385
  const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
1230
1386
  if (result.exitCode !== 0) {
@@ -1309,7 +1465,8 @@ async function pushRetaggedReleaseTags(
1309
1465
  }
1310
1466
 
1311
1467
  async function githubReleaseExists(root: string, tag: string): Promise<boolean> {
1312
- return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
1468
+ const result = await $`gh release view ${tag} --json tagName`.cwd(root).quiet().nothrow();
1469
+ return githubReleaseLookupExists(tag, result.exitCode, decode(result.stdout), decode(result.stderr));
1313
1470
  }
1314
1471
 
1315
1472
  function githubReleaseUrl(root: string, tag: string): string {
@@ -1462,61 +1619,113 @@ async function runLatestNpmTrust(
1462
1619
  if (status === 0) {
1463
1620
  return 'configured';
1464
1621
  }
1465
- throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
1622
+ throw new Error(npmCommandFailedMessage(npmArgs, status));
1466
1623
  }
1467
1624
 
1468
- async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisher[]> {
1625
+ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisherLookup> {
1469
1626
  const args = ['trust', 'list', pkg.name, '--json'];
1470
1627
  let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1471
1628
  if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
1472
1629
  const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1473
1630
  if (status !== 0) {
1474
- throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
1631
+ throw new Error(npmCommandFailedMessage(args, status));
1475
1632
  }
1476
1633
  result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1477
1634
  }
1635
+ if (result.exitCode !== 0 && npmTrustListAccessDenied(result.stdout, result.stderr)) {
1636
+ return npmTrustAccessDetails(root, pkg.name);
1637
+ }
1478
1638
  if (result.exitCode !== 0) {
1479
- throw new Error(
1480
- `nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
1481
- );
1639
+ throw new Error(npmCommandFailedMessage(args, result.exitCode, result.stdout, result.stderr));
1482
1640
  }
1483
1641
  return parseTrustedPublishers(result.stdout, pkg.name);
1484
1642
  }
1485
1643
 
1644
+ /** npm trust list/setup requires an authenticated package owner. */
1645
+ export function npmTrustListAccessDenied(stdout: string, stderr: string): boolean {
1646
+ const output = `${stdout}\n${stderr}`;
1647
+ // E403: authenticated but not an owner. E401 / login-required: no usable session.
1648
+ return (
1649
+ /\bE403\b/.test(output) ||
1650
+ /\bE401\b/.test(output) ||
1651
+ /You must be logged in to publish packages/i.test(output) ||
1652
+ /npm error 401 Unauthorized/i.test(output)
1653
+ );
1654
+ }
1655
+
1656
+ function npmCommandFailedMessage(npmArgs: string[], exitCode: number, stdout = '', stderr = ''): string {
1657
+ const command = `nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')}`;
1658
+ const detail = npmFailureDetail(stdout, stderr);
1659
+ return detail.length > 0
1660
+ ? `${command} failed with exit code ${exitCode}\n${detail}`
1661
+ : `${command} failed with exit code ${exitCode}`;
1662
+ }
1663
+
1664
+ function npmFailureDetail(stdout: string, stderr: string): string {
1665
+ const chunks = [stderr.trim(), stdout.trim()].filter((chunk) => chunk.length > 0);
1666
+ if (chunks.length === 0) {
1667
+ return '';
1668
+ }
1669
+ // Prefer the actionable npm error line over notice spam when present.
1670
+ const joined = chunks.join('\n');
1671
+ const errorLines = joined
1672
+ .split('\n')
1673
+ .map((line) => line.trimEnd())
1674
+ .filter((line) => /npm error|error code|401 Unauthorized|403 Forbidden|E401|E403|logged in/i.test(line));
1675
+ if (errorLines.length > 0) {
1676
+ return errorLines.slice(0, 8).join('\n');
1677
+ }
1678
+ // Fall back to a short tail of combined output.
1679
+ return joined.split('\n').slice(-12).join('\n');
1680
+ }
1681
+
1682
+ async function npmTrustAccessDetails(root: string, packageName: string): Promise<NpmTrustAccessDenied> {
1683
+ const [identityResult, ownersResult] = await Promise.all([
1684
+ runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', 'whoami'], root),
1685
+ runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', 'owner', 'ls', packageName], root),
1686
+ ]);
1687
+ return {
1688
+ status: 'access-denied',
1689
+ identity:
1690
+ identityResult.exitCode === 0 && identityResult.stdout.trim().length > 0
1691
+ ? identityResult.stdout.trim()
1692
+ : '(not authenticated)',
1693
+ owners:
1694
+ ownersResult.exitCode === 0 && ownersResult.stdout.trim().length > 0
1695
+ ? ownersResult.stdout.trim()
1696
+ : '(owner lookup unavailable)',
1697
+ };
1698
+ }
1699
+
1486
1700
  export function parseTrustedPublishers(stdout: string, packageName: string): TrustedPublisher[] {
1487
1701
  if (stdout.trim().length === 0) {
1488
1702
  return [];
1489
1703
  }
1490
- const parsed = JSON.parse(stdout) as unknown;
1704
+ // createIsParse returns null for non-matching shapes and bare `null`.
1705
+ const parsed = parseTrustedPublisherJson(stdout);
1491
1706
  if (parsed === null) {
1492
- return [];
1493
- }
1494
- if (Array.isArray(parsed)) {
1495
- return parsed.map((value) => parseTrustedPublisher(value, packageName));
1496
- }
1497
- if (isRecord(parsed)) {
1498
- return [parseTrustedPublisher(parsed, packageName)];
1707
+ if (stdout.trim() === 'null') {
1708
+ return [];
1709
+ }
1710
+ throw new Error(`${packageName}: npm trust list returned invalid trusted publisher JSON.`);
1499
1711
  }
1500
- throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
1712
+ return (Array.isArray(parsed) ? parsed : [parsed]).map((value) => ({
1713
+ id: value.id,
1714
+ type: value.type,
1715
+ file: value.file,
1716
+ repository: value.repository,
1717
+ }));
1501
1718
  }
1502
1719
 
1503
- function parseTrustedPublisher(value: unknown, packageName: string): TrustedPublisher {
1504
- if (!isRecord(value)) {
1505
- throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
1506
- }
1507
- const id = stringProperty(value, 'id');
1508
- const type = stringProperty(value, 'type');
1509
- if (!id || !type) {
1510
- throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
1511
- }
1512
- return {
1513
- id,
1514
- type,
1515
- file: stringProperty(value, 'file') ?? undefined,
1516
- repository: stringProperty(value, 'repository') ?? undefined,
1517
- };
1720
+ interface TrustedPublisherJson {
1721
+ id: string;
1722
+ type: string;
1723
+ file?: string;
1724
+ repository?: string;
1518
1725
  }
1519
1726
 
1727
+ const parseTrustedPublisherJson = typia.json.createIsParse<TrustedPublisherJson | TrustedPublisherJson[]>();
1728
+
1520
1729
  async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
1521
1730
  await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
1522
1731
  }