@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.
- package/dist/cli.js +6 -5
- package/dist/github-ci/index.d.ts +8 -6
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +35 -35
- package/dist/monorepo/ci-workflow.d.ts +3 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +59 -15
- package/dist/monorepo/managed-files.d.ts +1 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +15 -5
- package/dist/monorepo/publish-workflow.d.ts +2 -2
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +6 -6
- package/dist/wrangler/cloudflare.d.ts +1 -1
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/{deploy-environment.d.ts → deploy-stage.d.ts} +6 -6
- package/dist/wrangler/deploy-stage.d.ts.map +1 -0
- package/dist/wrangler/{deploy-environment.js → deploy-stage.js} +26 -26
- package/dist/wrangler/stage.d.ts +58 -0
- package/dist/wrangler/stage.d.ts.map +1 -0
- package/dist/wrangler/{environment.js → stage.js} +44 -44
- package/package.json +7 -7
- package/src/cli.ts +9 -7
- package/src/github-ci/index.test.ts +101 -32
- package/src/github-ci/index.ts +51 -45
- package/src/monorepo/__tests__/ci-workflow.test.ts +87 -49
- package/src/monorepo/__tests__/publish-workflow.test.ts +8 -8
- package/src/monorepo/ci-workflow.ts +67 -14
- package/src/monorepo/managed-files.test.ts +26 -5
- package/src/monorepo/managed-files.ts +18 -5
- package/src/monorepo/publish-workflow.ts +9 -9
- package/src/wrangler/cloudflare.ts +1 -1
- package/src/wrangler/{deploy-environment.test.ts → deploy-stage.test.ts} +11 -11
- package/src/wrangler/{deploy-environment.ts → deploy-stage.ts} +41 -41
- package/src/wrangler/{environment.test.ts → stage.test.ts} +15 -15
- package/src/wrangler/{environment.ts → stage.ts} +49 -52
- package/dist/wrangler/deploy-environment.d.ts.map +0 -1
- package/dist/wrangler/environment.d.ts +0 -58
- 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.
|
|
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('
|
|
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('
|
|
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 --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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-
|
|
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-
|
|
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
|
|
109
|
-
if (
|
|
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}"
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 --
|
|
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
|
-
|
|
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.
|
|
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 --
|
|
946
|
+
' run: smoo github-ci nx-deploy --stage production --mode run-many --verify --name "Deploy Production"',
|
|
947
947
|
);
|
|
948
948
|
}
|
|
949
949
|
lines.push(
|
|
@@ -12,8 +12,8 @@ import type {
|
|
|
12
12
|
WorkerRoute,
|
|
13
13
|
WorkerScript,
|
|
14
14
|
} from './cloudflare.js';
|
|
15
|
-
import { cleanupPullRequest,
|
|
16
|
-
import type { LiveKvNamespace } from './
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|