@smoothbricks/cli 0.10.1 โ†’ 0.10.3

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 (77) hide show
  1. package/README.md +25 -13
  2. package/dist/cli.js +24 -0
  3. package/dist/github-ci/index.d.ts +17 -2
  4. package/dist/github-ci/index.d.ts.map +1 -1
  5. package/dist/github-ci/index.js +145 -14
  6. package/dist/github-ci/outputs.d.ts +22 -0
  7. package/dist/github-ci/outputs.d.ts.map +1 -0
  8. package/dist/github-ci/outputs.js +649 -0
  9. package/dist/lib/conflict-markers.d.ts.map +1 -1
  10. package/dist/lib/workspace.d.ts +1 -1
  11. package/dist/lib/workspace.d.ts.map +1 -1
  12. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  13. package/dist/monorepo/ci-workflow.js +2 -1
  14. package/dist/monorepo/managed-files.d.ts +2 -0
  15. package/dist/monorepo/managed-files.d.ts.map +1 -1
  16. package/dist/monorepo/managed-files.js +50 -5
  17. package/dist/monorepo/package-policy.d.ts.map +1 -1
  18. package/dist/monorepo/package-policy.js +26 -0
  19. package/dist/monorepo/packs/index.d.ts +3 -0
  20. package/dist/monorepo/packs/index.d.ts.map +1 -1
  21. package/dist/monorepo/packs/index.js +5 -2
  22. package/dist/monorepo/publish-workflow.d.ts +1 -0
  23. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  24. package/dist/monorepo/publish-workflow.js +240 -2
  25. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  26. package/dist/monorepo/tool-validation.js +20 -7
  27. package/dist/nx/index.d.ts +6 -0
  28. package/dist/nx/index.d.ts.map +1 -1
  29. package/dist/nx/index.js +55 -5
  30. package/dist/release/github-release.d.ts +5 -2
  31. package/dist/release/github-release.d.ts.map +1 -1
  32. package/dist/release/github-release.js +11 -0
  33. package/dist/release/index.d.ts +19 -1
  34. package/dist/release/index.d.ts.map +1 -1
  35. package/dist/release/index.js +140 -31
  36. package/dist/release/orchestration.d.ts +8 -1
  37. package/dist/release/orchestration.d.ts.map +1 -1
  38. package/dist/release/orchestration.js +38 -1
  39. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  40. package/dist/wrangler/prepare-env.js +1 -1
  41. package/dist/wrangler/scaffold.d.ts.map +1 -1
  42. package/dist/wrangler/scaffold.js +1 -1
  43. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +5 -0
  44. package/managed/raw/tooling/direnv/setup-environment.ts +32 -1
  45. package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
  46. package/managed/templates/github/actions/cache-node-modules/action.yml +3 -2
  47. package/managed/templates/github/actions/cache-nx/action.yml +2 -2
  48. package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
  49. package/package.json +7 -14
  50. package/src/cli.ts +30 -4
  51. package/src/github-ci/index.test.ts +486 -0
  52. package/src/github-ci/index.ts +177 -16
  53. package/src/github-ci/outputs.ts +506 -0
  54. package/src/monorepo/__tests__/ci-workflow.test.ts +13 -0
  55. package/src/monorepo/__tests__/publish-workflow.test.ts +234 -5
  56. package/src/monorepo/ci-workflow.ts +3 -1
  57. package/src/monorepo/managed-files.test.ts +75 -0
  58. package/src/monorepo/managed-files.ts +61 -5
  59. package/src/monorepo/package-policy.test.ts +57 -38
  60. package/src/monorepo/package-policy.ts +28 -0
  61. package/src/monorepo/packs/index.test.ts +18 -1
  62. package/src/monorepo/packs/index.ts +7 -3
  63. package/src/monorepo/publish-workflow.ts +481 -2
  64. package/src/monorepo/tool-validation.test.ts +59 -24
  65. package/src/monorepo/tool-validation.ts +21 -7
  66. package/src/monorepo/wrangler.test.ts +9 -2
  67. package/src/nx/index.test.ts +39 -0
  68. package/src/nx/index.ts +65 -5
  69. package/src/release/__tests__/fixture-repo.test.ts +18 -16
  70. package/src/release/__tests__/github-release.test.ts +11 -0
  71. package/src/release/__tests__/orchestration.test.ts +57 -2
  72. package/src/release/__tests__/trust-publisher.test.ts +91 -2
  73. package/src/release/github-release.ts +12 -0
  74. package/src/release/index.ts +218 -40
  75. package/src/release/orchestration.ts +55 -3
  76. package/src/wrangler/prepare-env.ts +14 -4
  77. package/src/wrangler/scaffold.ts +8 -3
@@ -371,6 +371,7 @@ export function validatePublicPackageMetadata(root: string): number {
371
371
 
372
372
  export function validateWorkspaceDependencies(root: string, options: PackageTargetPolicyOptions = {}): number {
373
373
  let failures = 0;
374
+ failures += validateCiSkipTags(root, options);
374
375
  const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
375
376
  for (const pkg of listPackageJsonRecords(root)) {
376
377
  for (const field of workspaceDependencyFields) {
@@ -404,6 +405,33 @@ export function validateWorkspaceDependencies(root: string, options: PackageTarg
404
405
  return failures;
405
406
  }
406
407
 
408
+ const ciSkipTagPrefix = 'ci:skip:';
409
+ const invalidCiSkipTargetSyntax = /[,*?[\]{}]/;
410
+
411
+ function validateCiSkipTags(root: string, options: PackageTargetPolicyOptions): number {
412
+ let failures = 0;
413
+ for (const pkg of getWorkspacePackages(root)) {
414
+ const resolved = options.resolvedTargetsByProject?.get(pkg.projectName);
415
+ const resolvedTargets = resolved && 'targets' in resolved ? resolved.targets : resolved;
416
+ for (const tag of pkg.tags) {
417
+ if (tag !== 'ci:skip' && !tag.startsWith(ciSkipTagPrefix)) {
418
+ continue;
419
+ }
420
+ const target = tag.startsWith(ciSkipTagPrefix) ? tag.slice(ciSkipTagPrefix.length) : '';
421
+ if (!target || invalidCiSkipTargetSyntax.test(target)) {
422
+ console.error(`${pkg.path}: nx tag ${tag} must use ci:skip:<target> with one exact Nx target name`);
423
+ failures++;
424
+ continue;
425
+ }
426
+ if (resolvedTargets && !resolvedTargets.has(target)) {
427
+ console.error(`${pkg.path}: nx tag ${tag} names missing Nx target ${target}`);
428
+ failures++;
429
+ }
430
+ }
431
+ }
432
+ return failures;
433
+ }
434
+
407
435
  // ---------------------------------------------------------------------------
408
436
  // Helpers kept in CLI (not Nx-specific)
409
437
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
  import type { MonorepoPack } from './index.js';
3
- import { runValidatePacks } from './index.js';
3
+ import { resolvedTargetsByProject, runValidatePacks } from './index.js';
4
4
 
5
5
  const ctx = { root: '/workspace', syncRuntime: false };
6
6
 
@@ -186,4 +186,21 @@ describe('monorepo validation pack phases', () => {
186
186
  expect(result).toEqual({ failures: 1, failedChecks: 1 });
187
187
  expect(events).toEqual(['pre-fix', 'build']);
188
188
  });
189
+
190
+ it('propagates parsed target dependencies through the production adapter', () => {
191
+ const targetDependencies = new Map([
192
+ ['build', ['compile-linux']],
193
+ ['compile-linux', ['package-linux']],
194
+ ]);
195
+
196
+ const resolved = resolvedTargetsByProject([
197
+ {
198
+ project: 'native',
199
+ targets: ['build', 'compile-linux', 'package-linux'],
200
+ targetDependencies,
201
+ },
202
+ ]);
203
+
204
+ expect(resolved.get('native')?.targetDependencies).toBe(targetDependencies);
205
+ });
189
206
  });
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { printCommandOutput, runResult, runStatus } from '../../lib/run.js';
4
- import { readProjectTargets } from '../../nx/index.js';
4
+ import { type ProjectTargets, readProjectTargets } from '../../nx/index.js';
5
5
  import { syncBunLockfileVersions, validateBunLockfileVersions } from '../lockfile.js';
6
6
  import { validateManagedFiles } from '../managed-files.js';
7
7
  import { fixNxSync, validateNxSync } from '../nx-sync.js';
@@ -400,14 +400,14 @@ function printCapturedOutput(output: CapturedOutput): void {
400
400
  }
401
401
  }
402
402
 
403
- async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<string, ResolvedProjectTargets>> {
404
- const projects = await readProjectTargets(ctx.root);
403
+ export function resolvedTargetsByProject(projects: ProjectTargets[]): Map<string, ResolvedProjectTargets> {
405
404
  return new Map(
406
405
  projects.map((project) => [
407
406
  project.project,
408
407
  {
409
408
  targets: new Set(project.targets),
410
409
  ...(project.buildDependsOn ? { buildDependsOn: project.buildDependsOn } : {}),
410
+ ...(project.targetDependencies ? { targetDependencies: project.targetDependencies } : {}),
411
411
  ...(project.targetExecutors ? { targetExecutors: project.targetExecutors } : {}),
412
412
  ...(project.targetScripts ? { targetScripts: project.targetScripts } : {}),
413
413
  },
@@ -415,6 +415,10 @@ async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<s
415
415
  );
416
416
  }
417
417
 
418
+ async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<string, ResolvedProjectTargets>> {
419
+ return resolvedTargetsByProject(await readProjectTargets(ctx.root));
420
+ }
421
+
418
422
  function ensureLocalSmooShim(root: string): void {
419
423
  const shim = join(root, 'tooling', 'smoo');
420
424
  if (!existsSync(shim)) {
@@ -1,5 +1,19 @@
1
+ /* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
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';
1
9
  import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
2
10
 
11
+ const PUBLISH_WORKFLOW_FORMAT_OPTIONS = Object.freeze({
12
+ parser: 'yaml',
13
+ printWidth: 120,
14
+ proseWrap: 'always',
15
+ } satisfies PrettierOptions);
16
+
3
17
  export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
4
18
  export type PublishWorkflowCondition = 'version-mode-not-none' | 'deploy-production' | 'failure' | 'always';
5
19
  export type PublishWorkflowNxTarget = 'build' | 'lint' | 'test';
@@ -40,6 +54,7 @@ export interface PublishWorkflowDefinitionOptions {
40
54
  deploy?: boolean;
41
55
  deployProvider?: 'cloudflare';
42
56
  repoName?: string;
57
+ platformTargetGlobs?: readonly string[];
43
58
  }
44
59
 
45
60
  export interface PublishWorkflowInputs {
@@ -254,8 +269,19 @@ function shouldRunStep(
254
269
  }
255
270
 
256
271
  export function renderPublishWorkflowYaml(options: PublishWorkflowDefinitionOptions = {}): string {
257
- const steps = definePublishWorkflow(options).steps;
258
- 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);
259
285
  }
260
286
 
261
287
  function renderPublishWorkflowHeader(options: PublishWorkflowDefinitionOptions): string {
@@ -473,6 +499,459 @@ function conditionalRunStep(step: PublishWorkflowStep, run: string): string[] {
473
499
  return [` - name: ${step.name}`, ` if: ${githubExpression(condition)}`, ` run: ${run}`];
474
500
  }
475
501
 
502
+ function renderSingleJobPublishWorkflowSteps(
503
+ steps: PublishWorkflowStep[],
504
+ options: PublishWorkflowDefinitionOptions,
505
+ ): string {
506
+ const lines: string[] = [];
507
+ for (const step of steps) {
508
+ lines.push(...sectionLinesBefore(step));
509
+ lines.push(...commentLinesForStep(step));
510
+ lines.push(...yamlLinesForStep(step, options));
511
+ lines.push('');
512
+ if (step.kind === PublishWorkflowStepKind.Build) {
513
+ lines.push(
514
+ ' - name: ๐Ÿง Build supplemental Linux targets',
515
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
516
+ ` run: smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects "${githubExpression(
517
+ 'steps.version.outputs.projects',
518
+ )}"`,
519
+ '',
520
+ );
521
+ }
522
+ }
523
+ return `${lines.join('\n').trimEnd()}\n`;
524
+ }
525
+
526
+ function renderPlatformPublishWorkflowYaml(options: PublishWorkflowDefinitionOptions): string {
527
+ const deployInput =
528
+ options.deploy === true
529
+ ? `
530
+ deploy_environment:
531
+ type: choice
532
+ description: Deploy live systems after a successful publish.
533
+ options: [none, production]
534
+ default: none`
535
+ : '';
536
+ const steps = definePublishWorkflow(options).steps;
537
+ return `name: Publish
538
+
539
+ on:
540
+ workflow_dispatch:
541
+ inputs:
542
+ bump:
543
+ type: choice
544
+ description:
545
+ Use auto for conventional commits, or force a semver bump. Prerelease publishes to next; all others publish to
546
+ latest.
547
+ options: [auto, patch, minor, major, prerelease]
548
+ default: auto
549
+ dry_run:
550
+ type: boolean
551
+ description: Run release commands without writing versions, tags, publishes, or GitHub Releases.
552
+ default: false${deployInput}
553
+
554
+ permissions:
555
+ contents: read
556
+
557
+ concurrency:
558
+ group: release-${githubExpression('github.ref')}
559
+ cancel-in-progress: false
560
+
561
+ defaults:
562
+ run:
563
+ working-directory: tooling/direnv
564
+
565
+ jobs:
566
+ linux-release-candidate:
567
+ runs-on: ubuntu-latest
568
+ permissions:
569
+ contents: write
570
+ id-token: none
571
+ outputs:
572
+ mode: ${githubExpression('steps.version.outputs.mode')}
573
+ release-sha: ${githubExpression('steps.release-state.outputs.sha')}
574
+ env:
575
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
576
+ GH_TOKEN: ${githubExpression('github.token')}
577
+ steps:
578
+ ${renderLinuxReleaseCandidateSteps(steps, options)}
579
+
580
+ macos-platform:
581
+ runs-on: macos-latest
582
+ permissions:
583
+ contents: read
584
+ id-token: none
585
+ env:
586
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
587
+ GH_TOKEN: ${githubExpression('github.token')}
588
+ steps:
589
+ ${renderMacosPlatformSteps(options)}
590
+
591
+ publish-on-linux:
592
+ needs: [linux-release-candidate, macos-platform]
593
+ runs-on: ubuntu-latest
594
+ permissions:
595
+ contents: write
596
+ id-token: write
597
+ env:
598
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
599
+ GH_TOKEN: ${githubExpression('github.token')}
600
+ steps:
601
+ ${renderFinalLinuxPublishSteps(options)}
602
+ `;
603
+ }
604
+
605
+ function renderLinuxReleaseCandidateSteps(
606
+ steps: PublishWorkflowStep[],
607
+ options: PublishWorkflowDefinitionOptions,
608
+ ): string {
609
+ const lines: string[] = [];
610
+ let stepNumber = 2;
611
+ for (const step of steps) {
612
+ if (
613
+ step.kind === PublishWorkflowStepKind.RepairPendingReleases ||
614
+ step.kind === PublishWorkflowStepKind.PublishRelease ||
615
+ step.kind === PublishWorkflowStepKind.DeployProduction ||
616
+ step.kind === PublishWorkflowStepKind.SaveNixDevenv
617
+ ) {
618
+ continue;
619
+ }
620
+ lines.push(...sectionLinesBefore(step));
621
+ if (step.kind === PublishWorkflowStepKind.Checkout) {
622
+ lines.push(' # Step 1: GitHub adds "Set up job" automatically', ` # Step ${stepNumber}`);
623
+ } else if (step.kind === PublishWorkflowStepKind.SetupDevenv) {
624
+ lines.push(
625
+ ` # Step ${stepNumber}. Composite action internals do not affect top-level job step`,
626
+ ' # anchors; update these comments if top-level steps move.',
627
+ );
628
+ } else {
629
+ lines.push(` # Step ${stepNumber}`);
630
+ }
631
+ if (step.kind === PublishWorkflowStepKind.Checkout) {
632
+ lines.push(
633
+ ` - name: ${step.name}`,
634
+ ' uses: actions/checkout@v6.0.2',
635
+ ' with:',
636
+ ` ref: ${githubExpression('github.sha')}`,
637
+ ' filter: blob:none',
638
+ ' fetch-depth: 0',
639
+ );
640
+ } else if (step.kind === PublishWorkflowStepKind.Build) {
641
+ lines.push(
642
+ ` - name: ${step.name}`,
643
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
644
+ ' run:',
645
+ ` smoo github-ci nx-run-many --targets build --projects "${githubExpression(
646
+ 'steps.version.outputs.projects',
647
+ )}" --collect-outputs "${githubExpression('runner.temp')}/release-build-outputs"`,
648
+ );
649
+ } else {
650
+ lines.push(...yamlLinesForStep(step, options));
651
+ }
652
+ stepNumber += 1;
653
+ if (step.kind === PublishWorkflowStepKind.VersionRelease) {
654
+ lines.push(
655
+ '',
656
+ ` # Step ${stepNumber}`,
657
+ ' - name: ๐Ÿ”’ Capture candidate release SHA',
658
+ ' id: release-state',
659
+ ' run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"',
660
+ );
661
+ stepNumber += 1;
662
+ }
663
+ if (step.kind === PublishWorkflowStepKind.Build && hasLinuxPlatformTargets(options)) {
664
+ lines.push(
665
+ '',
666
+ ` # Step ${stepNumber}`,
667
+ ' - name: ๐Ÿง Build supplemental Linux targets',
668
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
669
+ ' run:',
670
+ ` smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(
671
+ ',',
672
+ )}" --projects "${githubExpression(
673
+ 'steps.version.outputs.projects',
674
+ )}" --collect-outputs "${githubExpression('runner.temp')}/linux-platform-outputs"`,
675
+ );
676
+ stepNumber += 1;
677
+ }
678
+ lines.push('');
679
+ }
680
+ lines.push(
681
+ ' # --- Candidate transfer --------------------------------------------------',
682
+ '',
683
+ ` # Step ${stepNumber++}`,
684
+ ' - name: ๐Ÿ“ฆ Bundle validated release state',
685
+ ' run:',
686
+ ` mkdir -p "${githubExpression('runner.temp')}/publish-release-state" && git bundle create`,
687
+ ` "${githubExpression('runner.temp')}/publish-release-state/release-state.bundle" HEAD --tags && git rev-parse HEAD >`,
688
+ ` "${githubExpression('runner.temp')}/publish-release-state/release-head"`,
689
+ '',
690
+ ` # Step ${stepNumber++}`,
691
+ ' - name: ๐Ÿ“ค Upload validated release state',
692
+ ' uses: actions/upload-artifact@v7.0.1',
693
+ ' with:',
694
+ ` name: publish-release-state-${githubExpression('github.run_id')}`,
695
+ ` path: ${githubExpression('runner.temp')}/publish-release-state`,
696
+ ' if-no-files-found: error',
697
+ ' retention-days: 1',
698
+ '',
699
+ ` # Step ${stepNumber++}`,
700
+ ' - name: ๐Ÿ“ค Upload validated build outputs',
701
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
702
+ ' uses: actions/upload-artifact@v7.0.1',
703
+ ' with:',
704
+ ` name: publish-release-outputs-${githubExpression('github.run_id')}`,
705
+ ` path: ${githubExpression('runner.temp')}/release-build-outputs`,
706
+ ' if-no-files-found: error',
707
+ ' retention-days: 1',
708
+ ' include-hidden-files: true',
709
+ );
710
+ if (hasLinuxPlatformTargets(options)) {
711
+ lines.push(
712
+ '',
713
+ ` # Step ${stepNumber++}`,
714
+ ' - name: ๐Ÿ“ค Upload supplemental Linux outputs',
715
+ ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
716
+ ' uses: actions/upload-artifact@v7.0.1',
717
+ ' with:',
718
+ ` name: publish-linux-outputs-${githubExpression('github.run_id')}`,
719
+ ` path: ${githubExpression('runner.temp')}/linux-platform-outputs`,
720
+ ' if-no-files-found: error',
721
+ ' retention-days: 1',
722
+ ' include-hidden-files: true',
723
+ );
724
+ }
725
+ lines.push(
726
+ '',
727
+ ' # --- Cleanup ------------------------------------------------------------',
728
+ '',
729
+ ` # Step ${stepNumber}`,
730
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
731
+ ` if: ${githubExpression('always()')}`,
732
+ ' uses: ./.github/actions/save-nix-devenv',
733
+ ' with:',
734
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
735
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
736
+ );
737
+ return lines.join('\n').trimEnd();
738
+ }
739
+
740
+ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): string {
741
+ let stepNumber = 4;
742
+ const lines = [
743
+ ' # --- Setup --------------------------------------------------------------',
744
+ '',
745
+ ' # Step 1: GitHub adds "Set up job" automatically',
746
+ ' # Step 2',
747
+ ' - name: ๐Ÿ“ฅ Checkout dispatch commit',
748
+ ' uses: actions/checkout@v6.0.2',
749
+ ' with:',
750
+ ` ref: ${githubExpression('github.sha')}`,
751
+ ' filter: blob:none',
752
+ ' fetch-depth: 0',
753
+ '',
754
+ ' # Step 3. Composite action internals do not affect top-level job step',
755
+ ' # anchors; update these comments if top-level steps move.',
756
+ ' - name: ๐Ÿงฑ Setup Nix/devenv',
757
+ ' id: setup',
758
+ ' uses: ./.github/actions/setup-devenv',
759
+ ];
760
+ if (isSmoothBricksCodebasePackageName(options.repoName)) {
761
+ lines.push(
762
+ '',
763
+ ` # Step ${stepNumber++}`,
764
+ ' - name: ๐Ÿ—๏ธ Build self-hosted smoo',
765
+ ' # SmoothBricks self-hosts smoo from source for release commands.',
766
+ ' run: nx build cli',
767
+ );
768
+ }
769
+ lines.push(
770
+ '',
771
+ ` # Step ${stepNumber++}`,
772
+ ' - name: ๐ŸŽ Build selected macOS and iOS release outputs',
773
+ ' run:',
774
+ ` smoo release build-platform-outputs --bump "${githubExpression(
775
+ 'inputs.bump',
776
+ )}" --ref "${githubExpression('github.sha')}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}" --output`,
777
+ ` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
778
+ '',
779
+ ` # Step ${stepNumber++}`,
780
+ ' - name: ๐Ÿ“ค Upload macOS platform outputs',
781
+ ' uses: actions/upload-artifact@v7.0.1',
782
+ ' with:',
783
+ ` name: publish-macos-outputs-${githubExpression('github.run_id')}`,
784
+ ` path: ${githubExpression('runner.temp')}/macos-platform-outputs`,
785
+ ' if-no-files-found: error',
786
+ ' retention-days: 1',
787
+ ' include-hidden-files: true',
788
+ '',
789
+ ' # --- Cleanup ------------------------------------------------------------',
790
+ '',
791
+ ` # Step ${stepNumber}`,
792
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
793
+ ` if: ${githubExpression('always()')}`,
794
+ ' uses: ./.github/actions/save-nix-devenv',
795
+ ' with:',
796
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
797
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
798
+ );
799
+ return lines.join('\n').trimEnd();
800
+ }
801
+
802
+ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions): string {
803
+ const mode = 'needs.linux-release-candidate.outputs.mode';
804
+ let stepNumber = 2;
805
+ const lines = [
806
+ ' # --- Setup --------------------------------------------------------------',
807
+ '',
808
+ ' # Step 1: GitHub adds "Set up job" automatically',
809
+ ` # Step ${stepNumber++}`,
810
+ ' - name: ๐Ÿ“ฅ Checkout dispatch commit',
811
+ ' uses: actions/checkout@v6.0.2',
812
+ ' with:',
813
+ ` ref: ${githubExpression('github.sha')}`,
814
+ ' filter: blob:none',
815
+ ' fetch-depth: 0',
816
+ '',
817
+ ` # Step ${stepNumber++}`,
818
+ ' - name: ๐Ÿ“ฅ Download candidate artifacts',
819
+ ' uses: actions/download-artifact@v8.0.1',
820
+ ' with:',
821
+ ` pattern: publish-*-${githubExpression('github.run_id')}`,
822
+ ` path: ${githubExpression('runner.temp')}/publish-artifacts`,
823
+ ' merge-multiple: false',
824
+ '',
825
+ ` # Step ${stepNumber}. Composite action internals do not affect top-level job step`,
826
+ ' # anchors; update these comments if top-level steps move.',
827
+ ' - name: ๐Ÿงฑ Setup Nix/devenv',
828
+ ' id: setup',
829
+ ' uses: ./.github/actions/setup-devenv',
830
+ '',
831
+ ` # Step ${++stepNumber}`,
832
+ ' - name: ๐Ÿค– Configure release author',
833
+ ' run:',
834
+ ' git config user.name "github-actions[bot]" && git config user.email',
835
+ ' "41898282+github-actions[bot]@users.noreply.github.com"',
836
+ ];
837
+ stepNumber += 1;
838
+ if (isSmoothBricksCodebasePackageName(options.repoName)) {
839
+ lines.push(
840
+ '',
841
+ ` # Step ${stepNumber++}`,
842
+ ' - name: ๐Ÿ—๏ธ Build self-hosted smoo',
843
+ ' # SmoothBricks self-hosts smoo from source for release commands.',
844
+ ' run: nx build cli',
845
+ );
846
+ }
847
+ lines.push(
848
+ '',
849
+ ' # --- Repair -------------------------------------------------------------',
850
+ '',
851
+ ` # Step ${stepNumber++}`,
852
+ ' - name: ๐Ÿงฏ Repair pending releases',
853
+ ' run:',
854
+ ` smoo release repair-pending --ref "${githubExpression(
855
+ 'github.sha',
856
+ )}" --platform-outputs "${githubExpression(
857
+ 'runner.temp',
858
+ )}/publish-artifacts/publish-macos-outputs-${githubExpression('github.run_id')}/repairs" --dry-run`,
859
+ ` "${githubExpression('inputs.dry_run')}"`,
860
+ '',
861
+ ` # Step ${stepNumber++}`,
862
+ ' - name: โ™ป๏ธ Restore validated release state',
863
+ ' run:',
864
+ ` git fetch "${githubExpression(
865
+ 'runner.temp',
866
+ )}/publish-artifacts/publish-release-state-${githubExpression('github.run_id')}/release-state.bundle" HEAD --tags && git reset`,
867
+ ` --hard "$(cat "${githubExpression(
868
+ 'runner.temp',
869
+ )}/publish-artifacts/publish-release-state-${githubExpression('github.run_id')}/release-head")"`,
870
+ );
871
+ lines.push(
872
+ '',
873
+ ' # --- Validation ---------------------------------------------------------',
874
+ '',
875
+ ` # Step ${stepNumber++}`,
876
+ ' - name: ๐Ÿ“ฆ Apply verified Linux outputs',
877
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
878
+ ' run:',
879
+ ` smoo github-ci apply-outputs --source-sha "${githubExpression(
880
+ 'needs.linux-release-candidate.outputs.release-sha',
881
+ )}"`,
882
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-release-outputs-${githubExpression(
883
+ 'github.run_id',
884
+ )}"`,
885
+ );
886
+ if (hasLinuxPlatformTargets(options)) {
887
+ lines.push(
888
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-linux-outputs-${githubExpression(
889
+ 'github.run_id',
890
+ )}"`,
891
+ );
892
+ }
893
+ lines.push(
894
+ '',
895
+ ` # Step ${stepNumber++}`,
896
+ ' - name: ๐ŸŽ Apply verified macOS outputs',
897
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
898
+ ' run:',
899
+ ` smoo github-ci apply-outputs --source-sha "${githubExpression('github.sha')}"`,
900
+ ` "${githubExpression('runner.temp')}/publish-artifacts/publish-macos-outputs-${githubExpression(
901
+ 'github.run_id',
902
+ )}/current"`,
903
+ '',
904
+ ` # Step ${stepNumber++}`,
905
+ ` - name: โœ… Validate restored release (${githubExpression(mode)})`,
906
+ ` if: ${githubExpression(`${mode} != 'none'`)}`,
907
+ ' run: smoo monorepo validate',
908
+ '',
909
+ ' # --- Release ------------------------------------------------------------',
910
+ '',
911
+ ` # Step ${stepNumber++}`,
912
+ ` - name: ๐Ÿ“ฆ Publish release (${githubExpression(mode)})`,
913
+ ' # smoo packs with Bun, then publishes tarballs with npm. Existing',
914
+ ' # packages must already exist on npm and use trusted publishing/OIDC.',
915
+ ' # Missing package names are bootstrapped locally before trust setup.',
916
+ ` run: smoo release publish --bump "${githubExpression('inputs.bump')}" --dry-run "${githubExpression(
917
+ 'inputs.dry_run',
918
+ )}"`,
919
+ );
920
+ if (options.deploy === true) {
921
+ lines.push(
922
+ '',
923
+ ` # Step ${stepNumber++}`,
924
+ ' - name: ๐Ÿš€ Deploy production',
925
+ ' if:',
926
+ " ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_environment == 'production' &&",
927
+ " inputs.dry_run != 'true' }}",
928
+ ...deployEnvLines(options),
929
+ ' run: smoo github-ci nx-deploy --configuration production --mode run-many --verify --name "Deploy Production"',
930
+ );
931
+ }
932
+ lines.push(
933
+ '',
934
+ ' # --- Cleanup ------------------------------------------------------------',
935
+ '',
936
+ ` # Step ${stepNumber}`,
937
+ ' - name: ๐Ÿงน Cleanup and cache Nix/devenv',
938
+ ` if: ${githubExpression('always()')}`,
939
+ ' uses: ./.github/actions/save-nix-devenv',
940
+ ' with:',
941
+ ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
942
+ ` devenv-cache-hit: ${githubExpression('steps.setup.outputs.devenv-cache-hit')}`,
943
+ );
944
+ return lines.join('\n').trimEnd();
945
+ }
946
+
947
+ function hasMacosPlatformTargets(options: PublishWorkflowDefinitionOptions): boolean {
948
+ return MACOS_PLATFORM_TARGET_GLOBS.some((glob) => options.platformTargetGlobs?.includes(glob) === true);
949
+ }
950
+
951
+ function hasLinuxPlatformTargets(options: PublishWorkflowDefinitionOptions): boolean {
952
+ return LINUX_PLATFORM_TARGET_GLOBS.some((glob) => options.platformTargetGlobs?.includes(glob) === true);
953
+ }
954
+
476
955
  function githubExpression(expression: string): string {
477
956
  return ['$', '{{ ', expression, ' }}'].join('');
478
957
  }