@smoothbricks/cli 0.10.6 → 0.10.8
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.d.ts.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/github-ci/index.d.ts +44 -3
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +242 -36
- package/dist/lib/json.d.ts +1 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +3 -18
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +1 -0
- package/dist/monorepo/ci-workflow.js +16 -6
- package/dist/monorepo/managed-files.d.ts +6 -22
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +31 -48
- package/dist/monorepo/package-policy.js +2 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +2 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
- package/dist/monorepo/publish-workflow.js +3 -3
- package/dist/monorepo/runtime.d.ts +2 -2
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +11 -16
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +105 -28
- package/dist/nx/index.d.ts +7 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +117 -56
- package/dist/playwright/index.d.ts +22 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +44 -0
- package/dist/release/bootstrap-npm-packages.d.ts +3 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
- package/dist/release/bootstrap-npm-packages.js +21 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +17 -0
- package/dist/wrangler/cloudflare.d.ts +85 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -0
- package/dist/wrangler/cloudflare.js +235 -0
- package/dist/wrangler/deploy-environment.d.ts +48 -0
- package/dist/wrangler/deploy-environment.d.ts.map +1 -0
- package/dist/wrangler/deploy-environment.js +383 -0
- package/dist/wrangler/environment.d.ts +58 -0
- package/dist/wrangler/environment.d.ts.map +1 -0
- package/dist/wrangler/environment.js +297 -0
- package/managed/raw/tooling/devenv +71 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
- package/package.json +11 -4
- package/src/cli.ts +33 -3
- package/src/github-ci/index.test.ts +221 -1
- package/src/github-ci/index.ts +303 -32
- package/src/lib/json.ts +1 -1
- package/src/lib/workspace.ts +1 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
- package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +12 -4
- package/src/monorepo/ci-workflow.ts +16 -6
- package/src/monorepo/managed-files.test.ts +87 -23
- package/src/monorepo/managed-files.ts +35 -69
- package/src/monorepo/package-policy.test.ts +31 -0
- package/src/monorepo/package-policy.ts +2 -2
- package/src/monorepo/packs/index.ts +2 -0
- package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
- package/src/monorepo/publish-workflow.ts +7 -2
- package/src/monorepo/runtime.test.ts +23 -17
- package/src/monorepo/runtime.ts +12 -17
- package/src/monorepo/tool-validation.test.ts +100 -8
- package/src/monorepo/tool-validation.ts +115 -31
- package/src/nx/index.test.ts +45 -14
- package/src/nx/index.ts +133 -64
- package/src/playwright/index.test.ts +90 -0
- package/src/playwright/index.ts +73 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
- package/src/release/bootstrap-npm-packages.ts +34 -0
- package/src/release/index.ts +18 -0
- package/src/wrangler/cloudflare.ts +289 -0
- package/src/wrangler/deploy-environment.test.ts +354 -0
- package/src/wrangler/deploy-environment.ts +445 -0
- package/src/wrangler/environment.test.ts +173 -0
- package/src/wrangler/environment.ts +366 -0
- package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
|
@@ -8,10 +8,14 @@ import type { ProjectTargets } from '../nx/index.js';
|
|
|
8
8
|
import {
|
|
9
9
|
expandNxTargetDependencyRuns,
|
|
10
10
|
expandNxTargetRuns,
|
|
11
|
+
githubCiNxDeploy,
|
|
11
12
|
githubCiNxRunMany,
|
|
12
13
|
nxRunManyArgs,
|
|
13
14
|
nxSmartArgs,
|
|
15
|
+
publishGithubDeployment,
|
|
14
16
|
readGitHeadSha,
|
|
17
|
+
resolveDeploymentEnvironment,
|
|
18
|
+
selectEnvironmentDeployProjects,
|
|
15
19
|
} from './index.js';
|
|
16
20
|
import {
|
|
17
21
|
applyCollectedOutputs,
|
|
@@ -53,6 +57,28 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
53
57
|
]);
|
|
54
58
|
});
|
|
55
59
|
|
|
60
|
+
it('selects test owners from target globs and fails if any selected project lacks test', () => {
|
|
61
|
+
const platformProjects = projects.map((project) =>
|
|
62
|
+
project.project === 'mobile' ? { ...project, targets: [...project.targets, 'test'] } : project,
|
|
63
|
+
);
|
|
64
|
+
const expanded = expandNxTargetRuns(platformProjects, {
|
|
65
|
+
targets: 'test',
|
|
66
|
+
projectsWithTargets: '*-macos,*-ios',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(expanded.runs.map((run) => [run.target, run.projects.map((project) => project.project)])).toEqual([
|
|
70
|
+
['test', ['desktop', 'mobile']],
|
|
71
|
+
]);
|
|
72
|
+
const [testRun] = expanded.runs;
|
|
73
|
+
if (!testRun) {
|
|
74
|
+
throw new Error('Expected a test target run.');
|
|
75
|
+
}
|
|
76
|
+
expect(nxRunManyArgs(testRun)).toEqual(['run-many', '-t', 'test', '--projects=desktop,mobile', '--parallel=100%']);
|
|
77
|
+
expect(() => expandNxTargetRuns(projects, { targets: 'test', projectsWithTargets: '*-macos,*-ios' })).toThrow(
|
|
78
|
+
'Nx target test is missing for project(s) selected by --projects-with-targets *-macos,*-ios: mobile.',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
it('reports a zero-match glob as an empty run set', () => {
|
|
57
83
|
expect(expandNxTargetRuns(projects, { targets: '*-windows' })).toEqual({
|
|
58
84
|
runs: [],
|
|
@@ -155,6 +181,21 @@ describe('collected Nx outputs', () => {
|
|
|
155
181
|
});
|
|
156
182
|
});
|
|
157
183
|
|
|
184
|
+
it('runs test for projects selected by platform target ownership', async () => {
|
|
185
|
+
await withNxRunManyFixture(async ({ root }) => {
|
|
186
|
+
await writeFile(join(root, 'packages/app/test-target.ts'), "await Bun.write('test-ran.txt', 'tested');\n");
|
|
187
|
+
await writeFile(join(root, 'packages/app/build-target.ts'), "throw new Error('build must not run here');\n");
|
|
188
|
+
|
|
189
|
+
await githubCiNxRunMany(root, {
|
|
190
|
+
targets: 'test',
|
|
191
|
+
projectsWithTargets: '*-macos,*-ios',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await expect(readFile(join(root, 'test-ran.txt'), 'utf8')).resolves.toBe('tested');
|
|
195
|
+
await expect(readFile(join(root, 'build-ran.txt'), 'utf8')).rejects.toThrow();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
158
199
|
it('applies an empty manifest after artifact transport omits its empty workspace directory', async () => {
|
|
159
200
|
await withOutputFixture(async ({ root, artifact }) => {
|
|
160
201
|
const manifest = await collectNxOutputs(root, artifact, [], SOURCE_SHA);
|
|
@@ -472,7 +513,19 @@ async function withNxRunManyFixture(
|
|
|
472
513
|
join(root, 'packages/app/package.json'),
|
|
473
514
|
JSON.stringify({
|
|
474
515
|
name: '@fixture/app',
|
|
475
|
-
nx: {
|
|
516
|
+
nx: {
|
|
517
|
+
name: 'app',
|
|
518
|
+
targets: {
|
|
519
|
+
'build-macos': {
|
|
520
|
+
executor: 'nx:run-commands',
|
|
521
|
+
options: { command: 'bun packages/app/build-target.ts', cwd: '.' },
|
|
522
|
+
},
|
|
523
|
+
test: {
|
|
524
|
+
executor: 'nx:run-commands',
|
|
525
|
+
options: { command: 'bun packages/app/test-target.ts', cwd: '.' },
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
476
529
|
}),
|
|
477
530
|
);
|
|
478
531
|
await $`git init --quiet`.cwd(root);
|
|
@@ -484,3 +537,170 @@ async function withNxRunManyFixture(
|
|
|
484
537
|
await rm(temp, { recursive: true, force: true });
|
|
485
538
|
}
|
|
486
539
|
}
|
|
540
|
+
|
|
541
|
+
describe('event-aware environment deployment', () => {
|
|
542
|
+
it('resolves same-repository PR, private push, release, and explicit production environments', () => {
|
|
543
|
+
expect(
|
|
544
|
+
resolveDeploymentEnvironment(
|
|
545
|
+
undefined,
|
|
546
|
+
{ GITHUB_EVENT_NAME: 'pull_request' },
|
|
547
|
+
{
|
|
548
|
+
action: 'synchronize',
|
|
549
|
+
repository: { full_name: 'owner/repo' },
|
|
550
|
+
pull_request: { number: 123, head: { repo: { full_name: 'owner/repo' } } },
|
|
551
|
+
},
|
|
552
|
+
),
|
|
553
|
+
).toBe('pr123');
|
|
554
|
+
expect(
|
|
555
|
+
resolveDeploymentEnvironment(undefined, { GITHUB_EVENT_NAME: 'push', GITHUB_REF_NAME: 'private' }, undefined),
|
|
556
|
+
).toBe('staging');
|
|
557
|
+
expect(resolveDeploymentEnvironment(undefined, { GITHUB_EVENT_NAME: 'release' }, undefined)).toBe('production');
|
|
558
|
+
expect(resolveDeploymentEnvironment('production', {}, undefined)).toBe('production');
|
|
559
|
+
expect(() =>
|
|
560
|
+
resolveDeploymentEnvironment(
|
|
561
|
+
undefined,
|
|
562
|
+
{ GITHUB_EVENT_NAME: 'pull_request' },
|
|
563
|
+
{
|
|
564
|
+
action: 'opened',
|
|
565
|
+
repository: { full_name: 'owner/repo' },
|
|
566
|
+
pull_request: { number: 123, head: { repo: { full_name: 'fork/repo' } } },
|
|
567
|
+
},
|
|
568
|
+
),
|
|
569
|
+
).toThrow(/same-repository/);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('selects only deploy targets owned by the environment convention', async () => {
|
|
573
|
+
const definitions: Record<string, unknown> = {
|
|
574
|
+
'conloca-app': {
|
|
575
|
+
targets: {
|
|
576
|
+
deploy: { options: { command: 'smoo wrangler deploy-environment --environment {args.environment}' } },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
'conloca-app-backend': {
|
|
580
|
+
targets: { deploy: { command: 'smoo wrangler deploy-environment --environment {args.environment}' } },
|
|
581
|
+
},
|
|
582
|
+
'conloca-oauth-redirect': {
|
|
583
|
+
targets: { deploy: { options: { command: 'wrangler deploy --config wrangler.toml' } } },
|
|
584
|
+
},
|
|
585
|
+
'conloca-website': {
|
|
586
|
+
targets: { deploy: { options: { command: 'bun scripts/deploy-website.ts' } } },
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
await expect(
|
|
591
|
+
selectEnvironmentDeployProjects(Object.keys(definitions), async (project) => definitions[project]),
|
|
592
|
+
).resolves.toEqual(['conloca-app', 'conloca-app-backend']);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('publishes GitHub deployment JSON through a real stdin process seam', async () => {
|
|
596
|
+
const calls: Array<{ args: string[]; input: unknown }> = [];
|
|
597
|
+
await publishGithubDeployment(
|
|
598
|
+
'pr123',
|
|
599
|
+
'https://app.pr123.conloca.com',
|
|
600
|
+
{ GITHUB_REPOSITORY: 'owner/repo', GITHUB_SHA: 'abc123' },
|
|
601
|
+
{
|
|
602
|
+
run: async (args, input) => {
|
|
603
|
+
calls.push({ args, input: JSON.parse(input) });
|
|
604
|
+
return {
|
|
605
|
+
exitCode: 0,
|
|
606
|
+
stdout: calls.length === 1 ? JSON.stringify({ id: 42 }) : '',
|
|
607
|
+
stderr: '',
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
expect(calls).toEqual([
|
|
614
|
+
{
|
|
615
|
+
args: [
|
|
616
|
+
'api',
|
|
617
|
+
'--method',
|
|
618
|
+
'POST',
|
|
619
|
+
'-H',
|
|
620
|
+
'Accept: application/vnd.github+json',
|
|
621
|
+
'/repos/owner/repo/deployments',
|
|
622
|
+
'--input',
|
|
623
|
+
'-',
|
|
624
|
+
],
|
|
625
|
+
input: {
|
|
626
|
+
ref: 'abc123',
|
|
627
|
+
environment: 'pr123',
|
|
628
|
+
auto_merge: false,
|
|
629
|
+
required_contexts: [],
|
|
630
|
+
transient_environment: true,
|
|
631
|
+
production_environment: false,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
args: [
|
|
636
|
+
'api',
|
|
637
|
+
'--method',
|
|
638
|
+
'POST',
|
|
639
|
+
'-H',
|
|
640
|
+
'Accept: application/vnd.github+json',
|
|
641
|
+
'/repos/owner/repo/deployments/42/statuses',
|
|
642
|
+
'--input',
|
|
643
|
+
'-',
|
|
644
|
+
],
|
|
645
|
+
input: {
|
|
646
|
+
state: 'success',
|
|
647
|
+
environment: 'pr123',
|
|
648
|
+
environment_url: 'https://app.pr123.conloca.com',
|
|
649
|
+
auto_inactive: false,
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
]);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('deploys app/backend, follows with e2e-deployed, and publishes PR metadata', async () => {
|
|
656
|
+
const nxCalls: string[][] = [];
|
|
657
|
+
const listCalls: Array<[string, string]> = [];
|
|
658
|
+
const summaries: string[] = [];
|
|
659
|
+
const deployments: Array<[string, string]> = [];
|
|
660
|
+
|
|
661
|
+
await githubCiNxDeploy(
|
|
662
|
+
'/repo',
|
|
663
|
+
{ mode: 'run-many', name: 'Deploy Environment' },
|
|
664
|
+
{
|
|
665
|
+
processEnv: {
|
|
666
|
+
GITHUB_EVENT_NAME: 'pull_request',
|
|
667
|
+
GITHUB_STEP_SUMMARY: '/summary',
|
|
668
|
+
},
|
|
669
|
+
setStatus: async () => {},
|
|
670
|
+
eventPayload: {
|
|
671
|
+
action: 'opened',
|
|
672
|
+
repository: { full_name: 'owner/repo' },
|
|
673
|
+
pull_request: { number: 123, head: { repo: { full_name: 'owner/repo' } } },
|
|
674
|
+
},
|
|
675
|
+
listProjects: async (_root, target, mode) => {
|
|
676
|
+
listCalls.push([target, mode]);
|
|
677
|
+
return target === 'deploy' ? ['conloca-app', 'conloca-app-backend'] : ['conloca-e2e'];
|
|
678
|
+
},
|
|
679
|
+
runNx: async (args) => {
|
|
680
|
+
nxCalls.push(args);
|
|
681
|
+
return 0;
|
|
682
|
+
},
|
|
683
|
+
appendSummary: async (_path, content) => {
|
|
684
|
+
summaries.push(content);
|
|
685
|
+
},
|
|
686
|
+
publishDeployment: async (environment, url) => {
|
|
687
|
+
deployments.push([environment, url]);
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
expect(listCalls).toEqual([
|
|
693
|
+
['deploy', 'run-many'],
|
|
694
|
+
['e2e-deployed', 'run-many'],
|
|
695
|
+
]);
|
|
696
|
+
expect(nxCalls).toHaveLength(2);
|
|
697
|
+
expect(nxCalls[0]).toContain('--projects=conloca-app,conloca-app-backend');
|
|
698
|
+
expect(nxCalls[0]).toContain('--exclude=tag:permanent-deploy-target');
|
|
699
|
+
expect(nxCalls[0]).toContain('--environment=pr123');
|
|
700
|
+
expect(nxCalls[1]).toContain('-t');
|
|
701
|
+
expect(nxCalls[1]).toContain('e2e-deployed');
|
|
702
|
+
expect(nxCalls[1]).toContain('--environment=pr123');
|
|
703
|
+
expect(summaries).toEqual(['## pr123 deployment\n\n[View deployment](https://app.pr123.conloca.com)\n']);
|
|
704
|
+
expect(deployments).toEqual([['pr123', 'https://app.pr123.conloca.com']]);
|
|
705
|
+
});
|
|
706
|
+
});
|
package/src/github-ci/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { appendFile, mkdtemp, realpath, rename, rm } from 'node:fs/promises';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
@@ -6,24 +7,45 @@ import typia from 'typia';
|
|
|
6
7
|
import { parseStringArrayText } from '../lib/json.js';
|
|
7
8
|
import { decode, run, runStatus } from '../lib/run.js';
|
|
8
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';
|
|
9
16
|
import type { NxTargetRun } from './outputs.js';
|
|
10
17
|
|
|
11
|
-
interface GithubActionsEventPayload {
|
|
18
|
+
export interface GithubActionsEventPayload {
|
|
19
|
+
action?: string;
|
|
12
20
|
repository?: {
|
|
13
21
|
default_branch?: string;
|
|
22
|
+
full_name?: string;
|
|
23
|
+
};
|
|
24
|
+
pull_request?: {
|
|
25
|
+
number?: number;
|
|
26
|
+
head?: {
|
|
27
|
+
repo?: {
|
|
28
|
+
full_name?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
14
31
|
};
|
|
15
32
|
}
|
|
16
33
|
|
|
17
|
-
interface
|
|
34
|
+
interface NxProjectDeployTarget {
|
|
18
35
|
targets?: {
|
|
19
36
|
deploy?: {
|
|
20
|
-
|
|
37
|
+
command?: unknown;
|
|
38
|
+
options?: {
|
|
39
|
+
command?: unknown;
|
|
40
|
+
};
|
|
21
41
|
};
|
|
22
42
|
};
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
const parseGithubActionsEvent = typia.json.createIsParse<GithubActionsEventPayload>();
|
|
26
|
-
const
|
|
46
|
+
const isGithubDeployment = typia.createIs<{ id: number }>();
|
|
47
|
+
const isNxProjectDeployTarget = typia.createIs<NxProjectDeployTarget>();
|
|
48
|
+
const parseNxProjectDeployTarget = typia.json.createIsParse<NxProjectDeployTarget>();
|
|
27
49
|
|
|
28
50
|
type NxSmartMode = 'auto' | 'affected' | 'run-many';
|
|
29
51
|
|
|
@@ -168,6 +190,7 @@ export interface NxRunManyOptions {
|
|
|
168
190
|
configuration?: string;
|
|
169
191
|
collectOutputs?: string;
|
|
170
192
|
allowEmptyProjects?: boolean;
|
|
193
|
+
projectsWithTargets?: string;
|
|
171
194
|
}
|
|
172
195
|
|
|
173
196
|
export interface ExpandedNxTargetRuns {
|
|
@@ -176,10 +199,18 @@ export interface ExpandedNxTargetRuns {
|
|
|
176
199
|
}
|
|
177
200
|
|
|
178
201
|
export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunManyOptions): ExpandedNxTargetRuns {
|
|
179
|
-
const
|
|
180
|
-
if (options.projects !== undefined &&
|
|
202
|
+
const namedProjects = selectProjects(projects, options.projects);
|
|
203
|
+
if (options.projects !== undefined && namedProjects.length === 0 && options.allowEmptyProjects !== true) {
|
|
181
204
|
throw new Error(`No Nx projects matched --projects ${options.projects}.`);
|
|
182
205
|
}
|
|
206
|
+
const selectedProjects = selectProjectsWithTargets(namedProjects, options.projectsWithTargets);
|
|
207
|
+
if (
|
|
208
|
+
options.projectsWithTargets !== undefined &&
|
|
209
|
+
selectedProjects.length === 0 &&
|
|
210
|
+
options.allowEmptyProjects !== true
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(`No Nx projects own targets matching --projects-with-targets ${options.projectsWithTargets}.`);
|
|
213
|
+
}
|
|
183
214
|
const selectedTargetNames = [...new Set(selectedProjects.flatMap((project) => project.targets))].sort((a, b) =>
|
|
184
215
|
a.localeCompare(b),
|
|
185
216
|
);
|
|
@@ -195,6 +226,16 @@ export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunMan
|
|
|
195
226
|
unmatchedGlobs.push(targetPattern);
|
|
196
227
|
}
|
|
197
228
|
for (const target of targets) {
|
|
229
|
+
if (!isGlob && options.projectsWithTargets !== undefined) {
|
|
230
|
+
const missingProjects = selectedProjects
|
|
231
|
+
.filter((project) => !project.targets.includes(target))
|
|
232
|
+
.map((project) => project.project);
|
|
233
|
+
if (missingProjects.length > 0) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Nx target ${target} is missing for project(s) selected by --projects-with-targets ${options.projectsWithTargets}: ${missingProjects.join(', ')}.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
198
239
|
if (addedTargets.has(target)) {
|
|
199
240
|
continue;
|
|
200
241
|
}
|
|
@@ -321,6 +362,16 @@ function selectProjects(projects: ProjectTargets[], selectors: string | undefine
|
|
|
321
362
|
.sort((left, right) => left.project.localeCompare(right.project));
|
|
322
363
|
}
|
|
323
364
|
|
|
365
|
+
function selectProjectsWithTargets(projects: ProjectTargets[], selectors: string | undefined): ProjectTargets[] {
|
|
366
|
+
if (selectors === undefined) {
|
|
367
|
+
return projects;
|
|
368
|
+
}
|
|
369
|
+
const patterns = commaSeparatedValues(selectors);
|
|
370
|
+
return projects.filter((project) =>
|
|
371
|
+
project.targets.some((target) => patterns.some((pattern) => new Bun.Glob(pattern).match(target))),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
324
375
|
function commaSeparatedValues(value: string): string[] {
|
|
325
376
|
return value
|
|
326
377
|
.split(',')
|
|
@@ -328,35 +379,102 @@ function commaSeparatedValues(value: string): string[] {
|
|
|
328
379
|
.filter(Boolean);
|
|
329
380
|
}
|
|
330
381
|
|
|
382
|
+
export interface GithubCiNxDeployOptions {
|
|
383
|
+
environment?: string;
|
|
384
|
+
mode?: NxSmartMode;
|
|
385
|
+
name?: string;
|
|
386
|
+
step?: string;
|
|
387
|
+
verify?: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface GithubCiNxDeployDependencies {
|
|
391
|
+
listProjects?: (root: string, target: string, mode: 'affected' | 'run-many') => Promise<string[]>;
|
|
392
|
+
runNx?: (args: string[], root: string) => Promise<number>;
|
|
393
|
+
appendSummary?: (summaryPath: string, content: string) => Promise<void>;
|
|
394
|
+
publishDeployment?: (environment: `pr${number}`, url: string) => Promise<void>;
|
|
395
|
+
setStatus?: (state: 'pending' | 'success' | 'failure') => Promise<void>;
|
|
396
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
397
|
+
eventPayload?: GithubActionsEventPayload;
|
|
398
|
+
}
|
|
399
|
+
|
|
331
400
|
export async function githubCiNxDeploy(
|
|
332
401
|
root: string,
|
|
333
|
-
options:
|
|
402
|
+
options: GithubCiNxDeployOptions,
|
|
403
|
+
dependencies: GithubCiNxDeployDependencies = {},
|
|
334
404
|
): Promise<void> {
|
|
335
|
-
const
|
|
405
|
+
const processEnv = dependencies.processEnv ?? process.env;
|
|
406
|
+
const eventPayload = dependencies.eventPayload ?? readGithubActionsEvent(processEnv);
|
|
407
|
+
const environment = resolveDeploymentEnvironment(options.environment, processEnv, eventPayload);
|
|
408
|
+
const name = options.name ?? 'Deploy Environment';
|
|
336
409
|
const step = options.step ?? '';
|
|
337
|
-
|
|
410
|
+
const setStatus =
|
|
411
|
+
dependencies.setStatus ??
|
|
412
|
+
((state: 'pending' | 'success' | 'failure') =>
|
|
413
|
+
state === 'pending' ? createGithubStatus(name, step) : updateGithubStatus(name, state, step));
|
|
414
|
+
await setStatus('pending');
|
|
338
415
|
const mode = resolveNxSmartMode(options.mode ?? 'run-many');
|
|
339
|
-
const
|
|
416
|
+
const listProjects = dependencies.listProjects ?? listNxProjectsWithTarget;
|
|
417
|
+
const runNx = dependencies.runNx ?? ((args: string[], commandRoot: string) => runStatus('nx', args, commandRoot));
|
|
418
|
+
const projects = await listProjects(root, 'deploy', mode);
|
|
340
419
|
if (projects.length === 0) {
|
|
341
|
-
console.log(`No ${mode} deploy projects
|
|
342
|
-
await
|
|
420
|
+
console.log(`No ${mode} deploy projects; skipping ${environment}.`);
|
|
421
|
+
await setStatus('success');
|
|
343
422
|
return;
|
|
344
423
|
}
|
|
345
424
|
|
|
346
425
|
const projectList = projects.join(',');
|
|
347
426
|
const targets = options.verify === true ? ['build', 'lint', 'test', 'deploy'] : ['deploy'];
|
|
348
427
|
for (const target of targets) {
|
|
349
|
-
const nxArgs = [
|
|
428
|
+
const nxArgs = [
|
|
429
|
+
'run-many',
|
|
430
|
+
'-t',
|
|
431
|
+
target,
|
|
432
|
+
`--projects=${projectList}`,
|
|
433
|
+
'--exclude=tag:permanent-deploy-target',
|
|
434
|
+
`--parallel=${NX_PARALLEL}`,
|
|
435
|
+
];
|
|
350
436
|
if (target === 'deploy') {
|
|
351
|
-
nxArgs.push(`--
|
|
437
|
+
nxArgs.push(`--environment=${environment}`);
|
|
352
438
|
}
|
|
353
|
-
const status = await
|
|
439
|
+
const status = await runNx(nxArgs, root);
|
|
354
440
|
if (status !== 0) {
|
|
355
|
-
await
|
|
441
|
+
await setStatus('failure');
|
|
356
442
|
throw new Error(`nx ${nxArgs.join(' ')} failed with exit code ${status}`);
|
|
357
443
|
}
|
|
358
444
|
}
|
|
359
|
-
|
|
445
|
+
|
|
446
|
+
if (isPullRequestEnvironment(environment)) {
|
|
447
|
+
const url = `https://app.${environment}.conloca.com`;
|
|
448
|
+
const summaryPath = processEnv.GITHUB_STEP_SUMMARY;
|
|
449
|
+
if (summaryPath) {
|
|
450
|
+
const appendSummary = dependencies.appendSummary ?? appendFile;
|
|
451
|
+
await appendSummary(summaryPath, `## ${environment} deployment\n\n[View deployment](${url})\n`);
|
|
452
|
+
}
|
|
453
|
+
const publishDeployment =
|
|
454
|
+
dependencies.publishDeployment ??
|
|
455
|
+
((token: `pr${number}`, deploymentUrl: string) => publishGithubDeployment(token, deploymentUrl, processEnv));
|
|
456
|
+
await publishDeployment(environment, url);
|
|
457
|
+
}
|
|
458
|
+
if (environment !== 'production') {
|
|
459
|
+
const e2eProjects = await listProjects(root, 'e2e-deployed', 'run-many');
|
|
460
|
+
if (e2eProjects.length > 0) {
|
|
461
|
+
const nxArgs = [
|
|
462
|
+
'run-many',
|
|
463
|
+
'-t',
|
|
464
|
+
'e2e-deployed',
|
|
465
|
+
`--projects=${e2eProjects.join(',')}`,
|
|
466
|
+
`--parallel=${NX_PARALLEL}`,
|
|
467
|
+
`--environment=${environment}`,
|
|
468
|
+
];
|
|
469
|
+
const status = await runNx(nxArgs, root);
|
|
470
|
+
if (status !== 0) {
|
|
471
|
+
await setStatus('failure');
|
|
472
|
+
throw new Error(`nx ${nxArgs.join(' ')} failed with exit code ${status}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await setStatus('success');
|
|
360
478
|
}
|
|
361
479
|
|
|
362
480
|
function resolveNxSmartMode(mode: NxSmartMode): 'affected' | 'run-many' {
|
|
@@ -390,30 +508,183 @@ function eventDefaultBranch(): string | undefined {
|
|
|
390
508
|
}
|
|
391
509
|
}
|
|
392
510
|
|
|
393
|
-
|
|
511
|
+
export function resolveDeploymentEnvironment(
|
|
512
|
+
explicit: string | undefined,
|
|
513
|
+
environment: NodeJS.ProcessEnv,
|
|
514
|
+
event: GithubActionsEventPayload | undefined,
|
|
515
|
+
): EnvironmentToken {
|
|
516
|
+
if (explicit !== undefined) return parseEnvironmentToken(explicit);
|
|
517
|
+
if (environment.GITHUB_EVENT_NAME === 'pull_request') {
|
|
518
|
+
if (!event?.action || !['opened', 'reopened', 'synchronize'].includes(event.action)) {
|
|
519
|
+
throw new Error('Pull-request deployment runs only for opened, reopened, or synchronize events.');
|
|
520
|
+
}
|
|
521
|
+
const repository = event.repository?.full_name;
|
|
522
|
+
const headRepository = event.pull_request?.head?.repo?.full_name;
|
|
523
|
+
if (!repository || !headRepository || repository !== headRepository) {
|
|
524
|
+
throw new Error('Pull-request deployment is restricted to same-repository pull requests.');
|
|
525
|
+
}
|
|
526
|
+
const number = event.pull_request?.number;
|
|
527
|
+
if (number === undefined) throw new Error('GitHub pull_request event is missing its PR number.');
|
|
528
|
+
return pullRequestEnvironment(number);
|
|
529
|
+
}
|
|
530
|
+
if (environment.GITHUB_EVENT_NAME === 'push' && environment.GITHUB_REF_NAME === 'private') {
|
|
531
|
+
return 'staging';
|
|
532
|
+
}
|
|
533
|
+
if (environment.GITHUB_EVENT_NAME === 'release') {
|
|
534
|
+
return 'production';
|
|
535
|
+
}
|
|
536
|
+
throw new Error('Cannot resolve a deployment environment from this GitHub event; pass --environment explicitly.');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function readGithubActionsEvent(environment: NodeJS.ProcessEnv): GithubActionsEventPayload | undefined {
|
|
540
|
+
const eventPath = environment.GITHUB_EVENT_PATH;
|
|
541
|
+
if (!eventPath) return undefined;
|
|
542
|
+
try {
|
|
543
|
+
return parseGithubActionsEvent(readFileSync(eventPath, 'utf8')) || undefined;
|
|
544
|
+
} catch {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function listNxProjectsWithTarget(
|
|
394
550
|
root: string,
|
|
395
|
-
|
|
551
|
+
target: string,
|
|
396
552
|
mode: 'affected' | 'run-many',
|
|
397
553
|
): Promise<string[]> {
|
|
398
|
-
const listArgs =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
554
|
+
const listArgs = ['show', 'projects'];
|
|
555
|
+
if (mode === 'affected') listArgs.push('--affected');
|
|
556
|
+
listArgs.push('--withTarget', target);
|
|
557
|
+
if (target === 'deploy') listArgs.push('--exclude=tag:permanent-deploy-target');
|
|
558
|
+
listArgs.push('--json');
|
|
402
559
|
const result = await $`nx ${listArgs}`.cwd(root).quiet();
|
|
403
|
-
const candidates = nxProjectList(decode(result.stdout));
|
|
404
|
-
|
|
560
|
+
const candidates = nxProjectList(decode(result.stdout)).sort((left, right) => left.localeCompare(right));
|
|
561
|
+
if (target !== 'deploy') return candidates;
|
|
562
|
+
return selectEnvironmentDeployProjects(candidates, async (project) => {
|
|
563
|
+
const projectResult = await $`nx show project ${project} --json`.cwd(root).quiet();
|
|
564
|
+
const parsed = parseNxProjectDeployTarget(decode(projectResult.stdout));
|
|
565
|
+
if (!parsed) throw new Error(`nx show project ${project} returned invalid JSON.`);
|
|
566
|
+
return parsed;
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function selectEnvironmentDeployProjects(
|
|
571
|
+
candidates: string[],
|
|
572
|
+
loadProject: (project: string) => Promise<unknown>,
|
|
573
|
+
): Promise<string[]> {
|
|
574
|
+
const selected: string[] = [];
|
|
405
575
|
for (const project of candidates) {
|
|
406
|
-
|
|
407
|
-
|
|
576
|
+
const definition = await loadProject(project);
|
|
577
|
+
if (!isNxProjectDeployTarget(definition)) continue;
|
|
578
|
+
const deploy = definition.targets?.deploy;
|
|
579
|
+
const commandValue = deploy?.options?.command ?? deploy?.command;
|
|
580
|
+
if (typeof commandValue === 'string' && commandValue.includes('smoo wrangler deploy-environment')) {
|
|
581
|
+
selected.push(project);
|
|
408
582
|
}
|
|
409
583
|
}
|
|
410
|
-
return
|
|
584
|
+
return selected;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export interface GithubApiProcessResult {
|
|
588
|
+
exitCode: number;
|
|
589
|
+
stdout: string;
|
|
590
|
+
stderr: string;
|
|
411
591
|
}
|
|
412
592
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
593
|
+
export interface GithubApiProcessRunner {
|
|
594
|
+
run(args: string[], input: string, cwd: string): Promise<GithubApiProcessResult>;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export class NodeGithubApiProcessRunner implements GithubApiProcessRunner {
|
|
598
|
+
run(args: string[], input: string, cwd: string): Promise<GithubApiProcessResult> {
|
|
599
|
+
const { promise, resolve, reject } = Promise.withResolvers<GithubApiProcessResult>();
|
|
600
|
+
const child = spawn('gh', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
601
|
+
let stdout = '';
|
|
602
|
+
let stderr = '';
|
|
603
|
+
child.stdout.setEncoding('utf8');
|
|
604
|
+
child.stderr.setEncoding('utf8');
|
|
605
|
+
child.stdout.on('data', (chunk: string) => {
|
|
606
|
+
stdout += chunk;
|
|
607
|
+
});
|
|
608
|
+
child.stderr.on('data', (chunk: string) => {
|
|
609
|
+
stderr += chunk;
|
|
610
|
+
});
|
|
611
|
+
child.once('error', reject);
|
|
612
|
+
child.once('close', (code) => {
|
|
613
|
+
resolve({ exitCode: code ?? -1, stdout, stderr });
|
|
614
|
+
});
|
|
615
|
+
child.stdin.end(input);
|
|
616
|
+
return promise;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function publishGithubDeployment(
|
|
621
|
+
environment: `pr${number}`,
|
|
622
|
+
url: string,
|
|
623
|
+
processEnvironment: NodeJS.ProcessEnv,
|
|
624
|
+
runner: GithubApiProcessRunner = new NodeGithubApiProcessRunner(),
|
|
625
|
+
): Promise<void> {
|
|
626
|
+
const repository = processEnvironment.GITHUB_REPOSITORY;
|
|
627
|
+
const sha = processEnvironment.GITHUB_SHA;
|
|
628
|
+
if (!repository || !sha) throw new Error('GITHUB_REPOSITORY and GITHUB_SHA are required to publish a deployment.');
|
|
629
|
+
const createBody = JSON.stringify({
|
|
630
|
+
ref: sha,
|
|
631
|
+
environment,
|
|
632
|
+
auto_merge: false,
|
|
633
|
+
required_contexts: [],
|
|
634
|
+
transient_environment: true,
|
|
635
|
+
production_environment: false,
|
|
636
|
+
});
|
|
637
|
+
const createResult = await runner.run(
|
|
638
|
+
[
|
|
639
|
+
'api',
|
|
640
|
+
'--method',
|
|
641
|
+
'POST',
|
|
642
|
+
'-H',
|
|
643
|
+
'Accept: application/vnd.github+json',
|
|
644
|
+
`/repos/${repository}/deployments`,
|
|
645
|
+
'--input',
|
|
646
|
+
'-',
|
|
647
|
+
],
|
|
648
|
+
createBody,
|
|
649
|
+
process.cwd(),
|
|
650
|
+
);
|
|
651
|
+
if (createResult.exitCode !== 0) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
`GitHub deployment creation failed with exit code ${createResult.exitCode}: ${createResult.stderr.trim()}`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
let deployment: unknown;
|
|
657
|
+
try {
|
|
658
|
+
deployment = JSON.parse(createResult.stdout);
|
|
659
|
+
} catch {
|
|
660
|
+
throw new Error('GitHub returned invalid JSON while creating a deployment.');
|
|
661
|
+
}
|
|
662
|
+
if (!isGithubDeployment(deployment)) throw new Error('GitHub returned an invalid deployment response.');
|
|
663
|
+
const statusBody = JSON.stringify({
|
|
664
|
+
state: 'success',
|
|
665
|
+
environment,
|
|
666
|
+
environment_url: url,
|
|
667
|
+
auto_inactive: false,
|
|
668
|
+
});
|
|
669
|
+
const statusResult = await runner.run(
|
|
670
|
+
[
|
|
671
|
+
'api',
|
|
672
|
+
'--method',
|
|
673
|
+
'POST',
|
|
674
|
+
'-H',
|
|
675
|
+
'Accept: application/vnd.github+json',
|
|
676
|
+
`/repos/${repository}/deployments/${deployment.id}/statuses`,
|
|
677
|
+
'--input',
|
|
678
|
+
'-',
|
|
679
|
+
],
|
|
680
|
+
statusBody,
|
|
681
|
+
process.cwd(),
|
|
682
|
+
);
|
|
683
|
+
if (statusResult.exitCode !== 0) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`GitHub deployment status failed with exit code ${statusResult.exitCode}: ${statusResult.stderr.trim()}`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
417
688
|
}
|
|
418
689
|
|
|
419
690
|
function nxProjectList(output: string): string[] {
|