@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,7 +1,19 @@
1
1
  /* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
2
2
 
3
+ import synchronizedPrettier from '@prettier/sync';
4
+ import {
5
+ LINUX_PLATFORM_TARGET_GLOBS,
6
+ MACOS_PLATFORM_TARGET_GLOBS,
7
+ } from '@smoothbricks/nx-plugin/workspace-config-policy';
8
+ import type { Options as PrettierOptions } from 'prettier';
3
9
  import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
4
10
 
11
+ const PUBLISH_WORKFLOW_FORMAT_OPTIONS = Object.freeze({
12
+ parser: 'yaml',
13
+ printWidth: 120,
14
+ proseWrap: 'always',
15
+ } satisfies PrettierOptions);
16
+
5
17
  export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
6
18
  export type PublishWorkflowCondition = 'version-mode-not-none' | 'deploy-production' | 'failure' | 'always';
7
19
  export type PublishWorkflowNxTarget = 'build' | 'lint' | 'test';
@@ -42,6 +54,7 @@ export interface PublishWorkflowDefinitionOptions {
42
54
  deploy?: boolean;
43
55
  deployProvider?: 'cloudflare';
44
56
  repoName?: string;
57
+ platformTargetGlobs?: readonly string[];
45
58
  }
46
59
 
47
60
  export interface PublishWorkflowInputs {
@@ -256,8 +269,19 @@ function shouldRunStep(
256
269
  }
257
270
 
258
271
  export function renderPublishWorkflowYaml(options: PublishWorkflowDefinitionOptions = {}): string {
259
- const steps = definePublishWorkflow(options).steps;
260
- return `${renderPublishWorkflowHeader(options)}${renderPublishWorkflowSteps(steps, options)}`;
272
+ let workflow: string;
273
+ if (hasMacosPlatformTargets(options)) {
274
+ workflow = renderPlatformPublishWorkflowYaml(options);
275
+ } else if (hasLinuxPlatformTargets(options)) {
276
+ workflow = `${renderPublishWorkflowHeader(options)}${renderSingleJobPublishWorkflowSteps(
277
+ definePublishWorkflow(options).steps,
278
+ options,
279
+ )}`;
280
+ } else {
281
+ const steps = definePublishWorkflow(options).steps;
282
+ workflow = `${renderPublishWorkflowHeader(options)}${renderPublishWorkflowSteps(steps, options)}`;
283
+ }
284
+ return synchronizedPrettier.format(workflow, PUBLISH_WORKFLOW_FORMAT_OPTIONS);
261
285
  }
262
286
 
263
287
  function renderPublishWorkflowHeader(options: PublishWorkflowDefinitionOptions): string {
@@ -396,7 +420,9 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
396
420
  ' "$GITHUB_OUTPUT"',
397
421
  ];
398
422
  case PublishWorkflowStepKind.CheckManagedMonorepoFiles:
399
- return conditionalRunStep(step, 'smoo monorepo check');
423
+ // Drift is derived state with its own remediation PR; publishing only
424
+ // blocks on actual package issues (smoo monorepo validate).
425
+ return conditionalRunStep(step, 'smoo monorepo check --warn');
400
426
  case PublishWorkflowStepKind.Build:
401
427
  return conditionalRunStep(
402
428
  step,
@@ -475,6 +501,459 @@ function conditionalRunStep(step: PublishWorkflowStep, run: string): string[] {
475
501
  return [` - name: ${step.name}`, ` if: ${githubExpression(condition)}`, ` run: ${run}`];
476
502
  }
477
503
 
504
+ function renderSingleJobPublishWorkflowSteps(
505
+ steps: PublishWorkflowStep[],
506
+ options: PublishWorkflowDefinitionOptions,
507
+ ): string {
508
+ const lines: string[] = [];
509
+ for (const step of steps) {
510
+ lines.push(...sectionLinesBefore(step));
511
+ lines.push(...commentLinesForStep(step));
512
+ lines.push(...yamlLinesForStep(step, options));
513
+ lines.push('');
514
+ if (step.kind === PublishWorkflowStepKind.Build) {
515
+ lines.push(
516
+ ' - name: ๐Ÿง Build supplemental Linux targets',
517
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
518
+ ` run: smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects "${githubExpression(
519
+ 'steps.version.outputs.projects',
520
+ )}"`,
521
+ '',
522
+ );
523
+ }
524
+ }
525
+ return `${lines.join('\n').trimEnd()}\n`;
526
+ }
527
+
528
+ function renderPlatformPublishWorkflowYaml(options: PublishWorkflowDefinitionOptions): string {
529
+ const deployInput =
530
+ options.deploy === true
531
+ ? `
532
+ deploy_environment:
533
+ type: choice
534
+ description: Deploy live systems after a successful publish.
535
+ options: [none, production]
536
+ default: none`
537
+ : '';
538
+ const steps = definePublishWorkflow(options).steps;
539
+ return `name: Publish
540
+
541
+ on:
542
+ workflow_dispatch:
543
+ inputs:
544
+ bump:
545
+ type: choice
546
+ description:
547
+ Use auto for conventional commits, or force a semver bump. Prerelease publishes to next; all others publish to
548
+ latest.
549
+ options: [auto, patch, minor, major, prerelease]
550
+ default: auto
551
+ dry_run:
552
+ type: boolean
553
+ description: Run release commands without writing versions, tags, publishes, or GitHub Releases.
554
+ default: false${deployInput}
555
+
556
+ permissions:
557
+ contents: read
558
+
559
+ concurrency:
560
+ group: release-${githubExpression('github.ref')}
561
+ cancel-in-progress: false
562
+
563
+ defaults:
564
+ run:
565
+ working-directory: tooling/direnv
566
+
567
+ jobs:
568
+ linux-release-candidate:
569
+ runs-on: ubuntu-latest
570
+ permissions:
571
+ contents: write
572
+ id-token: none
573
+ outputs:
574
+ mode: ${githubExpression('steps.version.outputs.mode')}
575
+ release-sha: ${githubExpression('steps.release-state.outputs.sha')}
576
+ env:
577
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
578
+ GH_TOKEN: ${githubExpression('github.token')}
579
+ steps:
580
+ ${renderLinuxReleaseCandidateSteps(steps, options)}
581
+
582
+ macos-platform:
583
+ runs-on: macos-latest
584
+ permissions:
585
+ contents: read
586
+ id-token: none
587
+ env:
588
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
589
+ GH_TOKEN: ${githubExpression('github.token')}
590
+ steps:
591
+ ${renderMacosPlatformSteps(options)}
592
+
593
+ publish-on-linux:
594
+ needs: [linux-release-candidate, macos-platform]
595
+ runs-on: ubuntu-latest
596
+ permissions:
597
+ contents: write
598
+ id-token: write
599
+ env:
600
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
601
+ GH_TOKEN: ${githubExpression('github.token')}
602
+ steps:
603
+ ${renderFinalLinuxPublishSteps(options)}
604
+ `;
605
+ }
606
+
607
+ function renderLinuxReleaseCandidateSteps(
608
+ steps: PublishWorkflowStep[],
609
+ options: PublishWorkflowDefinitionOptions,
610
+ ): string {
611
+ const lines: string[] = [];
612
+ let stepNumber = 2;
613
+ for (const step of steps) {
614
+ if (
615
+ step.kind === PublishWorkflowStepKind.RepairPendingReleases ||
616
+ step.kind === PublishWorkflowStepKind.PublishRelease ||
617
+ step.kind === PublishWorkflowStepKind.DeployProduction ||
618
+ step.kind === PublishWorkflowStepKind.SaveNixDevenv
619
+ ) {
620
+ continue;
621
+ }
622
+ lines.push(...sectionLinesBefore(step));
623
+ if (step.kind === PublishWorkflowStepKind.Checkout) {
624
+ lines.push(' # Step 1: GitHub adds "Set up job" automatically', ` # Step ${stepNumber}`);
625
+ } else if (step.kind === PublishWorkflowStepKind.SetupDevenv) {
626
+ lines.push(
627
+ ` # Step ${stepNumber}. Composite action internals do not affect top-level job step`,
628
+ ' # anchors; update these comments if top-level steps move.',
629
+ );
630
+ } else {
631
+ lines.push(` # Step ${stepNumber}`);
632
+ }
633
+ if (step.kind === PublishWorkflowStepKind.Checkout) {
634
+ lines.push(
635
+ ` - name: ${step.name}`,
636
+ ' uses: actions/checkout@v6.0.2',
637
+ ' with:',
638
+ ` ref: ${githubExpression('github.sha')}`,
639
+ ' filter: blob:none',
640
+ ' fetch-depth: 0',
641
+ );
642
+ } else if (step.kind === PublishWorkflowStepKind.Build) {
643
+ lines.push(
644
+ ` - name: ${step.name}`,
645
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
646
+ ' run:',
647
+ ` smoo github-ci nx-run-many --targets build --projects "${githubExpression(
648
+ 'steps.version.outputs.projects',
649
+ )}" --collect-outputs "${githubExpression('runner.temp')}/release-build-outputs"`,
650
+ );
651
+ } else {
652
+ lines.push(...yamlLinesForStep(step, options));
653
+ }
654
+ stepNumber += 1;
655
+ if (step.kind === PublishWorkflowStepKind.VersionRelease) {
656
+ lines.push(
657
+ '',
658
+ ` # Step ${stepNumber}`,
659
+ ' - name: ๐Ÿ”’ Capture candidate release SHA',
660
+ ' id: release-state',
661
+ ' run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"',
662
+ );
663
+ stepNumber += 1;
664
+ }
665
+ if (step.kind === PublishWorkflowStepKind.Build && hasLinuxPlatformTargets(options)) {
666
+ lines.push(
667
+ '',
668
+ ` # Step ${stepNumber}`,
669
+ ' - name: ๐Ÿง Build supplemental Linux targets',
670
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
671
+ ' run:',
672
+ ` smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(
673
+ ',',
674
+ )}" --projects "${githubExpression(
675
+ 'steps.version.outputs.projects',
676
+ )}" --collect-outputs "${githubExpression('runner.temp')}/linux-platform-outputs"`,
677
+ );
678
+ stepNumber += 1;
679
+ }
680
+ lines.push('');
681
+ }
682
+ lines.push(
683
+ ' # --- Candidate transfer --------------------------------------------------',
684
+ '',
685
+ ` # Step ${stepNumber++}`,
686
+ ' - name: ๐Ÿ“ฆ Bundle validated release state',
687
+ ' run:',
688
+ ` mkdir -p "${githubExpression('runner.temp')}/publish-release-state" && git bundle create`,
689
+ ` "${githubExpression('runner.temp')}/publish-release-state/release-state.bundle" HEAD --tags && git rev-parse HEAD >`,
690
+ ` "${githubExpression('runner.temp')}/publish-release-state/release-head"`,
691
+ '',
692
+ ` # Step ${stepNumber++}`,
693
+ ' - name: ๐Ÿ“ค Upload validated release state',
694
+ ' uses: actions/upload-artifact@v7.0.1',
695
+ ' with:',
696
+ ` name: publish-release-state-${githubExpression('github.run_id')}`,
697
+ ` path: ${githubExpression('runner.temp')}/publish-release-state`,
698
+ ' if-no-files-found: error',
699
+ ' retention-days: 1',
700
+ '',
701
+ ` # Step ${stepNumber++}`,
702
+ ' - name: ๐Ÿ“ค Upload validated build outputs',
703
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
704
+ ' uses: actions/upload-artifact@v7.0.1',
705
+ ' with:',
706
+ ` name: publish-release-outputs-${githubExpression('github.run_id')}`,
707
+ ` path: ${githubExpression('runner.temp')}/release-build-outputs`,
708
+ ' if-no-files-found: error',
709
+ ' retention-days: 1',
710
+ ' include-hidden-files: true',
711
+ );
712
+ if (hasLinuxPlatformTargets(options)) {
713
+ lines.push(
714
+ '',
715
+ ` # Step ${stepNumber++}`,
716
+ ' - name: ๐Ÿ“ค Upload supplemental Linux outputs',
717
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
718
+ ' uses: actions/upload-artifact@v7.0.1',
719
+ ' with:',
720
+ ` name: publish-linux-outputs-${githubExpression('github.run_id')}`,
721
+ ` path: ${githubExpression('runner.temp')}/linux-platform-outputs`,
722
+ ' if-no-files-found: error',
723
+ ' retention-days: 1',
724
+ ' include-hidden-files: true',
725
+ );
726
+ }
727
+ lines.push(
728
+ '',
729
+ ' # --- Cleanup ------------------------------------------------------------',
730
+ '',
731
+ ` # Step ${stepNumber}`,
732
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
733
+ ` if: ${githubExpression('always()')}`,
734
+ ' uses: ./.github/actions/save-nix-devenv',
735
+ ' with:',
736
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
737
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
738
+ );
739
+ return lines.join('\n').trimEnd();
740
+ }
741
+
742
+ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): string {
743
+ let stepNumber = 4;
744
+ const lines = [
745
+ ' # --- Setup --------------------------------------------------------------',
746
+ '',
747
+ ' # Step 1: GitHub adds "Set up job" automatically',
748
+ ' # Step 2',
749
+ ' - name: ๐Ÿ“ฅ Checkout dispatch commit',
750
+ ' uses: actions/checkout@v6.0.2',
751
+ ' with:',
752
+ ` ref: ${githubExpression('github.sha')}`,
753
+ ' filter: blob:none',
754
+ ' fetch-depth: 0',
755
+ '',
756
+ ' # Step 3. Composite action internals do not affect top-level job step',
757
+ ' # anchors; update these comments if top-level steps move.',
758
+ ' - name: ๐Ÿงฑ Setup Nix/devenv',
759
+ ' id: setup',
760
+ ' uses: ./.github/actions/setup-devenv',
761
+ ];
762
+ if (isSmoothBricksCodebasePackageName(options.repoName)) {
763
+ lines.push(
764
+ '',
765
+ ` # Step ${stepNumber++}`,
766
+ ' - name: ๐Ÿ—๏ธ Build self-hosted smoo',
767
+ ' # SmoothBricks self-hosts smoo from source for release commands.',
768
+ ' run: nx build cli',
769
+ );
770
+ }
771
+ lines.push(
772
+ '',
773
+ ` # Step ${stepNumber++}`,
774
+ ' - name: ๐ŸŽ Build selected macOS and iOS release outputs',
775
+ ' run:',
776
+ ` smoo release build-platform-outputs --bump "${githubExpression(
777
+ 'inputs.bump',
778
+ )}" --ref "${githubExpression('github.sha')}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}" --output`,
779
+ ` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
780
+ '',
781
+ ` # Step ${stepNumber++}`,
782
+ ' - name: ๐Ÿ“ค Upload macOS platform outputs',
783
+ ' uses: actions/upload-artifact@v7.0.1',
784
+ ' with:',
785
+ ` name: publish-macos-outputs-${githubExpression('github.run_id')}`,
786
+ ` path: ${githubExpression('runner.temp')}/macos-platform-outputs`,
787
+ ' if-no-files-found: error',
788
+ ' retention-days: 1',
789
+ ' include-hidden-files: true',
790
+ '',
791
+ ' # --- Cleanup ------------------------------------------------------------',
792
+ '',
793
+ ` # Step ${stepNumber}`,
794
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
795
+ ` if: ${githubExpression('always()')}`,
796
+ ' uses: ./.github/actions/save-nix-devenv',
797
+ ' with:',
798
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
799
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
800
+ );
801
+ return lines.join('\n').trimEnd();
802
+ }
803
+
804
+ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions): string {
805
+ const mode = 'needs.linux-release-candidate.outputs.mode';
806
+ let stepNumber = 2;
807
+ const lines = [
808
+ ' # --- Setup --------------------------------------------------------------',
809
+ '',
810
+ ' # Step 1: GitHub adds "Set up job" automatically',
811
+ ` # Step ${stepNumber++}`,
812
+ ' - name: ๐Ÿ“ฅ Checkout dispatch commit',
813
+ ' uses: actions/checkout@v6.0.2',
814
+ ' with:',
815
+ ` ref: ${githubExpression('github.sha')}`,
816
+ ' filter: blob:none',
817
+ ' fetch-depth: 0',
818
+ '',
819
+ ` # Step ${stepNumber++}`,
820
+ ' - name: ๐Ÿ“ฅ Download candidate artifacts',
821
+ ' uses: actions/download-artifact@v8.0.1',
822
+ ' with:',
823
+ ` pattern: publish-*-${githubExpression('github.run_id')}`,
824
+ ` path: ${githubExpression('runner.temp')}/publish-artifacts`,
825
+ ' merge-multiple: false',
826
+ '',
827
+ ` # Step ${stepNumber}. Composite action internals do not affect top-level job step`,
828
+ ' # anchors; update these comments if top-level steps move.',
829
+ ' - name: ๐Ÿงฑ Setup Nix/devenv',
830
+ ' id: setup',
831
+ ' uses: ./.github/actions/setup-devenv',
832
+ '',
833
+ ` # Step ${++stepNumber}`,
834
+ ' - name: ๐Ÿค– Configure release author',
835
+ ' run:',
836
+ ' git config user.name "github-actions[bot]" && git config user.email',
837
+ ' "41898282+github-actions[bot]@users.noreply.github.com"',
838
+ ];
839
+ stepNumber += 1;
840
+ if (isSmoothBricksCodebasePackageName(options.repoName)) {
841
+ lines.push(
842
+ '',
843
+ ` # Step ${stepNumber++}`,
844
+ ' - name: ๐Ÿ—๏ธ Build self-hosted smoo',
845
+ ' # SmoothBricks self-hosts smoo from source for release commands.',
846
+ ' run: nx build cli',
847
+ );
848
+ }
849
+ lines.push(
850
+ '',
851
+ ' # --- Repair -------------------------------------------------------------',
852
+ '',
853
+ ` # Step ${stepNumber++}`,
854
+ ' - name: ๐Ÿงฏ Repair pending releases',
855
+ ' run:',
856
+ ` smoo release repair-pending --ref "${githubExpression(
857
+ 'github.sha',
858
+ )}" --platform-outputs "${githubExpression(
859
+ 'runner.temp',
860
+ )}/publish-artifacts/publish-macos-outputs-${githubExpression('github.run_id')}/repairs" --dry-run`,
861
+ ` "${githubExpression('inputs.dry_run')}"`,
862
+ '',
863
+ ` # Step ${stepNumber++}`,
864
+ ' - name: โ™ป๏ธ Restore validated release state',
865
+ ' run:',
866
+ ` git fetch "${githubExpression(
867
+ 'runner.temp',
868
+ )}/publish-artifacts/publish-release-state-${githubExpression('github.run_id')}/release-state.bundle" HEAD --tags && git reset`,
869
+ ` --hard "$(cat "${githubExpression(
870
+ 'runner.temp',
871
+ )}/publish-artifacts/publish-release-state-${githubExpression('github.run_id')}/release-head")"`,
872
+ );
873
+ lines.push(
874
+ '',
875
+ ' # --- Validation ---------------------------------------------------------',
876
+ '',
877
+ ` # Step ${stepNumber++}`,
878
+ ' - name: ๐Ÿ“ฆ Apply verified Linux outputs',
879
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
880
+ ' run:',
881
+ ` smoo github-ci apply-outputs --source-sha "${githubExpression(
882
+ 'needs.linux-release-candidate.outputs.release-sha',
883
+ )}"`,
884
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-release-outputs-${githubExpression(
885
+ 'github.run_id',
886
+ )}"`,
887
+ );
888
+ if (hasLinuxPlatformTargets(options)) {
889
+ lines.push(
890
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-linux-outputs-${githubExpression(
891
+ 'github.run_id',
892
+ )}"`,
893
+ );
894
+ }
895
+ lines.push(
896
+ '',
897
+ ` # Step ${stepNumber++}`,
898
+ ' - name: ๐ŸŽ Apply verified macOS outputs',
899
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
900
+ ' run:',
901
+ ` smoo github-ci apply-outputs --source-sha "${githubExpression('github.sha')}"`,
902
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-macos-outputs-${githubExpression(
903
+ 'github.run_id',
904
+ )}/current"`,
905
+ '',
906
+ ` # Step ${stepNumber++}`,
907
+ ` - name: โœ… Validate restored release (${githubExpression(mode)})`,
908
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
909
+ ' run: smoo monorepo validate',
910
+ '',
911
+ ' # --- Release ------------------------------------------------------------',
912
+ '',
913
+ ` # Step ${stepNumber++}`,
914
+ ` - name: ๐Ÿ“ฆ Publish release (${githubExpression(mode)})`,
915
+ ' # smoo packs with Bun, then publishes tarballs with npm. Existing',
916
+ ' # packages must already exist on npm and use trusted publishing/OIDC.',
917
+ ' # Missing package names are bootstrapped locally before trust setup.',
918
+ ` run: smoo release publish --bump "${githubExpression('inputs.bump')}" --dry-run "${githubExpression(
919
+ 'inputs.dry_run',
920
+ )}"`,
921
+ );
922
+ if (options.deploy === true) {
923
+ lines.push(
924
+ '',
925
+ ` # Step ${stepNumber++}`,
926
+ ' - name: ๐Ÿš€ Deploy production',
927
+ ' if:',
928
+ " ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_environment == 'production' &&",
929
+ " inputs.dry_run != 'true' }}",
930
+ ...deployEnvLines(options),
931
+ ' run: smoo github-ci nx-deploy --configuration production --mode run-many --verify --name "Deploy Production"',
932
+ );
933
+ }
934
+ lines.push(
935
+ '',
936
+ ' # --- Cleanup ------------------------------------------------------------',
937
+ '',
938
+ ` # Step ${stepNumber}`,
939
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
940
+ ` if: ${githubExpression('always()')}`,
941
+ ' uses: ./.github/actions/save-nix-devenv',
942
+ ' with:',
943
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
944
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
945
+ );
946
+ return lines.join('\n').trimEnd();
947
+ }
948
+
949
+ function hasMacosPlatformTargets(options: PublishWorkflowDefinitionOptions): boolean {
950
+ return MACOS_PLATFORM_TARGET_GLOBS.some((glob) => options.platformTargetGlobs?.includes(glob) === true);
951
+ }
952
+
953
+ function hasLinuxPlatformTargets(options: PublishWorkflowDefinitionOptions): boolean {
954
+ return LINUX_PLATFORM_TARGET_GLOBS.some((glob) => options.platformTargetGlobs?.includes(glob) === true);
955
+ }
956
+
478
957
  function githubExpression(expression: string): string {
479
958
  return ['$', '{{ ', expression, ' }}'].join('');
480
959
  }
@@ -1,11 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { runtimeTypesRangeForPublishedVersions } from './runtime.js';
2
+ import type { PackageJson } from '../lib/json.js';
3
+ import { runtimeTypesRangeForPublishedVersions, validateRuntimePins } from './runtime.js';
3
4
 
4
5
  describe('runtimeTypesRangeForPublishedVersions', () => {
5
- test('uses the installed Node major when @types/node has published it', () => {
6
+ test('uses the newest published minor within the installed Node major, never the ~major.0.0 floor', () => {
6
7
  expect(
7
8
  runtimeTypesRangeForPublishedVersions('@types/node', '24.12.0', 'major', ['24.0.0', '24.12.4', '25.9.1']),
8
- ).toBe('~24.0.0');
9
+ ).toBe('~24.12.4');
9
10
  });
10
11
 
11
12
  test('falls back to latest published @types/node when the Node major is unpublished', () => {
@@ -28,3 +29,41 @@ describe('runtimeTypesRangeForPublishedVersions', () => {
28
29
  );
29
30
  });
30
31
  });
32
+
33
+ describe('validateRuntimePins', () => {
34
+ const runtime = { node: '24.16.0', bun: '1.3.14' };
35
+ const aligned = () => ({
36
+ engines: { node: '>=24.0.0' },
37
+ packageManager: 'bun@1.3.14',
38
+ devDependencies: { '@types/node': '~24.13.0' },
39
+ });
40
+
41
+ test('passes when every pin agrees with the PATH runtimes', () => {
42
+ expect(validateRuntimePins(aligned(), runtime)).toBe(0);
43
+ });
44
+
45
+ test('fails when @types/node tracks a different major than the PATH node', () => {
46
+ const pkg = aligned();
47
+ pkg.devDependencies['@types/node'] = '~26.1.1';
48
+ expect(validateRuntimePins(pkg, runtime)).toBe(1);
49
+ });
50
+
51
+ test('fails the ~major.0.0 floor pin even when the major matches', () => {
52
+ const pkg = aligned();
53
+ pkg.devDependencies['@types/node'] = '~24.0.0';
54
+ expect(validateRuntimePins(pkg, runtime)).toBe(1);
55
+ });
56
+
57
+ test('fails engines.node and packageManager drift against the PATH runtimes', () => {
58
+ const pkg = aligned();
59
+ pkg.engines.node = '>=22.0.0';
60
+ pkg.packageManager = 'bun@1.2.0';
61
+ expect(validateRuntimePins(pkg, runtime)).toBe(2);
62
+ });
63
+
64
+ test('fails a missing @types/node pin', () => {
65
+ const pkg: PackageJson = aligned();
66
+ pkg.devDependencies = {};
67
+ expect(validateRuntimePins(pkg, runtime)).toBe(1);
68
+ });
69
+ });