@smoothbricks/cli 0.10.8 → 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 +7 -5
- package/dist/github-ci/index.d.ts +9 -7
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +36 -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 +7 -7
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +14 -6
- package/dist/wrangler/cloudflare.d.ts +4 -2
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +6 -3
- 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 +8 -8
- package/src/cli.ts +11 -8
- package/src/github-ci/index.test.ts +105 -34
- package/src/github-ci/index.ts +53 -46
- package/src/monorepo/__tests__/ci-workflow.test.ts +87 -49
- package/src/monorepo/__tests__/publish-workflow.test.ts +14 -12
- 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 +15 -12
- package/src/release/index.ts +12 -4
- package/src/wrangler/cloudflare.test.ts +76 -0
- package/src/wrangler/cloudflare.ts +6 -3
- 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
|
@@ -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]
|
|
@@ -778,16 +778,19 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
|
|
|
778
778
|
'',
|
|
779
779
|
` # Step ${stepNumber++}`,
|
|
780
780
|
' - name: 🍎 Build selected macOS and iOS release outputs',
|
|
781
|
+
' id: platform-outputs',
|
|
781
782
|
' run:',
|
|
782
783
|
` smoo release build-platform-outputs --bump "${githubExpression(
|
|
783
784
|
'inputs.bump',
|
|
784
785
|
)}" --ref "${githubExpression('github.sha')}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}" --output`,
|
|
785
|
-
` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
|
|
786
|
+
` "${githubExpression('runner.temp')}/macos-platform-outputs" --github-output "$GITHUB_OUTPUT"`,
|
|
786
787
|
'',
|
|
787
788
|
` # Step ${stepNumber++}`,
|
|
788
|
-
' - name: 🧪 Unit test macOS and iOS packages',
|
|
789
|
+
' - name: 🧪 Unit test selected macOS and iOS packages',
|
|
790
|
+
` if: steps.platform-outputs.outputs.projects != ''`,
|
|
789
791
|
' run:',
|
|
790
|
-
|
|
792
|
+
' smoo github-ci nx-run-many --targets test --projects',
|
|
793
|
+
` "${githubExpression('steps.platform-outputs.outputs.projects')}"`,
|
|
791
794
|
'',
|
|
792
795
|
` # Step ${stepNumber++}`,
|
|
793
796
|
' - name: 📤 Upload macOS platform outputs',
|
|
@@ -937,10 +940,10 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
|
|
|
937
940
|
` # Step ${stepNumber++}`,
|
|
938
941
|
' - name: 🚀 Deploy production',
|
|
939
942
|
' if:',
|
|
940
|
-
" ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.
|
|
943
|
+
" ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_stage == 'production' &&",
|
|
941
944
|
" inputs.dry_run != 'true' }}",
|
|
942
945
|
...deployEnvLines(options),
|
|
943
|
-
' run: smoo github-ci nx-deploy --
|
|
946
|
+
' run: smoo github-ci nx-deploy --stage production --mode run-many --verify --name "Deploy Production"',
|
|
944
947
|
);
|
|
945
948
|
}
|
|
946
949
|
lines.push(
|
package/src/release/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface ReleaseRepairPendingOptions {
|
|
|
80
80
|
|
|
81
81
|
export interface ReleasePlatformOutputsOptions {
|
|
82
82
|
bump: string;
|
|
83
|
+
githubOutput?: string;
|
|
83
84
|
output: string;
|
|
84
85
|
ref?: string;
|
|
85
86
|
targets: string;
|
|
@@ -221,7 +222,7 @@ export async function releaseCollectPlatformOutputs(
|
|
|
221
222
|
: `Release platform outputs: building current outputs for ${packageSummary(currentPackages)}.`,
|
|
222
223
|
);
|
|
223
224
|
const outputRoot = resolve(root, options.output);
|
|
224
|
-
await githubCiNxRunMany(root, {
|
|
225
|
+
const currentRuns = await githubCiNxRunMany(root, {
|
|
225
226
|
targets: options.targets,
|
|
226
227
|
projects: releasePackageProjects(currentPackages),
|
|
227
228
|
collectOutputs: join(outputRoot, 'current'),
|
|
@@ -236,6 +237,12 @@ export async function releaseCollectPlatformOutputs(
|
|
|
236
237
|
targets,
|
|
237
238
|
restoreRef,
|
|
238
239
|
);
|
|
240
|
+
if (options.githubOutput) {
|
|
241
|
+
const projects = [
|
|
242
|
+
...new Set(currentRuns.runs.flatMap((run) => run.projects.map((project) => project.project))),
|
|
243
|
+
].sort((left, right) => left.localeCompare(right));
|
|
244
|
+
await appendFile(options.githubOutput, `projects=${projects.join(',')}\n`);
|
|
245
|
+
}
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
@@ -966,12 +973,13 @@ function releaseRepairOutputsShell(
|
|
|
966
973
|
): ReleaseRepairOutputsShell<ReleasePackage> {
|
|
967
974
|
return {
|
|
968
975
|
...releaseTargetCheckoutShell(root),
|
|
969
|
-
collectRepairTargetOutputs: (target) =>
|
|
970
|
-
githubCiNxRunMany(root, {
|
|
976
|
+
collectRepairTargetOutputs: async (target) => {
|
|
977
|
+
await githubCiNxRunMany(root, {
|
|
971
978
|
targets: targetGlobs,
|
|
972
979
|
projects: releasePackageProjects(target.npmPackages),
|
|
973
980
|
collectOutputs: join(outputRoot, target.sha),
|
|
974
|
-
})
|
|
981
|
+
});
|
|
982
|
+
},
|
|
975
983
|
};
|
|
976
984
|
}
|
|
977
985
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CloudflareApiError, CloudflareRestClient } from './cloudflare.js';
|
|
3
|
+
|
|
4
|
+
type CloudflareFetcher = NonNullable<ConstructorParameters<typeof CloudflareRestClient>[2]>;
|
|
5
|
+
|
|
6
|
+
function jsonFetcher(body: unknown, status = 200): CloudflareFetcher {
|
|
7
|
+
return async () =>
|
|
8
|
+
new Response(JSON.stringify(body), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('CloudflareRestClient', () => {
|
|
15
|
+
it('accepts nullable diagnostics in successful API envelopes', async () => {
|
|
16
|
+
const client = new CloudflareRestClient(
|
|
17
|
+
'account-id',
|
|
18
|
+
'api-token',
|
|
19
|
+
jsonFetcher({
|
|
20
|
+
result: [
|
|
21
|
+
{
|
|
22
|
+
id: 'domain-id',
|
|
23
|
+
hostname: 'app.pr45.example.com',
|
|
24
|
+
service: 'app-pr45',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
success: true,
|
|
28
|
+
errors: null,
|
|
29
|
+
messages: null,
|
|
30
|
+
result_info: {
|
|
31
|
+
page: 1,
|
|
32
|
+
per_page: 1000,
|
|
33
|
+
count: 1,
|
|
34
|
+
total_count: 1,
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await expect(client.listWorkerDomains()).resolves.toEqual([
|
|
40
|
+
{
|
|
41
|
+
id: 'domain-id',
|
|
42
|
+
hostname: 'app.pr45.example.com',
|
|
43
|
+
service: 'app-pr45',
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('preserves structured Cloudflare API failures', async () => {
|
|
49
|
+
const client = new CloudflareRestClient(
|
|
50
|
+
'account-id',
|
|
51
|
+
'api-token',
|
|
52
|
+
jsonFetcher(
|
|
53
|
+
{
|
|
54
|
+
result: null,
|
|
55
|
+
success: false,
|
|
56
|
+
errors: [{ code: 10000, message: 'Authentication error' }],
|
|
57
|
+
messages: [],
|
|
58
|
+
},
|
|
59
|
+
403,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await client.listWorkerDomains();
|
|
65
|
+
throw new Error('expected listWorkerDomains to fail');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
expect(error).toBeInstanceOf(CloudflareApiError);
|
|
68
|
+
expect(error).toMatchObject({
|
|
69
|
+
message:
|
|
70
|
+
'Cloudflare API /accounts/account-id/workers/domains?per_page=1000&page=1 failed: Authentication error',
|
|
71
|
+
status: 403,
|
|
72
|
+
codes: [10000],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import typia from 'typia';
|
|
2
|
-
import type { LiveKvNamespace } from './
|
|
2
|
+
import type { LiveKvNamespace } from './stage.js';
|
|
3
3
|
|
|
4
4
|
export interface R2Bucket {
|
|
5
5
|
name: string;
|
|
@@ -60,7 +60,8 @@ export interface CloudflareClient {
|
|
|
60
60
|
interface CloudflareEnvelope {
|
|
61
61
|
success: boolean;
|
|
62
62
|
result?: unknown;
|
|
63
|
-
errors?: Array<{ code?: number; message?: string }
|
|
63
|
+
errors?: Array<{ code?: number; message?: string }> | null;
|
|
64
|
+
messages?: Array<{ code?: number; message?: string }> | null;
|
|
64
65
|
result_info?: {
|
|
65
66
|
page?: number;
|
|
66
67
|
total_pages?: number;
|
|
@@ -92,13 +93,15 @@ export class CloudflareApiError extends Error {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
type CloudflareFetcher = (input: string, init?: RequestInit) => Promise<Response>;
|
|
97
|
+
|
|
95
98
|
export class CloudflareRestClient implements CloudflareClient {
|
|
96
99
|
private readonly accountPath: string;
|
|
97
100
|
|
|
98
101
|
constructor(
|
|
99
102
|
accountId: string,
|
|
100
103
|
private readonly apiToken: string,
|
|
101
|
-
private readonly fetcher:
|
|
104
|
+
private readonly fetcher: CloudflareFetcher = fetch,
|
|
102
105
|
) {
|
|
103
106
|
if (!accountId || !apiToken) {
|
|
104
107
|
throw new Error('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required.');
|
|
@@ -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;
|