@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
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
nxSmartArgs,
|
|
15
15
|
publishGithubDeployment,
|
|
16
16
|
readGitHeadSha,
|
|
17
|
-
|
|
18
|
-
|
|
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('
|
|
139
|
-
'
|
|
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
|
-
'
|
|
142
|
-
'--
|
|
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
|
|
544
|
-
it('resolves same-repository PR, private push, release, and explicit production
|
|
544
|
+
describe('event-aware stage deployment', () => {
|
|
545
|
+
it('resolves same-repository PR, private push, release, and explicit production stages', () => {
|
|
545
546
|
expect(
|
|
546
|
-
|
|
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
|
-
|
|
558
|
+
resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'push', GITHUB_REF_NAME: 'private' }, undefined),
|
|
558
559
|
).toBe('staging');
|
|
559
|
-
expect(
|
|
560
|
-
expect(
|
|
560
|
+
expect(resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'release' }, undefined)).toBe('production');
|
|
561
|
+
expect(resolveDeploymentStage('production', {}, undefined)).toBe('production');
|
|
561
562
|
expect(() =>
|
|
562
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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('--
|
|
702
|
-
expect(nxCalls[
|
|
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
|
});
|
package/src/github-ci/index.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
409
|
-
const name = options.name ?? 'Deploy
|
|
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 ${
|
|
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(`--
|
|
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 (
|
|
448
|
-
const url = `https://app.${
|
|
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(
|
|
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(
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
518
|
+
export function resolveDeploymentStage(
|
|
513
519
|
explicit: string | undefined,
|
|
514
520
|
environment: NodeJS.ProcessEnv,
|
|
515
521
|
event: GithubActionsEventPayload | undefined,
|
|
516
|
-
):
|
|
517
|
-
if (explicit !== undefined) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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 {
|
|
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('
|
|
23
|
-
const
|
|
24
|
-
const
|
|
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])).
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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'
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
|
68
|
-
const rendered = renderCiWorkflowYaml({
|
|
69
|
-
|
|
70
|
-
|
|
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).
|
|
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
|
});
|