@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
@@ -232,7 +232,7 @@ describe('publish workflow definition', () => {
232
232
  );
233
233
  expect(rendered).not.toContain('GITHUB_SHA:');
234
234
  expect(finalJob).toContain("needs.linux-release-candidate.outputs.mode != 'none'");
235
- expect(finalJob).toContain("inputs.deploy_environment == 'production'");
235
+ expect(finalJob).toContain("inputs.deploy_stage == 'production'");
236
236
  expect(finalJob).toContain("inputs.dry_run != 'true'");
237
237
  expect(finalJob).toContain('smoo release publish --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"');
238
238
  expect(finalJob).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
@@ -250,7 +250,7 @@ describe('publish workflow definition', () => {
250
250
  it('omits production deploy controls when no production deploy target exists', () => {
251
251
  const rendered = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
252
252
 
253
- expect(rendered).not.toContain('deploy_environment');
253
+ expect(rendered).not.toContain('deploy_stage');
254
254
  expect(rendered).not.toContain('Deploy production');
255
255
  expect(rendered).not.toContain('nx-deploy');
256
256
  });
@@ -258,12 +258,12 @@ describe('publish workflow definition', () => {
258
258
  it('renders production deploy controls for repos with production deploy targets', () => {
259
259
  const rendered = renderPublishWorkflowYaml({ deploy: true, repoName: '@smoothbricks/codebase' });
260
260
 
261
- expect(rendered).toContain('deploy_environment:');
261
+ expect(rendered).toContain('deploy_stage:');
262
262
  expect(rendered).toContain('- name: 🚀 Deploy production');
263
263
  expect(rendered).not.toContain('CLOUDFLARE_API_TOKEN');
264
264
  expect(rendered).not.toContain('CLOUDFLARE_ACCOUNT_ID');
265
265
  expect(rendered).toContain(
266
- 'smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
266
+ 'smoo github-ci nx-deploy --stage production --mode run-many --verify --name "Deploy Production"',
267
267
  );
268
268
  });
269
269
 
@@ -284,7 +284,7 @@ describe('publish workflow definition', () => {
284
284
  repairs: [],
285
285
  current: [],
286
286
  bump: 'auto',
287
- deployEnvironment: 'production',
287
+ deployStage: 'production',
288
288
  dryRun: false,
289
289
  version: { mode: 'new', projects: ['app'] },
290
290
  }).run();
@@ -293,7 +293,7 @@ describe('publish workflow definition', () => {
293
293
  repairs: [],
294
294
  current: [],
295
295
  bump: 'auto',
296
- deployEnvironment: 'production',
296
+ deployStage: 'production',
297
297
  dryRun: true,
298
298
  version: { mode: 'new', projects: ['app'] },
299
299
  }).run();
@@ -422,7 +422,7 @@ interface WorkflowScenarioConfig {
422
422
  repairs: ReleaseGap[];
423
423
  current: ReleaseGap[];
424
424
  bump: PublishWorkflowBump;
425
- deployEnvironment?: 'none' | 'production';
425
+ deployStage?: 'none' | 'production';
426
426
  dryRun: boolean;
427
427
  version: PublishWorkflowVersionOutputs;
428
428
  }
@@ -448,7 +448,7 @@ function publishWorkflowScenario(config: WorkflowScenarioConfig): { run(): Promi
448
448
  async run() {
449
449
  const state = new WorkflowScenarioState(config);
450
450
  await runPublishWorkflow(definePublishWorkflow({ deploy: config.deploy, repoName: config.repoName }), {
451
- inputs: { bump: config.bump, deployEnvironment: config.deployEnvironment ?? 'none', dryRun: config.dryRun },
451
+ inputs: { bump: config.bump, deployStage: config.deployStage ?? 'none', dryRun: config.dryRun },
452
452
  callbacks: state.callbacks(),
453
453
  });
454
454
  return state.outcome();
@@ -8,6 +8,7 @@ export enum CiWorkflowStepKind {
8
8
  SetNxShas = 'set-nx-shas',
9
9
  RestoreNxCache = 'restore-nx-cache',
10
10
  Build = 'build',
11
+ BrowserTests = 'browser-tests',
11
12
  Lint = 'lint',
12
13
  UnitTests = 'unit-tests',
13
14
  ManagedFilesCheck = 'managed-files-check',
@@ -26,6 +27,8 @@ export interface CiWorkflowStep {
26
27
 
27
28
  export interface CiWorkflowDefinitionOptions {
28
29
  deploy: boolean;
30
+ browserTests: boolean;
31
+ e2eDeployment: boolean;
29
32
  deployProvider?: 'cloudflare';
30
33
  pushBranches: string[];
31
34
  /** Default ubuntu-latest when omitted. */
@@ -41,15 +44,20 @@ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkfl
41
44
  { kind: CiWorkflowStepKind.SetNxShas, name: '🧭 Set Nx SHAs' },
42
45
  { kind: CiWorkflowStepKind.RestoreNxCache, name: '🧠 Restore Nx cache' },
43
46
  { kind: CiWorkflowStepKind.Build, name: '🔨 Build' },
44
- { kind: CiWorkflowStepKind.Lint, name: '🔍 Lint' },
45
- { kind: CiWorkflowStepKind.UnitTests, name: '🧪 Unit Tests' },
46
- { kind: CiWorkflowStepKind.ManagedFilesCheck, name: '🩺 Check managed-file drift' },
47
- { kind: CiWorkflowStepKind.ManagedFilesDispatch, name: '🔁 Dispatch managed-file drift healing' },
48
47
  ];
49
48
  if (options.deploy) {
50
- steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy Environment' });
49
+ steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy Stage' });
51
50
  }
52
51
  steps.push(
52
+ { kind: CiWorkflowStepKind.Lint, name: '🔍 Lint' },
53
+ { kind: CiWorkflowStepKind.UnitTests, name: '🧪 Unit Tests' },
54
+ );
55
+ if (options.browserTests) {
56
+ steps.push({ kind: CiWorkflowStepKind.BrowserTests, name: '🌐 Browser Tests' });
57
+ }
58
+ steps.push(
59
+ { kind: CiWorkflowStepKind.ManagedFilesCheck, name: '🩺 Check managed-file drift' },
60
+ { kind: CiWorkflowStepKind.ManagedFilesDispatch, name: '🔁 Dispatch managed-file drift healing' },
53
61
  { kind: CiWorkflowStepKind.SaveNxCache, name: '💾 Save Nx cache' },
54
62
  { kind: CiWorkflowStepKind.UploadTraceDbs, name: '📎 Upload trace DBs' },
55
63
  { kind: CiWorkflowStepKind.SaveNixDevenv, name: '🧹 Cleanup and cache Nix/devenv' },
@@ -59,7 +67,7 @@ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkfl
59
67
 
60
68
  export function renderCiWorkflowYaml(options: CiWorkflowDefinitionOptions): string {
61
69
  const steps = defineCiWorkflow(options);
62
- return `${renderCiWorkflowHeader(options)}${renderCiWorkflowSteps(steps, options)}`;
70
+ return `${renderCiWorkflowHeader(options)}${renderCiWorkflowSteps(steps, options)}${renderE2eDeploymentJob(options)}`;
63
71
  }
64
72
 
65
73
  function renderCiWorkflowHeader(options: CiWorkflowDefinitionOptions): string {
@@ -77,6 +85,10 @@ permissions:
77
85
  contents: read
78
86
  ${options.deploy ? ' deployments: write\n' : ''} statuses: write
79
87
 
88
+ concurrency:
89
+ group: \${{ github.workflow }}-\${{ github.ref }}
90
+ cancel-in-progress: true
91
+
80
92
  defaults:
81
93
  run:
82
94
  working-directory: tooling/direnv
@@ -86,7 +98,13 @@ jobs:
86
98
  name: Validate
87
99
  ${renderRunsOnLine(options.runsOn)}
88
100
  timeout-minutes: 45
89
- env:
101
+ ${
102
+ options.e2eDeployment
103
+ ? ` outputs:
104
+ deployment-stage: ${githubExpression('steps.deploy.outputs.stage')}
105
+ `
106
+ : ''
107
+ } env:
90
108
  NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
91
109
  GH_TOKEN: ${githubExpression('github.token')}
92
110
  steps:
@@ -169,6 +187,8 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
169
187
  return [` - name: ${step.name}`, ' id: nx-cache', ' uses: ./.github/actions/cache-nx'];
170
188
  case CiWorkflowStepKind.Build:
171
189
  return nxSmartStep(step, 'build', 'Build');
190
+ case CiWorkflowStepKind.BrowserTests:
191
+ return nxSmartStep(step, 'test-browser', 'Browser Tests');
172
192
  case CiWorkflowStepKind.Lint:
173
193
  return nxSmartStep(step, 'lint', 'Lint');
174
194
  case CiWorkflowStepKind.UnitTests:
@@ -197,6 +217,7 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
197
217
  case CiWorkflowStepKind.Deploy:
198
218
  return [
199
219
  ` - name: ${step.name}`,
220
+ ' id: deploy',
200
221
  ' if: >-',
201
222
  ' ${{',
202
223
  " (github.event_name == 'pull_request' &&",
@@ -205,7 +226,7 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
205
226
  " (github.event_name == 'push' && github.ref == 'refs/heads/private')",
206
227
  ' }}',
207
228
  ...deployEnvLines(options),
208
- ` run: smoo github-ci nx-deploy --mode run-many --name "Deploy Environment" --step ${step.number}`,
229
+ ` run: smoo github-ci nx-deploy --mode run-many --name "Deploy Stage" --step ${step.number}`,
209
230
  ];
210
231
  case CiWorkflowStepKind.SaveNxCache:
211
232
  return [
@@ -255,12 +276,6 @@ function deployEnvLines(options: CiWorkflowDefinitionOptions): string[] {
255
276
  ' env:',
256
277
  ' CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}',
257
278
  ' CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
258
- ' GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}',
259
- ' GITHUB_APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}',
260
- ' GITHUB_APP_PRIVATE_KEY_PEM: ${{ secrets.GITHUB_APP_PRIVATE_KEY_PEM }}',
261
- ' OAUTH_STATE_SIGNING_KEY: ${{ secrets.OAUTH_STATE_SIGNING_KEY }}',
262
- ' MAIL_CAPTURE_CONTROL_TOKEN: ${{ secrets.MAIL_CAPTURE_CONTROL_TOKEN }}',
263
- ' TOKEN_ENCRYPTION_KEY: ${{ secrets.TOKEN_ENCRYPTION_KEY }}',
264
279
  ];
265
280
  }
266
281
 
@@ -275,3 +290,41 @@ function renderYamlList(values: string[], spaces: number): string {
275
290
  const indent = ' '.repeat(spaces);
276
291
  return values.map((value) => `${indent}- ${value}`).join('\n');
277
292
  }
293
+
294
+ function renderE2eDeploymentJob(options: CiWorkflowDefinitionOptions): string {
295
+ if (!options.e2eDeployment) return '';
296
+ return `
297
+
298
+ e2e-deployment:
299
+ name: Deployment E2E
300
+ needs: main
301
+ ${renderRunsOnLine(options.runsOn)}
302
+ timeout-minutes: 15
303
+ if: \${{ needs.main.result == 'success' && needs.main.outputs.deployment-stage != '' }}
304
+ steps:
305
+ # Step 1: GitHub adds "Set up job" automatically
306
+ # Step 2
307
+ - name: 📥 Checkout
308
+ uses: actions/checkout@v6.0.2
309
+ with:
310
+ filter: blob:none
311
+ fetch-depth: 0
312
+
313
+ # Step 3. Composite action internals do not affect top-level job step anchors.
314
+ - name: 🧱 Setup Nix/devenv
315
+ id: setup
316
+ uses: ./.github/actions/setup-devenv
317
+
318
+ # Step 4
319
+ - name: Deployment E2E
320
+ run: smoo github-ci nx-smart --target e2e-deployment --mode run-many --stage "\${{ needs.main.outputs.deployment-stage }}" --name "Deployment E2E" --step 4
321
+
322
+ # Step 5
323
+ - name: 🧹 Cleanup and cache Nix/devenv
324
+ if: always()
325
+ uses: ./.github/actions/save-nix-devenv
326
+ with:
327
+ nix-cache-hit: \${{ steps.setup.outputs.nix-cache-hit }}
328
+ devenv-cache-hit: \${{ steps.setup.outputs.devenv-cache-hit }}
329
+ `;
330
+ }
@@ -9,6 +9,7 @@ import { type NxProjects, targetNamesFromProjects } from '../nx/index.js';
9
9
  import {
10
10
  deployTargetInfoFromProjects,
11
11
  extractInlineLocalBlocksForTest,
12
+ hasExactTargetForTest,
12
13
  INLINE_LOCAL_BEGIN,
13
14
  INLINE_LOCAL_END,
14
15
  LOCAL_SECTION_MARKER,
@@ -128,7 +129,14 @@ describe('managed-file inline local blocks', () => {
128
129
  it("reinserting refuses when the anchor no longer appears — never silently drops the repo's customization", () => {
129
130
  const fresh = ['a:', ' - one', 'b:'].join('\n'); // ' - two' is gone
130
131
  expect(() => reinsertInlineLocalBlocksForTest(fresh, [{ anchor: ' - two', lines: ' - repo-owned' }])).toThrow(
131
- /no longer matches/,
132
+ /matches no line/,
133
+ );
134
+ });
135
+
136
+ it('reinserting refuses an ambiguous multiply occurring anchor', () => {
137
+ const fresh = ['anchor', 'middle', 'anchor'].join('\n');
138
+ expect(() => reinsertInlineLocalBlocksForTest(fresh, [{ anchor: 'anchor', lines: 'repo-owned' }])).toThrow(
139
+ /matches 2 lines/,
132
140
  );
133
141
  });
134
142
 
@@ -136,8 +144,7 @@ describe('managed-file inline local blocks', () => {
136
144
  // Lines that can serve as anchors: non-empty, not a marker, and each drawn
137
145
  // from a small alphabet so uniqueness is checkable — the round-trip only
138
146
  // holds when an anchor line occurs exactly once in the managed section
139
- // (reinsert finds the FIRST occurrence by design, same as the real file
140
- // shape: comments/patterns are unique in practice).
147
+ // because ambiguous anchors are rejected rather than selecting one.
141
148
  const linePool = fc.constantFrom('alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta');
142
149
  fc.assert(
143
150
  fc.property(
@@ -205,6 +212,20 @@ describe('nx graph project helpers', () => {
205
212
  );
206
213
  });
207
214
 
215
+ it('detects browser and deployment E2E targets by exact name only', () => {
216
+ const exact = targetNamesFromProjects({
217
+ app: { targets: { 'test-browser': {}, 'e2e-deployment': {} } },
218
+ });
219
+ const nearMisses = targetNamesFromProjects({
220
+ app: { targets: { 'test-browser-extra': {}, 'e2e-deployments': {}, 'pre-e2e-deployment': {} } },
221
+ });
222
+
223
+ expect(hasExactTargetForTest(exact, 'test-browser')).toBe(true);
224
+ expect(hasExactTargetForTest(exact, 'e2e-deployment')).toBe(true);
225
+ expect(hasExactTargetForTest(nearMisses, 'test-browser')).toBe(false);
226
+ expect(hasExactTargetForTest(nearMisses, 'e2e-deployment')).toBe(false);
227
+ });
228
+
208
229
  it('detects deploy configurations and cloudflare provider from graph nodes', () => {
209
230
  expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({
210
231
  exists: true,
@@ -217,12 +238,12 @@ describe('nx graph project helpers', () => {
217
238
  expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
218
239
  });
219
240
 
220
- it('recognizes convention-driven deploy-environment targets without per-environment configurations', () => {
241
+ it('recognizes convention-driven deploy-stage targets without per-stage configurations', () => {
221
242
  const projects: NxProjects = {
222
243
  app: {
223
244
  targets: {
224
245
  deploy: {
225
- options: { command: 'smoo wrangler deploy-environment --environment {args.environment}' },
246
+ options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' },
226
247
  },
227
248
  },
228
249
  },
@@ -105,13 +105,15 @@ function reinsertInlineLocalBlocks(content: string, blocks: InlineLocalBlock[]):
105
105
  if (blocks.length === 0) return content;
106
106
  const lines = content.split('\n');
107
107
  for (const block of blocks) {
108
- const index = lines.indexOf(block.anchor);
109
- if (index === -1) {
108
+ const matches = lines.flatMap((line, index) => (line === block.anchor ? [index] : []));
109
+ if (matches.length !== 1) {
110
+ const reason = matches.length === 0 ? 'no line' : `${matches.length} lines`;
110
111
  throw new Error(
111
- `${INLINE_LOCAL_BEGIN} block anchored on "${block.anchor}" no longer matches any line in the updated ` +
112
+ `${INLINE_LOCAL_BEGIN} block anchored on "${block.anchor}" matches ${reason} in the updated ` +
112
113
  'template — reconcile the repo-owned block manually',
113
114
  );
114
115
  }
116
+ const index = matches[0] as number;
115
117
  const markerIndent = block.markerIndent ?? '';
116
118
  lines.splice(
117
119
  index + 1,
@@ -136,6 +138,8 @@ interface ManagedFileContext {
136
138
  hasReleasePackages: boolean;
137
139
  hasStagingDeployTargets: boolean;
138
140
  hasProductionDeployTargets: boolean;
141
+ hasBrowserTestTargets: boolean;
142
+ hasE2eDeploymentTargets: boolean;
139
143
  stagingDeployProvider?: 'cloudflare';
140
144
  productionDeployProvider?: 'cloudflare';
141
145
  ciPushBranches: string[];
@@ -320,6 +324,8 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
320
324
  return renderCiWorkflowYaml({
321
325
  deploy: context.hasStagingDeployTargets,
322
326
  deployProvider: context.stagingDeployProvider,
327
+ browserTests: context.hasBrowserTestTargets,
328
+ e2eDeployment: context.hasStagingDeployTargets && context.hasE2eDeploymentTargets,
323
329
  pushBranches: context.ciPushBranches,
324
330
  runsOn: context.ciRunsOn,
325
331
  });
@@ -356,7 +362,8 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
356
362
  const nxProjects = await loadNxProjects(root);
357
363
  const stagingDeploy = deployTargetInfoFromProjects(nxProjects, 'staging');
358
364
  const productionDeploy = deployTargetInfoFromProjects(nxProjects, 'production');
359
- const platformTargetGlobs = platformTargetGlobsForTest(targetNamesFromProjects(nxProjects));
365
+ const targetNames = targetNamesFromProjects(nxProjects);
366
+ const platformTargetGlobs = platformTargetGlobsForTest(targetNames);
360
367
  const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
361
368
  ? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
362
369
  : `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
@@ -364,6 +371,8 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
364
371
  hasReleasePackages: listReleasePackages(root, packageJson).length > 0,
365
372
  hasStagingDeployTargets: stagingDeploy.exists,
366
373
  hasProductionDeployTargets: productionDeploy.exists,
374
+ hasBrowserTestTargets: hasExactTargetForTest(targetNames, 'test-browser'),
375
+ hasE2eDeploymentTargets: hasExactTargetForTest(targetNames, 'e2e-deployment'),
367
376
  stagingDeployProvider: stagingDeploy.provider,
368
377
  productionDeployProvider: productionDeploy.provider,
369
378
  ciPushBranches,
@@ -374,6 +383,10 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
374
383
  };
375
384
  }
376
385
 
386
+ export function hasExactTargetForTest(targetNames: Iterable<string>, target: string): boolean {
387
+ return [...targetNames].includes(target);
388
+ }
389
+
377
390
  export function platformTargetGlobsForTest(targetNames: Iterable<string>): string[] {
378
391
  const names = [...targetNames];
379
392
  return PLATFORM_TARGET_GLOBS.filter((glob) => {
@@ -404,7 +417,7 @@ function deployTargetInfoFromTargets(targets: Record<string, NxTargetConfig>, co
404
417
  }
405
418
  const baseCommandValue = deploy.options?.command ?? deploy.command;
406
419
  const baseCommand = typeof baseCommandValue === 'string' ? baseCommandValue : '';
407
- if (baseCommand.includes('smoo wrangler deploy-environment')) {
420
+ if (baseCommand.includes('smoo wrangler deploy-stage')) {
408
421
  return { exists: true, provider: 'cloudflare' };
409
422
  }
410
423
  const config = deploy.configurations?.[configuration];
@@ -18,7 +18,7 @@ const PUBLISH_WORKFLOW_FORMAT_OPTIONS = Object.freeze({
18
18
  export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
19
19
  export type PublishWorkflowCondition = 'version-mode-not-none' | 'deploy-production' | 'failure' | 'always';
20
20
  export type PublishWorkflowNxTarget = 'build' | 'lint' | 'test';
21
- export type PublishWorkflowDeployEnvironment = 'none' | 'production';
21
+ export type PublishWorkflowDeployStage = 'none' | 'production';
22
22
 
23
23
  export enum PublishWorkflowStepKind {
24
24
  Checkout = 'checkout',
@@ -62,7 +62,7 @@ export interface PublishWorkflowDefinitionOptions {
62
62
 
63
63
  export interface PublishWorkflowInputs {
64
64
  bump: PublishWorkflowBump;
65
- deployEnvironment: PublishWorkflowDeployEnvironment;
65
+ deployStage: PublishWorkflowDeployStage;
66
66
  dryRun: boolean;
67
67
  }
68
68
 
@@ -263,7 +263,7 @@ function shouldRunStep(
263
263
  return version.mode !== 'none';
264
264
  }
265
265
  if (step.condition === 'deploy-production') {
266
- return version.mode !== 'none' && inputs.deployEnvironment === 'production' && !inputs.dryRun;
266
+ return version.mode !== 'none' && inputs.deployStage === 'production' && !inputs.dryRun;
267
267
  }
268
268
  if (step.condition === 'failure') {
269
269
  return failed;
@@ -291,7 +291,7 @@ function renderPublishWorkflowHeader(options: PublishWorkflowDefinitionOptions):
291
291
  const deployInput =
292
292
  options.deploy === true
293
293
  ? `
294
- deploy_environment:
294
+ deploy_stage:
295
295
  type: choice
296
296
  description: Deploy live systems after a successful publish.
297
297
  options: [none, production]
@@ -483,10 +483,10 @@ function deployProductionStep(step: PublishWorkflowStep, options: PublishWorkflo
483
483
  return [
484
484
  ` - name: ${step.name}`,
485
485
  ' if:',
486
- " ${{ steps.version.outputs.mode != 'none' && inputs.deploy_environment == 'production' && inputs.dry_run !=",
486
+ " ${{ steps.version.outputs.mode != 'none' && inputs.deploy_stage == 'production' && inputs.dry_run !=",
487
487
  " 'true' }}",
488
488
  ...deployEnvLines(options),
489
- ' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
489
+ ' run: smoo github-ci nx-deploy --stage production --mode run-many --verify --name "Deploy Production"',
490
490
  ];
491
491
  }
492
492
 
@@ -534,7 +534,7 @@ function renderPlatformPublishWorkflowYaml(options: PublishWorkflowDefinitionOpt
534
534
  const deployInput =
535
535
  options.deploy === true
536
536
  ? `
537
- deploy_environment:
537
+ deploy_stage:
538
538
  type: choice
539
539
  description: Deploy live systems after a successful publish.
540
540
  options: [none, production]
@@ -940,10 +940,10 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
940
940
  ` # Step ${stepNumber++}`,
941
941
  ' - name: 🚀 Deploy production',
942
942
  ' if:',
943
- " ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_environment == 'production' &&",
943
+ " ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_stage == 'production' &&",
944
944
  " inputs.dry_run != 'true' }}",
945
945
  ...deployEnvLines(options),
946
- ' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
946
+ ' run: smoo github-ci nx-deploy --stage production --mode run-many --verify --name "Deploy Production"',
947
947
  );
948
948
  }
949
949
  lines.push(
@@ -1,5 +1,5 @@
1
1
  import typia from 'typia';
2
- import type { LiveKvNamespace } from './environment.js';
2
+ import type { LiveKvNamespace } from './stage.js';
3
3
 
4
4
  export interface R2Bucket {
5
5
  name: string;
@@ -12,8 +12,8 @@ import type {
12
12
  WorkerRoute,
13
13
  WorkerScript,
14
14
  } from './cloudflare.js';
15
- import { cleanupPullRequest, deployEnvironment, type ProcessResult, type ProcessRunner } from './deploy-environment.js';
16
- import type { LiveKvNamespace } from './environment.js';
15
+ import { cleanupPullRequest, deployStage, type ProcessResult, type ProcessRunner } from './deploy-stage.js';
16
+ import type { LiveKvNamespace } from './stage.js';
17
17
 
18
18
  const HASH = '16577780061662788004';
19
19
  const FIXTURE = `[env.staging]
@@ -154,14 +154,14 @@ class FakeCloudflare implements CloudflareClient {
154
154
  }
155
155
  }
156
156
 
157
- describe('deploy-environment remote version fallback', () => {
157
+ describe('deploy-stage remote version fallback', () => {
158
158
  it('returns a remote cache hit for the active tagged 100% version', async () => {
159
159
  const root = await fixtureRoot();
160
160
  const runner = new FakeRunner([{ id: 'version-1', annotations: { 'workers/tag': `nx-${HASH}` } }], {
161
161
  versions: [{ version_id: 'version-1', percentage: 100 }],
162
162
  });
163
163
 
164
- const result = await deployEnvironment(root, 'pr123', dependencies(runner, new FakeCloudflare()));
164
+ const result = await deployStage(root, 'pr123', dependencies(runner, new FakeCloudflare()));
165
165
 
166
166
  expect(result.action).toBe('remote-cache-hit');
167
167
  expect(runner.calls.map((args) => args.slice(0, 2))).toEqual([
@@ -176,7 +176,7 @@ describe('deploy-environment remote version fallback', () => {
176
176
  versions: [{ version_id: 'version-2', percentage: 100 }],
177
177
  });
178
178
 
179
- const result = await deployEnvironment(root, 'pr123', dependencies(runner, new FakeCloudflare()));
179
+ const result = await deployStage(root, 'pr123', dependencies(runner, new FakeCloudflare()));
180
180
 
181
181
  expect(result.action).toBe('activated');
182
182
  expect(runner.calls.at(-1)?.slice(0, 3)).toEqual(['versions', 'deploy', '--version-tag']);
@@ -189,7 +189,7 @@ describe('deploy-environment remote version fallback', () => {
189
189
  await writeFile(join(root, '.dev.vars.example'), 'OAUTH_STATE_SIGNING_KEY=""\nTOKEN_ENCRYPTION_KEY=""\n');
190
190
  const runner = new FakeRunner([], { versions: [{ version_id: 'version-2', percentage: 100 }] });
191
191
 
192
- const result = await deployEnvironment(root, 'pr123', {
192
+ const result = await deployStage(root, 'pr123', {
193
193
  ...dependencies(runner, new FakeCloudflare()),
194
194
  processEnv: {
195
195
  CLOUDFLARE_ACCOUNT_ID: 'account-1',
@@ -214,13 +214,13 @@ describe('deploy-environment remote version fallback', () => {
214
214
  expect(existsSync(requiredTestValue(runner.secretsPathSeen, 'secrets path'))).toBe(false);
215
215
  });
216
216
 
217
- it('passes only present manifest values for fixed environments and preserves absent remote secrets', async () => {
217
+ it('passes only present manifest values for fixed stages and preserves absent remote secrets', async () => {
218
218
  const root = await fixtureRoot();
219
219
  await writeFile(join(root, '.dev.vars.example'), 'OAUTH_STATE_SIGNING_KEY=""\nTOKEN_ENCRYPTION_KEY=""\n');
220
220
  const runner = new FakeRunner([], {});
221
221
  const cloudflare = new FakeCloudflare();
222
222
 
223
- const result = await deployEnvironment(root, 'staging', {
223
+ const result = await deployStage(root, 'staging', {
224
224
  runner,
225
225
  cloudflare,
226
226
  processEnv: {
@@ -244,7 +244,7 @@ describe('deploy-environment remote version fallback', () => {
244
244
  cloudflare.scripts = [];
245
245
 
246
246
  await expect(
247
- deployEnvironment(root, 'pr123', {
247
+ deployStage(root, 'pr123', {
248
248
  ...dependencies(new FakeRunner([], {}), cloudflare),
249
249
  processEnv: {
250
250
  CLOUDFLARE_ACCOUNT_ID: 'account-1',
@@ -268,7 +268,7 @@ describe('deploy-environment remote version fallback', () => {
268
268
  throw new Error('record already exists');
269
269
  };
270
270
 
271
- const result = await deployEnvironment(root, 'pr123', dependencies(new FakeRunner([], {}), cloudflare));
271
+ const result = await deployStage(root, 'pr123', dependencies(new FakeRunner([], {}), cloudflare));
272
272
 
273
273
  expect(result.action).toBe('deployed');
274
274
  expect(createAttempts).toBe(1);
@@ -276,7 +276,7 @@ describe('deploy-environment remote version fallback', () => {
276
276
  });
277
277
  });
278
278
 
279
- describe('cleanup-pr exact environment matching', () => {
279
+ describe('cleanup-pr exact stage matching', () => {
280
280
  it('rejects an invalid PR before touching the client', async () => {
281
281
  const cloudflare = new FakeCloudflare();
282
282
  let calls = 0;