@smoothbricks/cli 0.10.9 → 0.10.10

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 (39) hide show
  1. package/dist/cli.js +6 -5
  2. package/dist/github-ci/index.d.ts +8 -6
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +35 -35
  5. package/dist/monorepo/ci-workflow.d.ts +3 -0
  6. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  7. package/dist/monorepo/ci-workflow.js +59 -15
  8. package/dist/monorepo/managed-files.d.ts +1 -0
  9. package/dist/monorepo/managed-files.d.ts.map +1 -1
  10. package/dist/monorepo/managed-files.js +15 -5
  11. package/dist/monorepo/publish-workflow.d.ts +2 -2
  12. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  13. package/dist/monorepo/publish-workflow.js +6 -6
  14. package/dist/wrangler/cloudflare.d.ts +1 -1
  15. package/dist/wrangler/cloudflare.d.ts.map +1 -1
  16. package/dist/wrangler/{deploy-environment.d.ts → deploy-stage.d.ts} +6 -6
  17. package/dist/wrangler/deploy-stage.d.ts.map +1 -0
  18. package/dist/wrangler/{deploy-environment.js → deploy-stage.js} +26 -26
  19. package/dist/wrangler/stage.d.ts +58 -0
  20. package/dist/wrangler/stage.d.ts.map +1 -0
  21. package/dist/wrangler/{environment.js → stage.js} +44 -44
  22. package/package.json +7 -7
  23. package/src/cli.ts +9 -7
  24. package/src/github-ci/index.test.ts +101 -32
  25. package/src/github-ci/index.ts +51 -45
  26. package/src/monorepo/__tests__/ci-workflow.test.ts +87 -49
  27. package/src/monorepo/__tests__/publish-workflow.test.ts +8 -8
  28. package/src/monorepo/ci-workflow.ts +67 -14
  29. package/src/monorepo/managed-files.test.ts +26 -5
  30. package/src/monorepo/managed-files.ts +18 -5
  31. package/src/monorepo/publish-workflow.ts +9 -9
  32. package/src/wrangler/cloudflare.ts +1 -1
  33. package/src/wrangler/{deploy-environment.test.ts → deploy-stage.test.ts} +11 -11
  34. package/src/wrangler/{deploy-environment.ts → deploy-stage.ts} +41 -41
  35. package/src/wrangler/{environment.test.ts → stage.test.ts} +15 -15
  36. package/src/wrangler/{environment.ts → stage.ts} +49 -52
  37. package/dist/wrangler/deploy-environment.d.ts.map +0 -1
  38. package/dist/wrangler/environment.d.ts +0 -58
  39. package/dist/wrangler/environment.d.ts.map +0 -1
@@ -14,8 +14,8 @@ import {
14
14
  nxSmartArgs,
15
15
  publishGithubDeployment,
16
16
  readGitHeadSha,
17
- resolveDeploymentEnvironment,
18
- selectEnvironmentDeployProjects,
17
+ resolveDeploymentStage,
18
+ selectStageDeployProjects,
19
19
  } from './index.js';
20
20
  import {
21
21
  applyCollectedOutputs,
@@ -134,12 +134,13 @@ describe('GitHub CI Nx target expansion', () => {
134
134
  ).toEqual(['native:package-linux', 'native:compile-linux', 'native:build']);
135
135
  });
136
136
 
137
- it('adds the generic target skip tag only to nx-smart', () => {
138
- expect(nxSmartArgs('test', 'affected')).toEqual([
139
- 'affected',
137
+ it('adds optional stage before the generic target skip tag only to nx-smart', () => {
138
+ expect(nxSmartArgs('e2e-deployment', 'run-many', undefined, 'pr123')).toEqual([
139
+ 'run-many',
140
140
  '-t',
141
- 'test',
142
- '--exclude=tag:ci:skip:test',
141
+ 'e2e-deployment',
142
+ '--stage=pr123',
143
+ '--exclude=tag:ci:skip:e2e-deployment',
143
144
  '--parallel=100%',
144
145
  ]);
145
146
  expect(nxRunManyArgs({ target: 'test', projects: projects.slice(0, 1) })).not.toContain(
@@ -540,10 +541,10 @@ async function withNxRunManyFixture(
540
541
  }
541
542
  }
542
543
 
543
- describe('event-aware environment deployment', () => {
544
- it('resolves same-repository PR, private push, release, and explicit production environments', () => {
544
+ describe('event-aware stage deployment', () => {
545
+ it('resolves same-repository PR, private push, release, and explicit production stages', () => {
545
546
  expect(
546
- resolveDeploymentEnvironment(
547
+ resolveDeploymentStage(
547
548
  undefined,
548
549
  { GITHUB_EVENT_NAME: 'pull_request' },
549
550
  {
@@ -554,12 +555,12 @@ describe('event-aware environment deployment', () => {
554
555
  ),
555
556
  ).toBe('pr123');
556
557
  expect(
557
- resolveDeploymentEnvironment(undefined, { GITHUB_EVENT_NAME: 'push', GITHUB_REF_NAME: 'private' }, undefined),
558
+ resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'push', GITHUB_REF_NAME: 'private' }, undefined),
558
559
  ).toBe('staging');
559
- expect(resolveDeploymentEnvironment(undefined, { GITHUB_EVENT_NAME: 'release' }, undefined)).toBe('production');
560
- expect(resolveDeploymentEnvironment('production', {}, undefined)).toBe('production');
560
+ expect(resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'release' }, undefined)).toBe('production');
561
+ expect(resolveDeploymentStage('production', {}, undefined)).toBe('production');
561
562
  expect(() =>
562
- resolveDeploymentEnvironment(
563
+ resolveDeploymentStage(
563
564
  undefined,
564
565
  { GITHUB_EVENT_NAME: 'pull_request' },
565
566
  {
@@ -571,15 +572,15 @@ describe('event-aware environment deployment', () => {
571
572
  ).toThrow(/same-repository/);
572
573
  });
573
574
 
574
- it('selects only deploy targets owned by the environment convention', async () => {
575
+ it('selects only deploy targets owned by the stage convention', async () => {
575
576
  const definitions: Record<string, unknown> = {
576
577
  'conloca-app': {
577
578
  targets: {
578
- deploy: { options: { command: 'smoo wrangler deploy-environment --environment {args.environment}' } },
579
+ deploy: { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
579
580
  },
580
581
  },
581
582
  'conloca-app-backend': {
582
- targets: { deploy: { command: 'smoo wrangler deploy-environment --environment {args.environment}' } },
583
+ targets: { deploy: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
583
584
  },
584
585
  'conloca-oauth-redirect': {
585
586
  targets: { deploy: { options: { command: 'wrangler deploy --config wrangler.toml' } } },
@@ -590,7 +591,7 @@ describe('event-aware environment deployment', () => {
590
591
  };
591
592
 
592
593
  await expect(
593
- selectEnvironmentDeployProjects(Object.keys(definitions), async (project) => definitions[project]),
594
+ selectStageDeployProjects(Object.keys(definitions), async (project) => definitions[project]),
594
595
  ).resolves.toEqual(['conloca-app', 'conloca-app-backend']);
595
596
  });
596
597
 
@@ -654,19 +655,21 @@ describe('event-aware environment deployment', () => {
654
655
  ]);
655
656
  });
656
657
 
657
- it('deploys app/backend, follows with e2e-deployed, and publishes PR metadata', async () => {
658
+ it('deploys app/backend, publishes PR metadata, and emits the resolved stage', async () => {
658
659
  const nxCalls: string[][] = [];
659
660
  const listCalls: Array<[string, string]> = [];
660
661
  const summaries: string[] = [];
661
662
  const deployments: Array<[string, string]> = [];
663
+ const outputs: string[] = [];
662
664
 
663
665
  await githubCiNxDeploy(
664
666
  '/repo',
665
- { mode: 'run-many', name: 'Deploy Environment' },
667
+ { mode: 'run-many', name: 'Deploy Stage' },
666
668
  {
667
669
  processEnv: {
668
670
  GITHUB_EVENT_NAME: 'pull_request',
669
671
  GITHUB_STEP_SUMMARY: '/summary',
672
+ GITHUB_OUTPUT: '/output',
670
673
  },
671
674
  setStatus: async () => {},
672
675
  eventPayload: {
@@ -676,7 +679,7 @@ describe('event-aware environment deployment', () => {
676
679
  },
677
680
  listProjects: async (_root, target, mode) => {
678
681
  listCalls.push([target, mode]);
679
- return target === 'deploy' ? ['conloca-app', 'conloca-app-backend'] : ['conloca-e2e'];
682
+ return ['conloca-app', 'conloca-app-backend'];
680
683
  },
681
684
  runNx: async (args) => {
682
685
  nxCalls.push(args);
@@ -685,24 +688,90 @@ describe('event-aware environment deployment', () => {
685
688
  appendSummary: async (_path, content) => {
686
689
  summaries.push(content);
687
690
  },
688
- publishDeployment: async (environment, url) => {
689
- deployments.push([environment, url]);
691
+ appendOutput: async (_path, content) => {
692
+ outputs.push(content);
693
+ },
694
+ publishDeployment: async (stage, url) => {
695
+ deployments.push([stage, url]);
690
696
  },
691
697
  },
692
698
  );
693
699
 
694
- expect(listCalls).toEqual([
695
- ['deploy', 'run-many'],
696
- ['e2e-deployed', 'run-many'],
697
- ]);
698
- expect(nxCalls).toHaveLength(2);
700
+ expect(listCalls).toEqual([['deploy', 'run-many']]);
701
+ expect(nxCalls).toHaveLength(1);
699
702
  expect(nxCalls[0]).toContain('--projects=conloca-app,conloca-app-backend');
700
703
  expect(nxCalls[0]).toContain('--exclude=tag:permanent-deploy-target');
701
- expect(nxCalls[0]).toContain('--environment=pr123');
702
- expect(nxCalls[1]).toContain('-t');
703
- expect(nxCalls[1]).toContain('e2e-deployed');
704
- expect(nxCalls[1]).toContain('--environment=pr123');
704
+ expect(nxCalls[0]).toContain('--stage=pr123');
705
+ expect(nxCalls[0]).not.toContain('e2e-deployment');
705
706
  expect(summaries).toEqual(['## pr123 deployment\n\n[View deployment](https://app.pr123.conloca.com)\n']);
706
707
  expect(deployments).toEqual([['pr123', 'https://app.pr123.conloca.com']]);
708
+ expect(outputs).toEqual(['stage=pr123\n']);
709
+ });
710
+
711
+ it('emits no stage when no deploy project exists', async () => {
712
+ const outputs: string[] = [];
713
+ const statuses: string[] = [];
714
+
715
+ await githubCiNxDeploy(
716
+ '/repo',
717
+ { stage: 'staging' },
718
+ {
719
+ processEnv: { GITHUB_OUTPUT: '/output' },
720
+ listProjects: async () => [],
721
+ appendOutput: async (_path, content) => {
722
+ outputs.push(content);
723
+ },
724
+ setStatus: async (status) => {
725
+ statuses.push(status);
726
+ },
727
+ },
728
+ );
729
+
730
+ expect(outputs).toEqual([]);
731
+ expect(statuses).toEqual(['pending', 'success']);
732
+ });
733
+
734
+ it('emits no stage after deployment or output failure', async () => {
735
+ const deploymentOutputs: string[] = [];
736
+ const deploymentStatuses: string[] = [];
737
+ await expect(
738
+ githubCiNxDeploy(
739
+ '/repo',
740
+ { stage: 'staging' },
741
+ {
742
+ processEnv: { GITHUB_OUTPUT: '/output' },
743
+ listProjects: async () => ['app'],
744
+ runNx: async () => 1,
745
+ appendOutput: async (_path, content) => {
746
+ deploymentOutputs.push(content);
747
+ },
748
+ setStatus: async (status) => {
749
+ deploymentStatuses.push(status);
750
+ },
751
+ },
752
+ ),
753
+ ).rejects.toThrow(/failed with exit code 1/);
754
+ expect(deploymentOutputs).toEqual([]);
755
+ expect(deploymentStatuses).toEqual(['pending', 'failure']);
756
+
757
+ const outputStatuses: string[] = [];
758
+ await expect(
759
+ githubCiNxDeploy(
760
+ '/repo',
761
+ { stage: 'staging' },
762
+ {
763
+ processEnv: { GITHUB_OUTPUT: '/output' },
764
+ listProjects: async () => ['app'],
765
+ runNx: async () => 0,
766
+ appendOutput: async () => {
767
+ throw new Error('output unavailable');
768
+ },
769
+ setStatus: async (status) => {
770
+ outputStatuses.push(status);
771
+ },
772
+ },
773
+ ),
774
+ ).rejects.toThrow('output unavailable');
775
+ expect(outputStatuses).toEqual(['pending', 'failure']);
707
776
  });
708
777
  });
@@ -7,12 +7,7 @@ import typia from 'typia';
7
7
  import { parseStringArrayText } from '../lib/json.js';
8
8
  import { decode, run, runStatus } from '../lib/run.js';
9
9
  import { type ProjectTargets, readProjectTargets } from '../nx/index.js';
10
- import {
11
- type EnvironmentToken,
12
- isPullRequestEnvironment,
13
- parseEnvironmentToken,
14
- pullRequestEnvironment,
15
- } from '../wrangler/environment.js';
10
+ import { type DeploymentStage, isPullRequestStage, parseDeploymentStage, pullRequestStage } from '../wrangler/stage.js';
16
11
  import type { NxTargetRun } from './outputs.js';
17
12
 
18
13
  export interface GithubActionsEventPayload {
@@ -159,24 +154,32 @@ async function addReferencesFrom(roots: Set<string>, path: string, cwd: string):
159
154
  }
160
155
  }
161
156
 
162
- export function nxSmartArgs(target: string, mode: 'affected' | 'run-many', configuration?: string): string[] {
157
+ export function nxSmartArgs(
158
+ target: string,
159
+ mode: 'affected' | 'run-many',
160
+ configuration?: string,
161
+ stage?: string,
162
+ ): string[] {
163
163
  const args = [mode, '-t', target];
164
164
  if (configuration) {
165
165
  args.push(`--configuration=${configuration}`);
166
166
  }
167
+ if (stage) {
168
+ args.push(`--stage=${stage}`);
169
+ }
167
170
  args.push(`--exclude=tag:ci:skip:${target}`, `--parallel=${NX_PARALLEL}`);
168
171
  return args;
169
172
  }
170
173
 
171
174
  export async function githubCiNxSmart(
172
175
  root: string,
173
- options: { target: string; name?: string; step?: string; mode?: NxSmartMode; configuration?: string },
176
+ options: { target: string; name?: string; step?: string; mode?: NxSmartMode; configuration?: string; stage?: string },
174
177
  ): Promise<void> {
175
178
  const name = options.name ?? options.target;
176
179
  const step = options.step ?? '';
177
180
  await createGithubStatus(name, step);
178
181
  const mode = resolveNxSmartMode(options.mode ?? 'auto');
179
- const nxArgs = nxSmartArgs(options.target, mode, options.configuration);
182
+ const nxArgs = nxSmartArgs(options.target, mode, options.configuration, options.stage);
180
183
  const status = await runStatus('nx', nxArgs, root);
181
184
  await updateGithubStatus(name, status === 0 ? 'success' : 'failure', step);
182
185
  if (status !== 0) {
@@ -381,7 +384,7 @@ function commaSeparatedValues(value: string): string[] {
381
384
  }
382
385
 
383
386
  export interface GithubCiNxDeployOptions {
384
- environment?: string;
387
+ stage?: string;
385
388
  mode?: NxSmartMode;
386
389
  name?: string;
387
390
  step?: string;
@@ -392,7 +395,8 @@ export interface GithubCiNxDeployDependencies {
392
395
  listProjects?: (root: string, target: string, mode: 'affected' | 'run-many') => Promise<string[]>;
393
396
  runNx?: (args: string[], root: string) => Promise<number>;
394
397
  appendSummary?: (summaryPath: string, content: string) => Promise<void>;
395
- publishDeployment?: (environment: `pr${number}`, url: string) => Promise<void>;
398
+ appendOutput?: (outputPath: string, content: string) => Promise<void>;
399
+ publishDeployment?: (stage: `pr${number}`, url: string) => Promise<void>;
396
400
  setStatus?: (state: 'pending' | 'success' | 'failure') => Promise<void>;
397
401
  processEnv?: NodeJS.ProcessEnv;
398
402
  eventPayload?: GithubActionsEventPayload;
@@ -405,8 +409,8 @@ export async function githubCiNxDeploy(
405
409
  ): Promise<void> {
406
410
  const processEnv = dependencies.processEnv ?? process.env;
407
411
  const eventPayload = dependencies.eventPayload ?? readGithubActionsEvent(processEnv);
408
- const environment = resolveDeploymentEnvironment(options.environment, processEnv, eventPayload);
409
- const name = options.name ?? 'Deploy Environment';
412
+ const stage = resolveDeploymentStage(options.stage, processEnv, eventPayload);
413
+ const name = options.name ?? 'Deploy Stage';
410
414
  const step = options.step ?? '';
411
415
  const setStatus =
412
416
  dependencies.setStatus ??
@@ -418,7 +422,7 @@ export async function githubCiNxDeploy(
418
422
  const runNx = dependencies.runNx ?? ((args: string[], commandRoot: string) => runStatus('nx', args, commandRoot));
419
423
  const projects = await listProjects(root, 'deploy', mode);
420
424
  if (projects.length === 0) {
421
- console.log(`No ${mode} deploy projects; skipping ${environment}.`);
425
+ console.log(`No ${mode} deploy projects; skipping ${stage}.`);
422
426
  await setStatus('success');
423
427
  return;
424
428
  }
@@ -435,7 +439,7 @@ export async function githubCiNxDeploy(
435
439
  `--parallel=${NX_PARALLEL}`,
436
440
  ];
437
441
  if (target === 'deploy') {
438
- nxArgs.push(`--environment=${environment}`);
442
+ nxArgs.push(`--stage=${stage}`);
439
443
  }
440
444
  const status = await runNx(nxArgs, root);
441
445
  if (status !== 0) {
@@ -444,37 +448,39 @@ export async function githubCiNxDeploy(
444
448
  }
445
449
  }
446
450
 
447
- if (isPullRequestEnvironment(environment)) {
448
- const url = `https://app.${environment}.conloca.com`;
451
+ if (isPullRequestStage(stage)) {
452
+ const url = `https://app.${stage}.conloca.com`;
449
453
  const summaryPath = processEnv.GITHUB_STEP_SUMMARY;
450
454
  if (summaryPath) {
451
455
  const appendSummary = dependencies.appendSummary ?? appendFile;
452
- await appendSummary(summaryPath, `## ${environment} deployment\n\n[View deployment](${url})\n`);
456
+ await appendSummary(
457
+ summaryPath,
458
+ `## ${stage} deployment
459
+
460
+ [View deployment](${url})
461
+ `,
462
+ );
453
463
  }
454
464
  const publishDeployment =
455
465
  dependencies.publishDeployment ??
456
466
  ((token: `pr${number}`, deploymentUrl: string) => publishGithubDeployment(token, deploymentUrl, processEnv));
457
- await publishDeployment(environment, url);
458
- }
459
- if (environment !== 'production') {
460
- const e2eProjects = await listProjects(root, 'e2e-deployed', 'run-many');
461
- if (e2eProjects.length > 0) {
462
- const nxArgs = [
463
- 'run-many',
464
- '-t',
465
- 'e2e-deployed',
466
- `--projects=${e2eProjects.join(',')}`,
467
- `--parallel=${NX_PARALLEL}`,
468
- `--environment=${environment}`,
469
- ];
470
- const status = await runNx(nxArgs, root);
471
- if (status !== 0) {
472
- await setStatus('failure');
473
- throw new Error(`nx ${nxArgs.join(' ')} failed with exit code ${status}`);
474
- }
467
+ await publishDeployment(stage, url);
468
+ }
469
+
470
+ const outputPath = processEnv.GITHUB_OUTPUT;
471
+ if (outputPath) {
472
+ try {
473
+ const appendOutput = dependencies.appendOutput ?? appendFile;
474
+ await appendOutput(
475
+ outputPath,
476
+ `stage=${stage}
477
+ `,
478
+ );
479
+ } catch (error) {
480
+ await setStatus('failure');
481
+ throw error;
475
482
  }
476
483
  }
477
-
478
484
  await setStatus('success');
479
485
  }
480
486
 
@@ -509,12 +515,12 @@ function eventDefaultBranch(): string | undefined {
509
515
  }
510
516
  }
511
517
 
512
- export function resolveDeploymentEnvironment(
518
+ export function resolveDeploymentStage(
513
519
  explicit: string | undefined,
514
520
  environment: NodeJS.ProcessEnv,
515
521
  event: GithubActionsEventPayload | undefined,
516
- ): EnvironmentToken {
517
- if (explicit !== undefined) return parseEnvironmentToken(explicit);
522
+ ): DeploymentStage {
523
+ if (explicit !== undefined) return parseDeploymentStage(explicit);
518
524
  if (environment.GITHUB_EVENT_NAME === 'pull_request') {
519
525
  if (!event?.action || !['opened', 'reopened', 'synchronize'].includes(event.action)) {
520
526
  throw new Error('Pull-request deployment runs only for opened, reopened, or synchronize events.');
@@ -526,7 +532,7 @@ export function resolveDeploymentEnvironment(
526
532
  }
527
533
  const number = event.pull_request?.number;
528
534
  if (number === undefined) throw new Error('GitHub pull_request event is missing its PR number.');
529
- return pullRequestEnvironment(number);
535
+ return pullRequestStage(number);
530
536
  }
531
537
  if (environment.GITHUB_EVENT_NAME === 'push' && environment.GITHUB_REF_NAME === 'private') {
532
538
  return 'staging';
@@ -534,7 +540,7 @@ export function resolveDeploymentEnvironment(
534
540
  if (environment.GITHUB_EVENT_NAME === 'release') {
535
541
  return 'production';
536
542
  }
537
- throw new Error('Cannot resolve a deployment environment from this GitHub event; pass --environment explicitly.');
543
+ throw new Error('Cannot resolve a deployment stage from this GitHub event; pass --stage explicitly.');
538
544
  }
539
545
 
540
546
  function readGithubActionsEvent(environment: NodeJS.ProcessEnv): GithubActionsEventPayload | undefined {
@@ -560,7 +566,7 @@ async function listNxProjectsWithTarget(
560
566
  const result = await $`nx ${listArgs}`.cwd(root).quiet();
561
567
  const candidates = nxProjectList(decode(result.stdout)).sort((left, right) => left.localeCompare(right));
562
568
  if (target !== 'deploy') return candidates;
563
- return selectEnvironmentDeployProjects(candidates, async (project) => {
569
+ return selectStageDeployProjects(candidates, async (project) => {
564
570
  const projectResult = await $`nx show project ${project} --json`.cwd(root).quiet();
565
571
  const parsed = parseNxProjectDeployTarget(decode(projectResult.stdout));
566
572
  if (!parsed) throw new Error(`nx show project ${project} returned invalid JSON.`);
@@ -568,7 +574,7 @@ async function listNxProjectsWithTarget(
568
574
  });
569
575
  }
570
576
 
571
- export async function selectEnvironmentDeployProjects(
577
+ export async function selectStageDeployProjects(
572
578
  candidates: string[],
573
579
  loadProject: (project: string) => Promise<unknown>,
574
580
  ): Promise<string[]> {
@@ -578,7 +584,7 @@ export async function selectEnvironmentDeployProjects(
578
584
  if (!isNxProjectDeployTarget(definition)) continue;
579
585
  const deploy = definition.targets?.deploy;
580
586
  const commandValue = deploy?.options?.command ?? deploy?.command;
581
- if (typeof commandValue === 'string' && commandValue.includes('smoo wrangler deploy-environment')) {
587
+ if (typeof commandValue === 'string' && commandValue.includes('smoo wrangler deploy-stage')) {
582
588
  selected.push(project);
583
589
  }
584
590
  }
@@ -3,58 +3,109 @@
3
3
  import { describe, expect, it } from 'bun:test';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
- import { CiWorkflowStepKind, defineCiWorkflow, renderCiWorkflowYaml } from '../ci-workflow.js';
6
+ import {
7
+ type CiWorkflowDefinitionOptions,
8
+ CiWorkflowStepKind,
9
+ defineCiWorkflow,
10
+ renderCiWorkflowYaml,
11
+ } from '../ci-workflow.js';
7
12
 
8
13
  const nixosRunsOn = ['nixos-latest-x64', 'self-hosted'] as const;
9
14
 
15
+ function options(overrides: Partial<CiWorkflowDefinitionOptions> = {}): CiWorkflowDefinitionOptions {
16
+ return {
17
+ deploy: false,
18
+ browserTests: false,
19
+ e2eDeployment: false,
20
+ pushBranches: ['main'],
21
+ ...overrides,
22
+ };
23
+ }
24
+
10
25
  describe('CI workflow definition', () => {
11
26
  it('renders the checked-in local CI workflow copy', async () => {
12
- const rendered = renderCiWorkflowYaml({
13
- deploy: false,
14
- pushBranches: ['main'],
15
- runsOn: [...nixosRunsOn],
16
- });
27
+ const rendered = renderCiWorkflowYaml(options({ runsOn: [...nixosRunsOn] }));
17
28
  const packageRoot = join(import.meta.dir, '..', '..', '..');
18
29
 
19
30
  await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/ci.yml'), 'utf8')).resolves.toBe(rendered);
20
31
  });
21
32
 
22
- it('inserts deploy after tests and renumbers following deeplink steps', () => {
23
- const steps = defineCiWorkflow({ deploy: true, pushBranches: ['main'] });
24
- const rendered = renderCiWorkflowYaml({ deploy: true, pushBranches: ['main'] });
33
+ it('deploys immediately after build and renumbers following steps', () => {
34
+ const definition = options({ deploy: true, browserTests: true });
35
+ const steps = defineCiWorkflow(definition);
36
+ const rendered = renderCiWorkflowYaml(definition);
25
37
 
26
- expect(steps.map((step) => [step.kind, step.number])).toContainEqual([CiWorkflowStepKind.Deploy, 11]);
27
- expect(rendered.match(/- name: 🚀 Deploy Environment/g)).toHaveLength(1);
28
- expect(rendered).not.toContain('CLOUDFLARE_API_TOKEN');
29
- expect(rendered).not.toContain('CLOUDFLARE_ACCOUNT_ID');
30
- expect(rendered).toContain('smoo github-ci nx-deploy --mode run-many --name "Deploy Environment" --step 11');
38
+ expect(steps.map((step) => [step.kind, step.number])).toEqual([
39
+ [CiWorkflowStepKind.Checkout, 2],
40
+ [CiWorkflowStepKind.SetupDevenv, 3],
41
+ [CiWorkflowStepKind.SetNxShas, 4],
42
+ [CiWorkflowStepKind.RestoreNxCache, 5],
43
+ [CiWorkflowStepKind.Build, 6],
44
+ [CiWorkflowStepKind.Deploy, 7],
45
+ [CiWorkflowStepKind.Lint, 8],
46
+ [CiWorkflowStepKind.UnitTests, 9],
47
+ [CiWorkflowStepKind.BrowserTests, 10],
48
+ [CiWorkflowStepKind.ManagedFilesCheck, 11],
49
+ [CiWorkflowStepKind.ManagedFilesDispatch, 12],
50
+ [CiWorkflowStepKind.SaveNxCache, 13],
51
+ [CiWorkflowStepKind.UploadTraceDbs, 14],
52
+ [CiWorkflowStepKind.SaveNixDevenv, 15],
53
+ ]);
54
+ expect(rendered.match(/- name: 🚀 Deploy Stage/g)).toHaveLength(1);
55
+ expect(rendered).toContain('id: deploy');
56
+ expect(rendered).toContain('smoo github-ci nx-deploy --mode run-many --name "Deploy Stage" --step 7');
57
+ expect(rendered).toContain('smoo github-ci nx-smart --target test-browser --name "Browser Tests" --step 10');
58
+ expect(rendered).toContain('group: ${{ github.workflow }}-${{ github.ref }}');
59
+ expect(rendered).toContain('cancel-in-progress: true');
31
60
  expect(rendered).toContain('github.event.pull_request.head.repo.full_name == github.repository');
32
61
  expect(rendered).toContain("github.ref == 'refs/heads/private'");
33
- expect(rendered).toContain("# Step 12\n # Nx's database cache needs artifact files");
34
- expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
35
- expect(rendered).toContain('id: setup');
36
- expect(rendered).not.toContain('Setup Nix/devenv (fork)');
37
- expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
38
- expect(rendered).not.toContain('github-actions-bootstrap.sh');
39
- expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
40
- expect(rendered).toContain('runs-on: ubuntu-latest');
62
+ expect(rendered).toContain("# Step 13\n # Nx's database cache needs artifact files");
41
63
  });
42
64
 
43
- it('adds Cloudflare credentials for Wrangler-backed deploys', () => {
44
- const rendered = renderCiWorkflowYaml({ deploy: true, deployProvider: 'cloudflare', pushBranches: ['main'] });
65
+ it('adds only generic Cloudflare credentials for Wrangler-backed deploys', () => {
66
+ const rendered = renderCiWorkflowYaml(options({ deploy: true, deployProvider: 'cloudflare' }));
45
67
 
46
68
  expect(rendered).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
47
69
  expect(rendered).toContain('CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}');
48
- expect(rendered).toContain('GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}');
49
- expect(rendered).toContain('GITHUB_APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}');
50
- expect(rendered).toContain('GITHUB_APP_PRIVATE_KEY_PEM: ${{ secrets.GITHUB_APP_PRIVATE_KEY_PEM }}');
51
- expect(rendered).toContain('OAUTH_STATE_SIGNING_KEY: ${{ secrets.OAUTH_STATE_SIGNING_KEY }}');
52
- expect(rendered).toContain('MAIL_CAPTURE_CONTROL_TOKEN: ${{ secrets.MAIL_CAPTURE_CONTROL_TOKEN }}');
53
- expect(rendered).toContain('TOKEN_ENCRYPTION_KEY: ${{ secrets.TOKEN_ENCRYPTION_KEY }}');
70
+ for (const appSecret of [
71
+ 'GITHUB_CLIENT_SECRET',
72
+ 'GITHUB_APP_PRIVATE_KEY',
73
+ 'GITHUB_APP_PRIVATE_KEY_PEM',
74
+ 'OAUTH_STATE_SIGNING_KEY',
75
+ 'MAIL_CAPTURE_CONTROL_TOKEN',
76
+ 'TOKEN_ENCRYPTION_KEY',
77
+ 'E2E_CONTROL_TOKEN',
78
+ ]) {
79
+ expect(rendered).not.toContain(appSecret);
80
+ }
81
+ });
82
+
83
+ it('renders deployment E2E as a dependent job with an independent stage input', () => {
84
+ const rendered = renderCiWorkflowYaml(options({ deploy: true, e2eDeployment: true, runsOn: [...nixosRunsOn] }));
85
+
86
+ expect(rendered).toContain('deployment-stage: ${{ steps.deploy.outputs.stage }}');
87
+ expect(rendered).toContain(' e2e-deployment:\n name: Deployment E2E\n needs: main');
88
+ expect(rendered).toContain(
89
+ "if: ${{ needs.main.result == 'success' && needs.main.outputs.deployment-stage != '' }}",
90
+ );
91
+ expect(rendered).toContain('timeout-minutes: 15');
92
+ expect(rendered).toContain(
93
+ 'smoo github-ci nx-smart --target e2e-deployment --mode run-many --stage "${{ needs.main.outputs.deployment-stage }}" --name "Deployment E2E" --step 4',
94
+ );
95
+ expect(rendered.match(/name: Deployment E2E/g)).toHaveLength(2);
96
+ expect(rendered).not.toContain('E2E_CONTROL_TOKEN');
97
+ });
98
+
99
+ it('omits optional browser and deployment-E2E lanes when disabled', () => {
100
+ const rendered = renderCiWorkflowYaml(options({ deploy: true }));
101
+
102
+ expect(rendered).not.toContain('--target test-browser');
103
+ expect(rendered).not.toContain(' e2e-deployment:');
104
+ expect(rendered).not.toContain('deployment-stage:');
54
105
  });
55
106
 
56
107
  it('uses the same architecture-scoped key to restore and save the Nx cache', async () => {
57
- const rendered = renderCiWorkflowYaml({ deploy: false, pushBranches: ['main'] });
108
+ const rendered = renderCiWorkflowYaml(options());
58
109
  const packageRoot = join(import.meta.dir, '..', '..', '..');
59
110
  const restoreAction = await readFile(join(packageRoot, '..', '..', '.github/actions/cache-nx/action.yml'), 'utf8');
60
111
  const restoreKey = restoreAction.match(/^\s*key: (.+)$/m)?.[1];
@@ -64,26 +115,13 @@ describe('CI workflow definition', () => {
64
115
  expect(saveKey).toBe(restoreKey);
65
116
  });
66
117
 
67
- it('nixos config: fork PRs on ubuntu; one setup-devenv (composite owns host path)', () => {
68
- const rendered = renderCiWorkflowYaml({
69
- deploy: false,
70
- pushBranches: ['main'],
71
- runsOn: [...nixosRunsOn],
72
- });
118
+ it('nixos config gates both jobs away from private runners for fork PRs', () => {
119
+ const rendered = renderCiWorkflowYaml(options({ deploy: true, e2eDeployment: true, runsOn: [...nixosRunsOn] }));
120
+ const runnerExpression =
121
+ "runs-on:\n ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&\n fromJSON('[\"nixos-latest-x64\",\"self-hosted\"]') || 'ubuntu-latest' }}";
73
122
 
74
- expect(rendered).toContain(
75
- "runs-on:\n ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&\n fromJSON('[\"nixos-latest-x64\",\"self-hosted\"]') || 'ubuntu-latest' }}",
76
- );
123
+ expect(rendered.match(new RegExp(runnerExpression.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'))).toHaveLength(2);
77
124
  expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
78
- expect(rendered).toContain('id: setup');
79
- expect(rendered).not.toContain('Setup Nix/devenv (fork)');
80
- expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
81
125
  expect(rendered).not.toContain('github-actions-bootstrap.sh');
82
- expect(rendered).not.toContain('determinate-nix-action');
83
- expect(rendered).toContain('if: always()');
84
- expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
85
- // workflow yaml does not embed composite internals
86
- expect(rendered).not.toContain('cache-node-modules');
87
- expect(rendered).not.toContain('cache-ttsc-plugins');
88
126
  });
89
127
  });