@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
package/src/lib/json.ts
CHANGED
|
@@ -80,7 +80,7 @@ export type NxDependsOn = string | NxDependsOnObject;
|
|
|
80
80
|
/** Nx target options are executor-specific open bags. */
|
|
81
81
|
export type NxTargetOptions = Record<string, unknown>;
|
|
82
82
|
|
|
83
|
-
export type NxInput = string |
|
|
83
|
+
export type NxInput = string | object;
|
|
84
84
|
|
|
85
85
|
export interface NxTargetConfig {
|
|
86
86
|
executor?: string;
|
package/src/lib/workspace.ts
CHANGED
|
@@ -129,6 +129,7 @@ function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]
|
|
|
129
129
|
const paths: string[] = [];
|
|
130
130
|
for (const pattern of workspacePatterns) {
|
|
131
131
|
if (!pattern.endsWith('/*')) {
|
|
132
|
+
paths.push(join(root, pattern, 'package.json'));
|
|
132
133
|
continue;
|
|
133
134
|
}
|
|
134
135
|
const parent = join(root, pattern.slice(0, -2));
|
|
@@ -24,12 +24,12 @@ describe('CI workflow definition', () => {
|
|
|
24
24
|
const rendered = renderCiWorkflowYaml({ deploy: true, pushBranches: ['main'] });
|
|
25
25
|
|
|
26
26
|
expect(steps.map((step) => [step.kind, step.number])).toContainEqual([CiWorkflowStepKind.Deploy, 11]);
|
|
27
|
-
expect(rendered
|
|
27
|
+
expect(rendered.match(/- name: 🚀 Deploy Environment/g)).toHaveLength(1);
|
|
28
28
|
expect(rendered).not.toContain('CLOUDFLARE_API_TOKEN');
|
|
29
29
|
expect(rendered).not.toContain('CLOUDFLARE_ACCOUNT_ID');
|
|
30
|
-
expect(rendered).toContain(
|
|
31
|
-
|
|
32
|
-
);
|
|
30
|
+
expect(rendered).toContain('smoo github-ci nx-deploy --mode run-many --name "Deploy Environment" --step 11');
|
|
31
|
+
expect(rendered).toContain('github.event.pull_request.head.repo.full_name == github.repository');
|
|
32
|
+
expect(rendered).toContain("github.ref == 'refs/heads/private'");
|
|
33
33
|
expect(rendered).toContain("# Step 12\n # Nx's database cache needs artifact files");
|
|
34
34
|
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
35
35
|
expect(rendered).toContain('id: setup');
|
|
@@ -45,6 +45,12 @@ describe('CI workflow definition', () => {
|
|
|
45
45
|
|
|
46
46
|
expect(rendered).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
|
|
47
47
|
expect(rendered).toContain('CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}');
|
|
48
|
+
expect(rendered).toContain('GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}');
|
|
49
|
+
expect(rendered).toContain('GITHUB_APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}');
|
|
50
|
+
expect(rendered).toContain('GITHUB_APP_PRIVATE_KEY_PEM: ${{ secrets.GITHUB_APP_PRIVATE_KEY_PEM }}');
|
|
51
|
+
expect(rendered).toContain('OAUTH_STATE_SIGNING_KEY: ${{ secrets.OAUTH_STATE_SIGNING_KEY }}');
|
|
52
|
+
expect(rendered).toContain('MAIL_CAPTURE_CONTROL_TOKEN: ${{ secrets.MAIL_CAPTURE_CONTROL_TOKEN }}');
|
|
53
|
+
expect(rendered).toContain('TOKEN_ENCRYPTION_KEY: ${{ secrets.TOKEN_ENCRYPTION_KEY }}');
|
|
48
54
|
});
|
|
49
55
|
|
|
50
56
|
it('uses the same architecture-scoped key to restore and save the Nx cache', async () => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: Assertions verify emitted GitHub Actions expressions literally. */
|
|
2
|
+
import { describe, expect, it } from 'bun:test';
|
|
3
|
+
import { managedFileTargetsForTest } from '../managed-files.js';
|
|
4
|
+
import { renderPrPreviewCleanupWorkflowYaml } from '../pr-preview-cleanup-workflow.js';
|
|
5
|
+
|
|
6
|
+
describe('PR preview cleanup workflow', () => {
|
|
7
|
+
it('renders one close-only same-repository cleanup job from the PR number', () => {
|
|
8
|
+
const rendered = renderPrPreviewCleanupWorkflowYaml({ runsOn: ['nixos-latest-x64', 'self-hosted'] });
|
|
9
|
+
expect(managedFileTargetsForTest).toContainEqual({
|
|
10
|
+
target: '.github/workflows/pr-preview-cleanup.yml',
|
|
11
|
+
executable: undefined,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(rendered).toContain('pull_request:\n types: [closed]');
|
|
15
|
+
expect(rendered).not.toContain('opened');
|
|
16
|
+
expect(rendered).not.toContain('synchronize');
|
|
17
|
+
expect(rendered).toContain('if: github.event.pull_request.head.repo.full_name == github.repository');
|
|
18
|
+
expect(rendered.match(/smoo wrangler cleanup-pr --pr/g)).toHaveLength(1);
|
|
19
|
+
expect(rendered).toContain('smoo wrangler cleanup-pr --pr ${{ github.event.pull_request.number }}');
|
|
20
|
+
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
21
|
+
expect(rendered).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -99,7 +99,7 @@ describe('publish workflow definition', () => {
|
|
|
99
99
|
|
|
100
100
|
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 15 }, (_, index) => index + 1));
|
|
101
101
|
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 19 }, (_, index) => index + 1));
|
|
102
|
-
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length:
|
|
102
|
+
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 8 }, (_, index) => index + 1));
|
|
103
103
|
expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1));
|
|
104
104
|
expect(singleJob).toContain('# Step 14\n - name: 📦 Publish release (${{ steps.version.outputs.mode }})');
|
|
105
105
|
expect(singleJob).toContain('# Step 15\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
@@ -109,7 +109,7 @@ describe('publish workflow definition', () => {
|
|
|
109
109
|
);
|
|
110
110
|
expect(linuxCandidate).toContain('# Step 19\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
111
111
|
expect(macosPlatform).toContain('# Step 5\n - name: 🍎 Build selected macOS and iOS release outputs');
|
|
112
|
-
expect(macosPlatform).toContain('# Step
|
|
112
|
+
expect(macosPlatform).toContain('# Step 8\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
113
113
|
expect(finalJob).toContain('# Step 7\n - name: 🧯 Repair pending releases');
|
|
114
114
|
expect(finalJob).toContain('# Step 8\n - name: ♻️ Restore validated release state');
|
|
115
115
|
expect(finalJob).toContain('# Step 9\n - name: 📦 Apply verified Linux outputs');
|
|
@@ -156,12 +156,20 @@ describe('publish workflow definition', () => {
|
|
|
156
156
|
expect(linuxCandidate).toContain(
|
|
157
157
|
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
158
158
|
);
|
|
159
|
-
expect(macosPlatform).not.toContain('smoo github-ci nx-run-many');
|
|
160
159
|
expect(foldedRunCommand(macosPlatform, '🍎 Build selected macOS and iOS release outputs')).toBe(
|
|
161
160
|
`smoo release build-platform-outputs --bump "\${{ inputs.bump }}" --ref "\${{ github.sha }}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(
|
|
162
161
|
',',
|
|
163
162
|
)}" --output "\${{ runner.temp }}/macos-platform-outputs"`,
|
|
164
163
|
);
|
|
164
|
+
expect(macosPlatform).toContain(
|
|
165
|
+
`run: smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
|
|
166
|
+
);
|
|
167
|
+
expect(macosPlatform.indexOf('- name: 🍎 Build selected macOS and iOS release outputs')).toBeLessThan(
|
|
168
|
+
macosPlatform.indexOf('- name: 🧪 Unit test macOS and iOS packages'),
|
|
169
|
+
);
|
|
170
|
+
expect(macosPlatform.indexOf('- name: 🧪 Unit test macOS and iOS packages')).toBeLessThan(
|
|
171
|
+
macosPlatform.indexOf('- name: 📤 Upload macOS platform outputs'),
|
|
172
|
+
);
|
|
165
173
|
expect(finalJob).not.toContain('smoo release version');
|
|
166
174
|
expect(finalJob).not.toContain('nx-run-many');
|
|
167
175
|
expect(foldedRunCommand(finalJob, '🧯 Repair pending releases')).toBe(
|
|
@@ -253,7 +261,7 @@ describe('publish workflow definition', () => {
|
|
|
253
261
|
expect(rendered).not.toContain('CLOUDFLARE_API_TOKEN');
|
|
254
262
|
expect(rendered).not.toContain('CLOUDFLARE_ACCOUNT_ID');
|
|
255
263
|
expect(rendered).toContain(
|
|
256
|
-
'smoo github-ci nx-deploy --
|
|
264
|
+
'smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
|
|
257
265
|
);
|
|
258
266
|
});
|
|
259
267
|
|
|
@@ -47,7 +47,7 @@ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkfl
|
|
|
47
47
|
{ kind: CiWorkflowStepKind.ManagedFilesDispatch, name: '🔁 Dispatch managed-file drift healing' },
|
|
48
48
|
];
|
|
49
49
|
if (options.deploy) {
|
|
50
|
-
steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy
|
|
50
|
+
steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy Environment' });
|
|
51
51
|
}
|
|
52
52
|
steps.push(
|
|
53
53
|
{ kind: CiWorkflowStepKind.SaveNxCache, name: '💾 Save Nx cache' },
|
|
@@ -75,7 +75,7 @@ permissions:
|
|
|
75
75
|
# actions:write lets the drift step dispatch the managed-files workflow.
|
|
76
76
|
actions: write
|
|
77
77
|
contents: read
|
|
78
|
-
statuses: write
|
|
78
|
+
${options.deploy ? ' deployments: write\n' : ''} statuses: write
|
|
79
79
|
|
|
80
80
|
defaults:
|
|
81
81
|
run:
|
|
@@ -197,11 +197,15 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
197
197
|
case CiWorkflowStepKind.Deploy:
|
|
198
198
|
return [
|
|
199
199
|
` - name: ${step.name}`,
|
|
200
|
-
' if:',
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
' if: >-',
|
|
201
|
+
' ${{',
|
|
202
|
+
" (github.event_name == 'pull_request' &&",
|
|
203
|
+
' contains(fromJSON(\'["opened","reopened","synchronize"]\'), github.event.action) &&',
|
|
204
|
+
' github.event.pull_request.head.repo.full_name == github.repository) ||',
|
|
205
|
+
" (github.event_name == 'push' && github.ref == 'refs/heads/private')",
|
|
206
|
+
' }}',
|
|
203
207
|
...deployEnvLines(options),
|
|
204
|
-
` run: smoo github-ci nx-deploy --
|
|
208
|
+
` run: smoo github-ci nx-deploy --mode run-many --name "Deploy Environment" --step ${step.number}`,
|
|
205
209
|
];
|
|
206
210
|
case CiWorkflowStepKind.SaveNxCache:
|
|
207
211
|
return [
|
|
@@ -251,6 +255,12 @@ function deployEnvLines(options: CiWorkflowDefinitionOptions): string[] {
|
|
|
251
255
|
' env:',
|
|
252
256
|
' CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}',
|
|
253
257
|
' 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 }}',
|
|
254
264
|
];
|
|
255
265
|
}
|
|
256
266
|
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are asserted literally. */
|
|
2
2
|
import { describe, expect, it } from 'bun:test';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { LINUX_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
6
7
|
import fc from 'fast-check';
|
|
8
|
+
import { type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
7
9
|
import {
|
|
8
10
|
deployTargetInfoFromProjects,
|
|
9
11
|
extractInlineLocalBlocksForTest,
|
|
10
12
|
INLINE_LOCAL_BEGIN,
|
|
11
13
|
INLINE_LOCAL_END,
|
|
12
14
|
LOCAL_SECTION_MARKER,
|
|
13
|
-
|
|
15
|
+
managedFileTargetsForTest,
|
|
14
16
|
platformTargetGlobsForTest,
|
|
15
17
|
reinsertInlineLocalBlocksForTest,
|
|
16
18
|
splitLocalSectionForTest,
|
|
17
|
-
targetNamesFromProjects,
|
|
18
19
|
} from './managed-files.js';
|
|
19
20
|
|
|
20
21
|
const MANAGED = '# managed content\npath merge=driver\n';
|
|
@@ -176,48 +177,111 @@ describe('managed publish platform discovery', () => {
|
|
|
176
177
|
});
|
|
177
178
|
|
|
178
179
|
describe('nx graph project helpers', () => {
|
|
179
|
-
const
|
|
180
|
+
const sampleProjects: NxProjects = {
|
|
180
181
|
lib: {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
lint: {},
|
|
186
|
-
},
|
|
182
|
+
targets: {
|
|
183
|
+
build: {},
|
|
184
|
+
'bundle-linux': {},
|
|
185
|
+
lint: {},
|
|
187
186
|
},
|
|
188
187
|
},
|
|
189
188
|
app: {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
production: { options: { command: 'wrangler deploy --env production' } },
|
|
197
|
-
},
|
|
189
|
+
targets: {
|
|
190
|
+
deploy: {
|
|
191
|
+
options: { command: 'echo no' },
|
|
192
|
+
configurations: {
|
|
193
|
+
staging: { command: 'wrangler deploy --env staging' },
|
|
194
|
+
production: { options: { command: 'wrangler deploy --env production' } },
|
|
198
195
|
},
|
|
199
|
-
'package-macos': {},
|
|
200
196
|
},
|
|
197
|
+
'package-macos': {},
|
|
201
198
|
},
|
|
202
199
|
},
|
|
203
200
|
};
|
|
204
201
|
|
|
205
202
|
it('collects target names from graph nodes without project names', () => {
|
|
206
|
-
expect(targetNamesFromProjects(
|
|
203
|
+
expect(targetNamesFromProjects(sampleProjects).sort()).toEqual(
|
|
207
204
|
['build', 'bundle-linux', 'deploy', 'lint', 'package-macos'].sort(),
|
|
208
205
|
);
|
|
209
206
|
});
|
|
210
207
|
|
|
211
208
|
it('detects deploy configurations and cloudflare provider from graph nodes', () => {
|
|
212
|
-
expect(deployTargetInfoFromProjects(
|
|
209
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({
|
|
213
210
|
exists: true,
|
|
214
211
|
provider: 'cloudflare',
|
|
215
212
|
});
|
|
216
|
-
expect(deployTargetInfoFromProjects(
|
|
213
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'production')).toEqual({
|
|
217
214
|
exists: true,
|
|
218
215
|
provider: 'cloudflare',
|
|
219
216
|
});
|
|
220
|
-
expect(deployTargetInfoFromProjects(
|
|
217
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('recognizes convention-driven deploy-environment targets without per-environment configurations', () => {
|
|
221
|
+
const projects: NxProjects = {
|
|
222
|
+
app: {
|
|
223
|
+
targets: {
|
|
224
|
+
deploy: {
|
|
225
|
+
options: { command: 'smoo wrangler deploy-environment --environment {args.environment}' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
expect(deployTargetInfoFromProjects(projects, 'staging')).toEqual({ exists: true, provider: 'cloudflare' });
|
|
232
|
+
expect(deployTargetInfoFromProjects(projects, 'production')).toEqual({ exists: true, provider: 'cloudflare' });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('managed raw files', () => {
|
|
237
|
+
it('manages the devenv wrapper as an executable byte-exact copy', async () => {
|
|
238
|
+
expect(managedFileTargetsForTest).toContainEqual({ target: 'tooling/devenv', executable: true });
|
|
239
|
+
|
|
240
|
+
const [source, generated] = await Promise.all([
|
|
241
|
+
readFile(join(REPO_ROOT, 'packages', 'cli', 'managed', 'raw', 'tooling', 'devenv'), 'utf8'),
|
|
242
|
+
readFile(join(REPO_ROOT, 'tooling', 'devenv'), 'utf8'),
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
expect(generated).toBe(source);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('exports the restored ttsc cache path while preserving host cache overrides', async () => {
|
|
249
|
+
const temp = await mkdtemp(join(tmpdir(), 'smoo-github-bootstrap-'));
|
|
250
|
+
const bin = join(temp, 'bin');
|
|
251
|
+
await mkdir(bin);
|
|
252
|
+
const devenv = join(bin, 'devenv');
|
|
253
|
+
await writeFile(devenv, '#!/bin/sh\nexit 0\n');
|
|
254
|
+
await chmod(devenv, 0o755);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const cases = [
|
|
258
|
+
{ input: '', expected: join(REPO_ROOT, '.cache', 'ttsc') },
|
|
259
|
+
{ input: join(temp, 'host-ttsc'), expected: join(temp, 'host-ttsc') },
|
|
260
|
+
];
|
|
261
|
+
for (const [index, cache] of cases.entries()) {
|
|
262
|
+
const githubEnv = join(temp, `github-env-${index}`);
|
|
263
|
+
const githubPath = join(temp, `github-path-${index}`);
|
|
264
|
+
const process = Bun.spawn(
|
|
265
|
+
[join(REPO_ROOT, 'tooling', 'direnv', 'github-actions-bootstrap.sh'), 'build-shell'],
|
|
266
|
+
{
|
|
267
|
+
cwd: join(REPO_ROOT, 'tooling', 'direnv'),
|
|
268
|
+
env: {
|
|
269
|
+
...Bun.env,
|
|
270
|
+
GITHUB_ENV: githubEnv,
|
|
271
|
+
GITHUB_PATH: githubPath,
|
|
272
|
+
PATH: `${bin}:${Bun.env.PATH ?? ''}`,
|
|
273
|
+
TTSC_CACHE_DIR: cache.input,
|
|
274
|
+
},
|
|
275
|
+
stderr: 'pipe',
|
|
276
|
+
stdout: 'pipe',
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
expect(await process.exited).toBe(0);
|
|
280
|
+
expect(await readFile(githubEnv, 'utf8')).toContain(`TTSC_CACHE_DIR=${cache.expected}\n`);
|
|
281
|
+
}
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(temp, { recursive: true, force: true });
|
|
284
|
+
}
|
|
221
285
|
});
|
|
222
286
|
});
|
|
223
287
|
|
|
@@ -2,11 +2,11 @@ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFi
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
5
|
-
|
|
6
|
-
import { createProjectGraphAsync } from 'nx/src/project-graph/project-graph.js';
|
|
7
|
-
import type { PackageJson } from '../lib/json.js';
|
|
5
|
+
import type { NxTargetConfig, PackageJson } from '../lib/json.js';
|
|
8
6
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
7
|
+
import { loadNxProjects, type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
9
8
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
9
|
+
import { renderPrPreviewCleanupWorkflowYaml } from './pr-preview-cleanup-workflow.js';
|
|
10
10
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
11
11
|
|
|
12
12
|
type ManagedKind = 'raw' | 'template' | 'generated';
|
|
@@ -25,6 +25,7 @@ interface ManagedFile {
|
|
|
25
25
|
target: string;
|
|
26
26
|
executable?: boolean;
|
|
27
27
|
releasePackagesOnly?: boolean;
|
|
28
|
+
cloudflareDeployOnly?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** Split a managed target's content into the managed part and the repo-owned tail. */
|
|
@@ -155,6 +156,12 @@ const managedFiles: ManagedFile[] = [
|
|
|
155
156
|
source: 'envrc',
|
|
156
157
|
target: '.envrc',
|
|
157
158
|
},
|
|
159
|
+
{
|
|
160
|
+
kind: 'raw',
|
|
161
|
+
source: 'tooling/devenv',
|
|
162
|
+
target: 'tooling/devenv',
|
|
163
|
+
executable: true,
|
|
164
|
+
},
|
|
158
165
|
{
|
|
159
166
|
kind: 'raw',
|
|
160
167
|
source: 'tooling/direnv/repo-path',
|
|
@@ -201,16 +208,17 @@ const managedFiles: ManagedFile[] = [
|
|
|
201
208
|
target: 'tooling/direnv/merge-newer-pins.sh',
|
|
202
209
|
executable: true,
|
|
203
210
|
},
|
|
204
|
-
{
|
|
205
|
-
kind: 'raw',
|
|
206
|
-
source: 'patches/ttsc@0.19.3.patch',
|
|
207
|
-
target: 'patches/ttsc@0.19.3.patch',
|
|
208
|
-
},
|
|
209
211
|
{
|
|
210
212
|
kind: 'generated',
|
|
211
213
|
source: 'ci-workflow',
|
|
212
214
|
target: '.github/workflows/ci.yml',
|
|
213
215
|
},
|
|
216
|
+
{
|
|
217
|
+
kind: 'generated',
|
|
218
|
+
source: 'pr-preview-cleanup-workflow',
|
|
219
|
+
target: '.github/workflows/pr-preview-cleanup.yml',
|
|
220
|
+
cloudflareDeployOnly: true,
|
|
221
|
+
},
|
|
214
222
|
{
|
|
215
223
|
kind: 'generated',
|
|
216
224
|
source: 'publish-workflow',
|
|
@@ -254,6 +262,8 @@ const managedFiles: ManagedFile[] = [
|
|
|
254
262
|
},
|
|
255
263
|
];
|
|
256
264
|
|
|
265
|
+
export const managedFileTargetsForTest = managedFiles.map(({ target, executable }) => ({ target, executable }));
|
|
266
|
+
|
|
257
267
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
258
268
|
|
|
259
269
|
export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
|
|
@@ -270,6 +280,9 @@ function applyManagedFile(
|
|
|
270
280
|
if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
|
|
271
281
|
return { target: file.target, action: 'skipped' };
|
|
272
282
|
}
|
|
283
|
+
if (file.cloudflareDeployOnly === true && context.stagingDeployProvider !== 'cloudflare') {
|
|
284
|
+
return { target: file.target, action: 'skipped' };
|
|
285
|
+
}
|
|
273
286
|
const target = resolve(root, file.target);
|
|
274
287
|
const content = getManagedContent(file, context);
|
|
275
288
|
if (existsSync(target)) {
|
|
@@ -320,6 +333,9 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
320
333
|
runsOn: context.ciRunsOn,
|
|
321
334
|
});
|
|
322
335
|
}
|
|
336
|
+
if (file.source === 'pr-preview-cleanup-workflow') {
|
|
337
|
+
return renderPrPreviewCleanupWorkflowYaml({ runsOn: context.ciRunsOn });
|
|
338
|
+
}
|
|
323
339
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
324
340
|
}
|
|
325
341
|
const sourceRoot = file.kind === 'raw' ? 'managed/raw' : 'managed/templates';
|
|
@@ -337,7 +353,7 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
|
|
|
337
353
|
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
338
354
|
const ciRunsOn = getCiRunsOn(packageJson?.json);
|
|
339
355
|
// In-process Nx API → daemon socket (no second Node/`nx` CLI process).
|
|
340
|
-
const nxProjects = await
|
|
356
|
+
const nxProjects = await loadNxProjects(root);
|
|
341
357
|
const stagingDeploy = deployTargetInfoFromProjects(nxProjects, 'staging');
|
|
342
358
|
const productionDeploy = deployTargetInfoFromProjects(nxProjects, 'production');
|
|
343
359
|
const platformTargetGlobs = platformTargetGlobsForTest(targetNamesFromProjects(nxProjects));
|
|
@@ -366,67 +382,12 @@ export function platformTargetGlobsForTest(targetNames: Iterable<string>): strin
|
|
|
366
382
|
});
|
|
367
383
|
}
|
|
368
384
|
|
|
369
|
-
/** Resolved Nx project node as returned under `graph.nodes` / project-graph.json. */
|
|
370
|
-
export type NxGraphProjectNode = {
|
|
371
|
-
data?: { targets?: Record<string, NxGraphTarget> };
|
|
372
|
-
targets?: Record<string, NxGraphTarget>;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
type NxGraphTarget = {
|
|
376
|
-
command?: string;
|
|
377
|
-
options?: { command?: string };
|
|
378
|
-
configurations?: Record<string, { command?: string; options?: { command?: string } }>;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Load resolved project nodes via Nx's programmatic API.
|
|
383
|
-
*
|
|
384
|
-
* `createProjectGraphAsync` is what the CLI uses: with the daemon up it speaks the
|
|
385
|
-
* daemon unix socket instead of rebuilding the graph in a fresh Node process.
|
|
386
|
-
* Spawning `nx graph` / `nx show` only added CLI boot + plugin-worker overhead on
|
|
387
|
-
* top of that. smoo runs under Bun; Bun can import this CJS module directly.
|
|
388
|
-
*/
|
|
389
|
-
async function loadNxGraphProjects(root: string): Promise<Record<string, NxGraphProjectNode>> {
|
|
390
|
-
const prevCwd = process.cwd();
|
|
391
|
-
process.chdir(root);
|
|
392
|
-
try {
|
|
393
|
-
const graph = (await createProjectGraphAsync()) as {
|
|
394
|
-
nodes: Record<string, { data?: { targets?: Record<string, NxGraphTarget> } }>;
|
|
395
|
-
};
|
|
396
|
-
const out: Record<string, NxGraphProjectNode> = {};
|
|
397
|
-
for (const [name, node] of Object.entries(graph.nodes)) {
|
|
398
|
-
out[name] = { data: { targets: node.data?.targets ?? {} } };
|
|
399
|
-
}
|
|
400
|
-
return out;
|
|
401
|
-
} finally {
|
|
402
|
-
process.chdir(prevCwd);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function targetsOfProject(node: NxGraphProjectNode): Record<string, NxGraphTarget> {
|
|
407
|
-
return node.data?.targets ?? node.targets ?? {};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Test seam: collect target names from a graph.nodes map. */
|
|
411
|
-
export function targetNamesFromProjects(nodes: Record<string, NxGraphProjectNode>): string[] {
|
|
412
|
-
const targetNames = new Set<string>();
|
|
413
|
-
for (const node of Object.values(nodes)) {
|
|
414
|
-
for (const name of Object.keys(targetsOfProject(node))) {
|
|
415
|
-
targetNames.add(name);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return [...targetNames];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
385
|
/** Test seam: deploy configuration presence/provider from graph nodes. */
|
|
422
|
-
export function deployTargetInfoFromProjects(
|
|
423
|
-
nodes: Record<string, NxGraphProjectNode>,
|
|
424
|
-
configuration: string,
|
|
425
|
-
): DeployTargetInfo {
|
|
386
|
+
export function deployTargetInfoFromProjects(projects: NxProjects, configuration: string): DeployTargetInfo {
|
|
426
387
|
let exists = false;
|
|
427
388
|
let provider: DeployTargetInfo['provider'];
|
|
428
|
-
for (const
|
|
429
|
-
const info = deployTargetInfoFromTargets(
|
|
389
|
+
for (const project of Object.values(projects)) {
|
|
390
|
+
const info = deployTargetInfoFromTargets(project.targets ?? {}, configuration);
|
|
430
391
|
if (!info.exists) {
|
|
431
392
|
continue;
|
|
432
393
|
}
|
|
@@ -436,16 +397,21 @@ export function deployTargetInfoFromProjects(
|
|
|
436
397
|
return { exists, provider };
|
|
437
398
|
}
|
|
438
399
|
|
|
439
|
-
function deployTargetInfoFromTargets(targets: Record<string,
|
|
400
|
+
function deployTargetInfoFromTargets(targets: Record<string, NxTargetConfig>, configuration: string): DeployTargetInfo {
|
|
440
401
|
const deploy = targets.deploy;
|
|
441
402
|
if (!deploy) {
|
|
442
403
|
return { exists: false };
|
|
443
404
|
}
|
|
405
|
+
const baseCommandValue = deploy.options?.command ?? deploy.command;
|
|
406
|
+
const baseCommand = typeof baseCommandValue === 'string' ? baseCommandValue : '';
|
|
407
|
+
if (baseCommand.includes('smoo wrangler deploy-environment')) {
|
|
408
|
+
return { exists: true, provider: 'cloudflare' };
|
|
409
|
+
}
|
|
444
410
|
const config = deploy.configurations?.[configuration];
|
|
445
411
|
if (!config) {
|
|
446
412
|
return { exists: false };
|
|
447
413
|
}
|
|
448
|
-
const commandValue = config.command ?? config.options?.command ??
|
|
414
|
+
const commandValue = config.command ?? config.options?.command ?? baseCommand;
|
|
449
415
|
const command = typeof commandValue === 'string' ? commandValue : '';
|
|
450
416
|
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
451
417
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
|
|
12
12
|
const TIMEOUT_FLAG = `--timeout=${BOUNDED_TEST_PER_TEST_TIMEOUT_MS}`;
|
|
13
13
|
|
|
14
|
+
import { validateCommitMessage } from './commit-msg.js';
|
|
15
|
+
|
|
14
16
|
import {
|
|
15
17
|
applyFixableMonorepoDefaults,
|
|
16
18
|
applyNxPluginDefaults,
|
|
@@ -245,6 +247,35 @@ describe('Nx project name policy', () => {
|
|
|
245
247
|
await rm(root, { recursive: true, force: true });
|
|
246
248
|
}
|
|
247
249
|
});
|
|
250
|
+
|
|
251
|
+
it('uses nx.name from an exact workspace package as the commit scope', async () => {
|
|
252
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
253
|
+
try {
|
|
254
|
+
await writeJson(join(root, 'package.json'), {
|
|
255
|
+
name: '@smoothbricks/codebase',
|
|
256
|
+
version: '0.0.0',
|
|
257
|
+
private: true,
|
|
258
|
+
workspaces: ['tooling'],
|
|
259
|
+
});
|
|
260
|
+
await writeJson(join(root, 'tooling/package.json'), {
|
|
261
|
+
name: '@smoothbricks/internal-tooling',
|
|
262
|
+
private: true,
|
|
263
|
+
nx: { name: 'tooling' },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const validScopes = listValidCommitScopes(root);
|
|
267
|
+
expect(validScopes).toEqual(new Set(['release', 'tooling']));
|
|
268
|
+
expect(validateCommitMessage('fix(tooling): repair tooling\n', { validScopes })).toBeNull();
|
|
269
|
+
expect(validateCommitMessage('fix(internal-tooling): repair tooling\n', { validScopes })).toContain(
|
|
270
|
+
'Invalid conventional commit scope',
|
|
271
|
+
);
|
|
272
|
+
expect(validateCommitMessage('fix(arbitrary): repair tooling\n', { validScopes })).toContain(
|
|
273
|
+
'Invalid conventional commit scope',
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(root, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
248
279
|
});
|
|
249
280
|
|
|
250
281
|
describe('workspace package script policy', () => {
|
|
@@ -165,7 +165,7 @@ export function applyWorkspaceDependencyDefaults(root: string, options: PackageT
|
|
|
165
165
|
} else {
|
|
166
166
|
console.log('unchanged tsconfig.test.json policy');
|
|
167
167
|
}
|
|
168
|
-
if (applyWorkspaceBoundedTestTargetPolicy(root)) {
|
|
168
|
+
if (applyWorkspaceBoundedTestTargetPolicy(root, options)) {
|
|
169
169
|
console.log('updated package test targets bounded execution policy');
|
|
170
170
|
} else {
|
|
171
171
|
console.log('unchanged package test targets bounded execution policy');
|
|
@@ -409,7 +409,7 @@ export function validateWorkspaceDependencies(root: string, options: PackageTarg
|
|
|
409
409
|
console.error(`${issue.path}: ${issue.message}`);
|
|
410
410
|
failures++;
|
|
411
411
|
}
|
|
412
|
-
for (const issue of checkWorkspaceBoundedTestTargetPolicy(root)) {
|
|
412
|
+
for (const issue of checkWorkspaceBoundedTestTargetPolicy(root, options)) {
|
|
413
413
|
console.error(`${issue.path}: ${issue.message}`);
|
|
414
414
|
failures++;
|
|
415
415
|
}
|
|
@@ -414,10 +414,12 @@ export function resolvedTargetsByProject(projects: ProjectTargets[]): Map<string
|
|
|
414
414
|
projects.map((project) => [
|
|
415
415
|
project.project,
|
|
416
416
|
{
|
|
417
|
+
...(project.root ? { root: project.root } : {}),
|
|
417
418
|
targets: new Set(project.targets),
|
|
418
419
|
...(project.buildDependsOn ? { buildDependsOn: project.buildDependsOn } : {}),
|
|
419
420
|
...(project.targetDependencies ? { targetDependencies: project.targetDependencies } : {}),
|
|
420
421
|
...(project.targetExecutors ? { targetExecutors: project.targetExecutors } : {}),
|
|
422
|
+
...(project.targetOptions ? { targetOptions: project.targetOptions } : {}),
|
|
421
423
|
...(project.targetScripts ? { targetScripts: project.targetScripts } : {}),
|
|
422
424
|
},
|
|
423
425
|
]),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
|
+
|
|
3
|
+
import { renderRunsOnLine } from './github-runs-on.js';
|
|
4
|
+
|
|
5
|
+
export interface PrPreviewCleanupWorkflowOptions {
|
|
6
|
+
runsOn?: string | string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderPrPreviewCleanupWorkflowYaml(options: PrPreviewCleanupWorkflowOptions = {}): string {
|
|
10
|
+
return `name: PR Preview Cleanup
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
types: [closed]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
cleanup:
|
|
21
|
+
name: Cleanup PR environment
|
|
22
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
23
|
+
${renderRunsOnLine(options.runsOn)}
|
|
24
|
+
timeout-minutes: 15
|
|
25
|
+
defaults:
|
|
26
|
+
run:
|
|
27
|
+
working-directory: tooling/direnv
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@v6.0.2
|
|
31
|
+
with:
|
|
32
|
+
filter: blob:none
|
|
33
|
+
fetch-depth: 1
|
|
34
|
+
|
|
35
|
+
- name: Setup Nix/devenv
|
|
36
|
+
uses: ./.github/actions/setup-devenv
|
|
37
|
+
|
|
38
|
+
- name: Cleanup PR environment
|
|
39
|
+
env:
|
|
40
|
+
CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
41
|
+
CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
42
|
+
run: smoo wrangler cleanup-pr --pr \${{ github.event.pull_request.number }}
|
|
43
|
+
`;
|
|
44
|
+
}
|