@smoothbricks/cli 0.10.7 → 0.10.9
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 -4
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +220 -34
- package/dist/monorepo/ci-workflow.js +16 -6
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +19 -1
- 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/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +85 -5
- 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 +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +31 -6
- package/dist/wrangler/cloudflare.d.ts +87 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -0
- package/dist/wrangler/cloudflare.js +238 -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/direnv/github-actions-bootstrap.sh +3 -4
- package/package.json +9 -2
- package/src/cli.ts +27 -3
- package/src/github-ci/index.test.ts +175 -2
- package/src/github-ci/index.ts +274 -31
- 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 +7 -5
- package/src/monorepo/ci-workflow.ts +16 -6
- package/src/monorepo/managed-files.test.ts +56 -1
- package/src/monorepo/managed-files.ts +20 -1
- package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
- package/src/monorepo/publish-workflow.ts +8 -5
- package/src/monorepo/tool-validation.test.ts +85 -0
- package/src/monorepo/tool-validation.ts +94 -5
- 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 +30 -4
- package/src/wrangler/cloudflare.test.ts +76 -0
- package/src/wrangler/cloudflare.ts +292 -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
|
@@ -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,6 +1,7 @@
|
|
|
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';
|
|
@@ -215,6 +216,21 @@ describe('nx graph project helpers', () => {
|
|
|
215
216
|
});
|
|
216
217
|
expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
|
|
217
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
|
+
});
|
|
218
234
|
});
|
|
219
235
|
|
|
220
236
|
describe('managed raw files', () => {
|
|
@@ -228,6 +244,45 @@ describe('managed raw files', () => {
|
|
|
228
244
|
|
|
229
245
|
expect(generated).toBe(source);
|
|
230
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
|
+
}
|
|
285
|
+
});
|
|
231
286
|
});
|
|
232
287
|
|
|
233
288
|
describe('managed cache actions', () => {
|
|
@@ -6,6 +6,7 @@ import type { NxTargetConfig, PackageJson } from '../lib/json.js';
|
|
|
6
6
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
7
7
|
import { loadNxProjects, type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
8
8
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
9
|
+
import { renderPrPreviewCleanupWorkflowYaml } from './pr-preview-cleanup-workflow.js';
|
|
9
10
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
10
11
|
|
|
11
12
|
type ManagedKind = 'raw' | 'template' | 'generated';
|
|
@@ -24,6 +25,7 @@ interface ManagedFile {
|
|
|
24
25
|
target: string;
|
|
25
26
|
executable?: boolean;
|
|
26
27
|
releasePackagesOnly?: boolean;
|
|
28
|
+
cloudflareDeployOnly?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** Split a managed target's content into the managed part and the repo-owned tail. */
|
|
@@ -211,6 +213,12 @@ const managedFiles: ManagedFile[] = [
|
|
|
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',
|
|
@@ -272,6 +280,9 @@ function applyManagedFile(
|
|
|
272
280
|
if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
|
|
273
281
|
return { target: file.target, action: 'skipped' };
|
|
274
282
|
}
|
|
283
|
+
if (file.cloudflareDeployOnly === true && context.stagingDeployProvider !== 'cloudflare') {
|
|
284
|
+
return { target: file.target, action: 'skipped' };
|
|
285
|
+
}
|
|
275
286
|
const target = resolve(root, file.target);
|
|
276
287
|
const content = getManagedContent(file, context);
|
|
277
288
|
if (existsSync(target)) {
|
|
@@ -322,6 +333,9 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
322
333
|
runsOn: context.ciRunsOn,
|
|
323
334
|
});
|
|
324
335
|
}
|
|
336
|
+
if (file.source === 'pr-preview-cleanup-workflow') {
|
|
337
|
+
return renderPrPreviewCleanupWorkflowYaml({ runsOn: context.ciRunsOn });
|
|
338
|
+
}
|
|
325
339
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
326
340
|
}
|
|
327
341
|
const sourceRoot = file.kind === 'raw' ? 'managed/raw' : 'managed/templates';
|
|
@@ -388,11 +402,16 @@ function deployTargetInfoFromTargets(targets: Record<string, NxTargetConfig>, co
|
|
|
388
402
|
if (!deploy) {
|
|
389
403
|
return { exists: false };
|
|
390
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
|
+
}
|
|
391
410
|
const config = deploy.configurations?.[configuration];
|
|
392
411
|
if (!config) {
|
|
393
412
|
return { exists: false };
|
|
394
413
|
}
|
|
395
|
-
const commandValue = config.command ?? config.options?.command ??
|
|
414
|
+
const commandValue = config.command ?? config.options?.command ?? baseCommand;
|
|
396
415
|
const command = typeof commandValue === 'string' ? commandValue : '';
|
|
397
416
|
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
398
417
|
}
|
|
@@ -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
|
+
}
|
|
@@ -486,7 +486,7 @@ function deployProductionStep(step: PublishWorkflowStep, options: PublishWorkflo
|
|
|
486
486
|
" ${{ steps.version.outputs.mode != 'none' && inputs.deploy_environment == 'production' && inputs.dry_run !=",
|
|
487
487
|
" 'true' }}",
|
|
488
488
|
...deployEnvLines(options),
|
|
489
|
-
' run: smoo github-ci nx-deploy --
|
|
489
|
+
' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
|
|
490
490
|
];
|
|
491
491
|
}
|
|
492
492
|
|
|
@@ -778,16 +778,19 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
|
|
|
778
778
|
'',
|
|
779
779
|
` # Step ${stepNumber++}`,
|
|
780
780
|
' - name: 🍎 Build selected macOS and iOS release outputs',
|
|
781
|
+
' id: platform-outputs',
|
|
781
782
|
' run:',
|
|
782
783
|
` smoo release build-platform-outputs --bump "${githubExpression(
|
|
783
784
|
'inputs.bump',
|
|
784
785
|
)}" --ref "${githubExpression('github.sha')}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}" --output`,
|
|
785
|
-
` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
|
|
786
|
+
` "${githubExpression('runner.temp')}/macos-platform-outputs" --github-output "$GITHUB_OUTPUT"`,
|
|
786
787
|
'',
|
|
787
788
|
` # Step ${stepNumber++}`,
|
|
788
|
-
' - name: 🧪 Unit test macOS and iOS packages',
|
|
789
|
+
' - name: 🧪 Unit test selected macOS and iOS packages',
|
|
790
|
+
` if: steps.platform-outputs.outputs.projects != ''`,
|
|
789
791
|
' run:',
|
|
790
|
-
|
|
792
|
+
' smoo github-ci nx-run-many --targets test --projects',
|
|
793
|
+
` "${githubExpression('steps.platform-outputs.outputs.projects')}"`,
|
|
791
794
|
'',
|
|
792
795
|
` # Step ${stepNumber++}`,
|
|
793
796
|
' - name: 📤 Upload macOS platform outputs',
|
|
@@ -940,7 +943,7 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
|
|
|
940
943
|
" ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_environment == 'production' &&",
|
|
941
944
|
" inputs.dry_run != 'true' }}",
|
|
942
945
|
...deployEnvLines(options),
|
|
943
|
-
' run: smoo github-ci nx-deploy --
|
|
946
|
+
' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
|
|
944
947
|
);
|
|
945
948
|
}
|
|
946
949
|
lines.push(
|
|
@@ -3,10 +3,12 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import {
|
|
6
|
+
applyDevenvPackageDefaults,
|
|
6
7
|
applyRootDevDependencyDefaults,
|
|
7
8
|
applyToolConfigDefaults,
|
|
8
9
|
applyToolingPackageDefaults,
|
|
9
10
|
readToolContext,
|
|
11
|
+
validateDevenvPackages,
|
|
10
12
|
validateToolConfig,
|
|
11
13
|
} from './tool-validation.js';
|
|
12
14
|
|
|
@@ -89,6 +91,88 @@ describe('tool configuration validation', () => {
|
|
|
89
91
|
expect(devenv).toContain('nodejs_latest');
|
|
90
92
|
expect(devenv).toContain('coreutils');
|
|
91
93
|
expect(devenv).toContain('git-format-staged');
|
|
94
|
+
expect(devenv).toContain('go # Builds ttsc source plugins');
|
|
95
|
+
expect(devenv).not.toContain('sccache');
|
|
96
|
+
} finally {
|
|
97
|
+
await rm(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('requires Go for managed TypeScript source-plugin builds without forcing Rust tools', async () => {
|
|
102
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
103
|
+
try {
|
|
104
|
+
const devenvPath = join(root, 'tooling/direnv/devenv.nix');
|
|
105
|
+
await mkdir(dirname(devenvPath), { recursive: true });
|
|
106
|
+
await writeFile(
|
|
107
|
+
devenvPath,
|
|
108
|
+
`{
|
|
109
|
+
pkgs,
|
|
110
|
+
...
|
|
111
|
+
}: {
|
|
112
|
+
packages = with pkgs; [
|
|
113
|
+
nodejs_latest
|
|
114
|
+
bun
|
|
115
|
+
git
|
|
116
|
+
git-format-staged
|
|
117
|
+
jq
|
|
118
|
+
alejandra
|
|
119
|
+
coreutils
|
|
120
|
+
gnutar
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(validateDevenvPackages(root)).toBe(1);
|
|
127
|
+
applyDevenvPackageDefaults(root);
|
|
128
|
+
expect(validateDevenvPackages(root)).toBe(0);
|
|
129
|
+
const devenv = await readFile(devenvPath, 'utf8');
|
|
130
|
+
expect(devenv).toContain('go # Builds ttsc source plugins');
|
|
131
|
+
expect(devenv).not.toContain('sccache');
|
|
132
|
+
} finally {
|
|
133
|
+
await rm(root, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('requires Rust compiler helpers only when the workspace contains Cargo manifests', async () => {
|
|
138
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
139
|
+
try {
|
|
140
|
+
const crateRoot = join(root, 'packages/native');
|
|
141
|
+
await mkdir(crateRoot, { recursive: true });
|
|
142
|
+
await writeFile(join(crateRoot, 'Cargo.toml'), '[package]\nname = "native"\nversion = "0.0.0"\n');
|
|
143
|
+
const devenvPath = join(root, 'tooling/direnv/devenv.nix');
|
|
144
|
+
await mkdir(dirname(devenvPath), { recursive: true });
|
|
145
|
+
await writeFile(
|
|
146
|
+
devenvPath,
|
|
147
|
+
`{
|
|
148
|
+
pkgs,
|
|
149
|
+
lib,
|
|
150
|
+
...
|
|
151
|
+
}: {
|
|
152
|
+
packages = with pkgs; [
|
|
153
|
+
nodejs_latest
|
|
154
|
+
bun
|
|
155
|
+
git
|
|
156
|
+
git-format-staged
|
|
157
|
+
jq
|
|
158
|
+
alejandra
|
|
159
|
+
coreutils
|
|
160
|
+
gnutar
|
|
161
|
+
go
|
|
162
|
+
stdenv.cc.cc.lib
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(validateDevenvPackages(root)).toBe(2);
|
|
169
|
+
applyDevenvPackageDefaults(root);
|
|
170
|
+
expect(validateDevenvPackages(root)).toBe(0);
|
|
171
|
+
const devenv = await readFile(devenvPath, 'utf8');
|
|
172
|
+
expect(devenv).toContain('sccache # Rust compiler wrapper and cache');
|
|
173
|
+
expect(devenv).toContain('++ lib.optionals pkgs.stdenv.isLinux [');
|
|
174
|
+
expect(devenv).toContain('pkgs.stdenv.cc');
|
|
175
|
+
expect(devenv).toContain('stdenv.cc.cc.lib');
|
|
92
176
|
} finally {
|
|
93
177
|
await rm(root, { recursive: true, force: true });
|
|
94
178
|
}
|
|
@@ -141,6 +225,7 @@ describe('tool configuration validation', () => {
|
|
|
141
225
|
alejandra
|
|
142
226
|
coreutils
|
|
143
227
|
gnutar
|
|
228
|
+
go
|
|
144
229
|
];
|
|
145
230
|
}
|
|
146
231
|
`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import typia from 'typia';
|
|
4
4
|
import { cliPackageVersion, isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
|
|
@@ -67,12 +67,24 @@ const rootDevDependencies: RequiredDependency[] = [
|
|
|
67
67
|
|
|
68
68
|
const cliPackageName = '@smoothbricks/cli';
|
|
69
69
|
|
|
70
|
-
const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar'];
|
|
70
|
+
const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar', 'go'];
|
|
71
71
|
// Any explicit nodejs provider is fine — the pinned major is the repo's choice
|
|
72
72
|
// (Lambda parity, org convergence, …). Whether the pins in package.json agree
|
|
73
73
|
// with the runtime is validated against the live PATH (validateRootRuntimeVersions),
|
|
74
74
|
// never against a version template here.
|
|
75
75
|
const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
|
|
76
|
+
const requiredRustDevenvPackages = ['sccache'];
|
|
77
|
+
const linuxCompilerPackage = 'pkgs.stdenv.cc';
|
|
78
|
+
const ignoredNativeManifestDirectories = new Set([
|
|
79
|
+
'.devenv',
|
|
80
|
+
'.direnv',
|
|
81
|
+
'.git',
|
|
82
|
+
'.nx',
|
|
83
|
+
'dist',
|
|
84
|
+
'node_modules',
|
|
85
|
+
'target',
|
|
86
|
+
'vendor',
|
|
87
|
+
]);
|
|
76
88
|
|
|
77
89
|
const obsoleteTtscPatchedDependencyKey = 'ttsc@0.19.3';
|
|
78
90
|
|
|
@@ -201,7 +213,9 @@ export function applyDevenvPackageDefaults(root: string): void {
|
|
|
201
213
|
changed = next !== content || changed;
|
|
202
214
|
content = next;
|
|
203
215
|
}
|
|
204
|
-
|
|
216
|
+
const compilesRust = workspaceCompilesRust(root);
|
|
217
|
+
const requiredPackages = [...requiredDevenvPackages, ...(compilesRust ? requiredRustDevenvPackages : [])];
|
|
218
|
+
for (const name of requiredPackages) {
|
|
205
219
|
if (hasNixPackage(content, name)) {
|
|
206
220
|
continue;
|
|
207
221
|
}
|
|
@@ -209,6 +223,11 @@ export function applyDevenvPackageDefaults(root: string): void {
|
|
|
209
223
|
changed = next !== content || changed;
|
|
210
224
|
content = next;
|
|
211
225
|
}
|
|
226
|
+
if (compilesRust && !hasLinuxCompilerPackage(content)) {
|
|
227
|
+
const next = addLinuxCompilerPackage(content);
|
|
228
|
+
changed = next !== content || changed;
|
|
229
|
+
content = next;
|
|
230
|
+
}
|
|
212
231
|
if (changed) {
|
|
213
232
|
writeFileSync(path, content);
|
|
214
233
|
console.log('updated tooling/direnv/devenv.nix packages');
|
|
@@ -316,12 +335,20 @@ export function validateDevenvPackages(root: string): number {
|
|
|
316
335
|
);
|
|
317
336
|
failures++;
|
|
318
337
|
}
|
|
319
|
-
|
|
338
|
+
const compilesRust = workspaceCompilesRust(root);
|
|
339
|
+
const requiredPackages = [...requiredDevenvPackages, ...(compilesRust ? requiredRustDevenvPackages : [])];
|
|
340
|
+
for (const name of requiredPackages) {
|
|
320
341
|
if (!hasNixPackage(content, name)) {
|
|
321
342
|
console.error(`tooling/direnv/devenv.nix packages must include ${name}`);
|
|
322
343
|
failures++;
|
|
323
344
|
}
|
|
324
345
|
}
|
|
346
|
+
if (compilesRust && !hasLinuxCompilerPackage(content)) {
|
|
347
|
+
console.error(
|
|
348
|
+
'tooling/direnv/devenv.nix packages must include pkgs.stdenv.cc in a Linux-only package block for Rust builds',
|
|
349
|
+
);
|
|
350
|
+
failures++;
|
|
351
|
+
}
|
|
325
352
|
return failures;
|
|
326
353
|
}
|
|
327
354
|
|
|
@@ -330,7 +357,9 @@ function addNixPackage(content: string, name: string, comment: string): string {
|
|
|
330
357
|
if (hasNixPackage(content, name)) {
|
|
331
358
|
return content;
|
|
332
359
|
}
|
|
333
|
-
const
|
|
360
|
+
const directPackagesStart = content.indexOf(' packages = with pkgs; [');
|
|
361
|
+
const groupedPackagesStart = content.indexOf(' (with pkgs; [');
|
|
362
|
+
const packagesStart = directPackagesStart === -1 ? groupedPackagesStart : directPackagesStart;
|
|
334
363
|
if (packagesStart === -1) {
|
|
335
364
|
return content;
|
|
336
365
|
}
|
|
@@ -342,6 +371,60 @@ function hasNixPackage(content: string, name: string): boolean {
|
|
|
342
371
|
return new RegExp(`(^|\\s)${escapeRegex(name)}(\\s|#|$)`, 'm').test(content);
|
|
343
372
|
}
|
|
344
373
|
|
|
374
|
+
function workspaceCompilesRust(root: string): boolean {
|
|
375
|
+
return containsCargoManifest(root);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function containsCargoManifest(directory: string): boolean {
|
|
379
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
380
|
+
if (entry.isFile() && entry.name === 'Cargo.toml') {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
entry.isDirectory() &&
|
|
385
|
+
entry.name.startsWith('.') === false &&
|
|
386
|
+
ignoredNativeManifestDirectories.has(entry.name) === false &&
|
|
387
|
+
containsCargoManifest(join(directory, entry.name))
|
|
388
|
+
) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function hasLinuxCompilerPackage(content: string): boolean {
|
|
396
|
+
const linuxPackages = /\+\+\s+lib\.optionals\s+pkgs\.stdenv\.isLinux\s+\[([\s\S]*?)\]/g;
|
|
397
|
+
return [...content.matchAll(linuxPackages)].some((match) => hasNixPackage(match[1] ?? '', linuxCompilerPackage));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function addLinuxCompilerPackage(content: string): string {
|
|
401
|
+
if (hasLinuxCompilerPackage(content)) {
|
|
402
|
+
return content;
|
|
403
|
+
}
|
|
404
|
+
const directStart = content.indexOf(' packages = with pkgs; [');
|
|
405
|
+
if (directStart !== -1) {
|
|
406
|
+
const listEnd = content.indexOf('\n ];', directStart);
|
|
407
|
+
if (listEnd === -1) {
|
|
408
|
+
return content;
|
|
409
|
+
}
|
|
410
|
+
const listBody = content.slice(directStart + ' packages = with pkgs; ['.length, listEnd);
|
|
411
|
+
const replacement = ` packages =\n (with pkgs; [${listBody}\n ])\n ++ lib.optionals pkgs.stdenv.isLinux [\n ${linuxCompilerPackage}\n ];`;
|
|
412
|
+
return `${content.slice(0, directStart)}${replacement}${content.slice(listEnd + '\n ];'.length)}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const packagesStart = content.indexOf(' packages =');
|
|
416
|
+
const nextOption = content.indexOf('\n\n ', packagesStart);
|
|
417
|
+
if (packagesStart === -1 || nextOption === -1) {
|
|
418
|
+
return content;
|
|
419
|
+
}
|
|
420
|
+
const terminator = content.lastIndexOf(';', nextOption);
|
|
421
|
+
if (terminator < packagesStart) {
|
|
422
|
+
return content;
|
|
423
|
+
}
|
|
424
|
+
const linuxPackages = `\n ++ lib.optionals pkgs.stdenv.isLinux [\n ${linuxCompilerPackage}\n ]`;
|
|
425
|
+
return `${content.slice(0, terminator)}${linuxPackages}${content.slice(terminator)}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
345
428
|
function nixPackageComment(name: string): string {
|
|
346
429
|
if (name === 'coreutils') {
|
|
347
430
|
return '# Provides fmt for commit message wrapping';
|
|
@@ -352,6 +435,12 @@ function nixPackageComment(name: string): string {
|
|
|
352
435
|
if (name === 'git') {
|
|
353
436
|
return '# Git hooks and repository inspection';
|
|
354
437
|
}
|
|
438
|
+
if (name === 'go') {
|
|
439
|
+
return '# Builds ttsc source plugins';
|
|
440
|
+
}
|
|
441
|
+
if (name === 'sccache') {
|
|
442
|
+
return '# Rust compiler wrapper and cache';
|
|
443
|
+
}
|
|
355
444
|
return '';
|
|
356
445
|
}
|
|
357
446
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { type ChromiumSetupDependencies, ensureChromium } from './index.js';
|
|
3
|
+
|
|
4
|
+
interface RunCall {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly args: string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setupDependencies(
|
|
12
|
+
env: ChromiumSetupDependencies['env'],
|
|
13
|
+
existingPaths: readonly string[],
|
|
14
|
+
calls: RunCall[],
|
|
15
|
+
): ChromiumSetupDependencies {
|
|
16
|
+
const existing = new Set(existingPaths);
|
|
17
|
+
return {
|
|
18
|
+
env,
|
|
19
|
+
exists: (path) => existing.has(path),
|
|
20
|
+
run: async (command, args, cwd, runEnv) => {
|
|
21
|
+
calls.push({ command, args, cwd, ...(runEnv ? { env: runEnv } : {}) });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('ensureChromium', () => {
|
|
27
|
+
it('uses preinstalled Chrome without invoking Playwright install on an ephemeral GitHub runner', async () => {
|
|
28
|
+
const calls: RunCall[] = [];
|
|
29
|
+
const executablePath = '/fixture/google-chrome';
|
|
30
|
+
|
|
31
|
+
const result = await ensureChromium(
|
|
32
|
+
'/workspace/package',
|
|
33
|
+
setupDependencies(
|
|
34
|
+
{ GITHUB_ACTIONS: 'true', PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: executablePath },
|
|
35
|
+
[executablePath],
|
|
36
|
+
calls,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual({ mode: 'system', executablePath });
|
|
41
|
+
expect(calls).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('installs Chromium into the persistent host-runner cache', async () => {
|
|
45
|
+
const calls: RunCall[] = [];
|
|
46
|
+
|
|
47
|
+
const result = await ensureChromium(
|
|
48
|
+
'/workspace/package',
|
|
49
|
+
setupDependencies({ GITHUB_ACTIONS: 'true' }, ['/var/cache/ci'], calls),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ mode: 'persistent-cache', browserCachePath: '/var/cache/ci/ms-playwright' });
|
|
53
|
+
expect(calls).toEqual([
|
|
54
|
+
{
|
|
55
|
+
command: 'playwright',
|
|
56
|
+
args: ['install', 'chromium', '--only-shell'],
|
|
57
|
+
cwd: '/workspace/package',
|
|
58
|
+
env: { PLAYWRIGHT_BROWSERS_PATH: '/var/cache/ci/ms-playwright' },
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('fails rather than downloading when an ephemeral GitHub runner has no system Chrome', async () => {
|
|
64
|
+
const calls: RunCall[] = [];
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
ensureChromium('/workspace/package', setupDependencies({ GITHUB_ACTIONS: 'true' }, [], calls)),
|
|
68
|
+
).rejects.toThrow('Refusing to download');
|
|
69
|
+
expect(calls).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('honors an explicit developer cache outside GitHub Actions', async () => {
|
|
73
|
+
const calls: RunCall[] = [];
|
|
74
|
+
|
|
75
|
+
const result = await ensureChromium(
|
|
76
|
+
'/workspace/package',
|
|
77
|
+
setupDependencies({ PLAYWRIGHT_BROWSERS_PATH: '/tmp/browser-cache' }, [], calls),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result).toEqual({ mode: 'developer-cache', browserCachePath: '/tmp/browser-cache' });
|
|
81
|
+
expect(calls).toEqual([
|
|
82
|
+
{
|
|
83
|
+
command: 'playwright',
|
|
84
|
+
args: ['install', 'chromium', '--only-shell'],
|
|
85
|
+
cwd: '/workspace/package',
|
|
86
|
+
env: { PLAYWRIGHT_BROWSERS_PATH: '/tmp/browser-cache' },
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
});
|