@smoothbricks/cli 0.10.5 → 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -2
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +5 -3
- package/dist/lib/json.d.ts +2 -0
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +2 -2
- package/dist/monorepo/ci-workflow.d.ts +2 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +10 -4
- package/dist/monorepo/github-runs-on.d.ts +14 -0
- package/dist/monorepo/github-runs-on.d.ts.map +1 -0
- package/dist/monorepo/github-runs-on.js +43 -0
- package/dist/monorepo/index.d.ts +2 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +7 -7
- package/dist/monorepo/managed-files.d.ts +30 -3
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +78 -63
- package/dist/monorepo/packs/index.js +1 -1
- package/dist/monorepo/publish-workflow.d.ts +3 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +17 -14
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +24 -7
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +13 -11
- package/managed/templates/github/actions/save-nix-devenv/action.yml +34 -3
- package/managed/templates/github/actions/setup-devenv/action.yml +44 -15
- package/managed/templates/github/workflows/managed-files.yml +64 -12
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/github-ci/index.test.ts +3 -3
- package/src/github-ci/index.ts +6 -3
- package/src/lib/json.ts +2 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
- package/src/monorepo/__tests__/publish-workflow.test.ts +28 -1
- package/src/monorepo/ci-workflow.ts +14 -4
- package/src/monorepo/github-runs-on.ts +45 -0
- package/src/monorepo/index.ts +7 -7
- package/src/monorepo/managed-files.test.ts +49 -0
- package/src/monorepo/managed-files.ts +100 -64
- package/src/monorepo/packs/index.ts +1 -1
- package/src/monorepo/publish-workflow.ts +23 -15
package/src/lib/json.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface PackagePublishConfig {
|
|
|
23
23
|
|
|
24
24
|
export interface PackageSmooGithub {
|
|
25
25
|
pushBranches?: string[];
|
|
26
|
+
/** GitHub Actions runs-on for managed CI (string or label list). Default: ubuntu-latest. */
|
|
27
|
+
runsOn?: string | string[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface PackageSmooConfig {
|
|
@@ -5,9 +5,15 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { CiWorkflowStepKind, defineCiWorkflow, renderCiWorkflowYaml } from '../ci-workflow.js';
|
|
7
7
|
|
|
8
|
+
const nixosRunsOn = ['nixos-latest-x64', 'self-hosted'] as const;
|
|
9
|
+
|
|
8
10
|
describe('CI workflow definition', () => {
|
|
9
11
|
it('renders the checked-in local CI workflow copy', async () => {
|
|
10
|
-
const rendered = renderCiWorkflowYaml({
|
|
12
|
+
const rendered = renderCiWorkflowYaml({
|
|
13
|
+
deploy: false,
|
|
14
|
+
pushBranches: ['main'],
|
|
15
|
+
runsOn: [...nixosRunsOn],
|
|
16
|
+
});
|
|
11
17
|
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
12
18
|
|
|
13
19
|
await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/ci.yml'), 'utf8')).resolves.toBe(rendered);
|
|
@@ -25,6 +31,13 @@ describe('CI workflow definition', () => {
|
|
|
25
31
|
'smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step 11',
|
|
26
32
|
);
|
|
27
33
|
expect(rendered).toContain("# Step 12\n # Nx's database cache needs artifact files");
|
|
34
|
+
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
35
|
+
expect(rendered).toContain('id: setup');
|
|
36
|
+
expect(rendered).not.toContain('Setup Nix/devenv (fork)');
|
|
37
|
+
expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
|
|
38
|
+
expect(rendered).not.toContain('github-actions-bootstrap.sh');
|
|
39
|
+
expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
|
|
40
|
+
expect(rendered).toContain('runs-on: ubuntu-latest');
|
|
28
41
|
});
|
|
29
42
|
|
|
30
43
|
it('adds Cloudflare credentials for Wrangler-backed deploys', () => {
|
|
@@ -44,4 +57,27 @@ describe('CI workflow definition', () => {
|
|
|
44
57
|
expect(restoreKey).toBe('${{ runner.os }}-${{ runner.arch }}-nx-db-v1-${{ github.sha }}');
|
|
45
58
|
expect(saveKey).toBe(restoreKey);
|
|
46
59
|
});
|
|
60
|
+
|
|
61
|
+
it('nixos config: fork PRs on ubuntu; one setup-devenv (composite owns host path)', () => {
|
|
62
|
+
const rendered = renderCiWorkflowYaml({
|
|
63
|
+
deploy: false,
|
|
64
|
+
pushBranches: ['main'],
|
|
65
|
+
runsOn: [...nixosRunsOn],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(rendered).toContain(
|
|
69
|
+
"runs-on:\n ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&\n fromJSON('[\"nixos-latest-x64\",\"self-hosted\"]') || 'ubuntu-latest' }}",
|
|
70
|
+
);
|
|
71
|
+
expect(rendered).toContain('uses: ./.github/actions/setup-devenv');
|
|
72
|
+
expect(rendered).toContain('id: setup');
|
|
73
|
+
expect(rendered).not.toContain('Setup Nix/devenv (fork)');
|
|
74
|
+
expect(rendered).not.toContain('Setup Nix/devenv (nixos)');
|
|
75
|
+
expect(rendered).not.toContain('github-actions-bootstrap.sh');
|
|
76
|
+
expect(rendered).not.toContain('determinate-nix-action');
|
|
77
|
+
expect(rendered).toContain('if: always()');
|
|
78
|
+
expect(rendered).toContain('uses: ./.github/actions/save-nix-devenv');
|
|
79
|
+
// workflow yaml does not embed composite internals
|
|
80
|
+
expect(rendered).not.toContain('cache-node-modules');
|
|
81
|
+
expect(rendered).not.toContain('cache-ttsc-plugins');
|
|
82
|
+
});
|
|
47
83
|
});
|
|
@@ -19,11 +19,14 @@ import {
|
|
|
19
19
|
runPublishWorkflow,
|
|
20
20
|
} from '../publish-workflow.js';
|
|
21
21
|
|
|
22
|
+
const nixosRunsOn = ['nixos-latest-x64', 'self-hosted'] as const;
|
|
23
|
+
|
|
22
24
|
describe('publish workflow definition', () => {
|
|
23
25
|
it('renders the checked-in local publish workflow copy', async () => {
|
|
24
26
|
const rendered = renderPublishWorkflowYaml({
|
|
25
27
|
repoName: '@smoothbricks/codebase',
|
|
26
28
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
29
|
+
runsOn: [...nixosRunsOn],
|
|
27
30
|
});
|
|
28
31
|
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
29
32
|
await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/publish.yml'), 'utf8')).resolves.toBe(
|
|
@@ -35,6 +38,7 @@ describe('publish workflow definition', () => {
|
|
|
35
38
|
const rendered = renderPublishWorkflowYaml({
|
|
36
39
|
repoName: '@smoothbricks/codebase',
|
|
37
40
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
41
|
+
runsOn: [...nixosRunsOn],
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
await expect(
|
|
@@ -125,6 +129,7 @@ describe('publish workflow definition', () => {
|
|
|
125
129
|
const native = renderPublishWorkflowYaml({
|
|
126
130
|
repoName: '@smoothbricks/codebase',
|
|
127
131
|
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
132
|
+
runsOn: [...nixosRunsOn],
|
|
128
133
|
});
|
|
129
134
|
const linuxCandidate = native.slice(
|
|
130
135
|
native.indexOf(' linux-release-candidate:'),
|
|
@@ -137,8 +142,9 @@ describe('publish workflow definition', () => {
|
|
|
137
142
|
expect(linuxOnly).toContain(
|
|
138
143
|
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
139
144
|
);
|
|
140
|
-
expect(native).toContain(' linux-release-candidate
|
|
145
|
+
expect(native).toContain(' linux-release-candidate:');
|
|
141
146
|
expect(native).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
147
|
+
expect(native).toContain(' publish-on-linux:');
|
|
142
148
|
expect(native).toContain(' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]');
|
|
143
149
|
expect(linuxCandidate).not.toContain('needs:');
|
|
144
150
|
expect(macosPlatform).not.toContain('needs:');
|
|
@@ -612,3 +618,24 @@ function packageNameFromTag(tag: string): string {
|
|
|
612
618
|
}
|
|
613
619
|
return tag.slice(0, versionSeparator);
|
|
614
620
|
}
|
|
621
|
+
|
|
622
|
+
it('linux publish jobs use smoo.github.runsOn; macOS stays macos-latest', () => {
|
|
623
|
+
const rendered = renderPublishWorkflowYaml({
|
|
624
|
+
repoName: '@smoothbricks/codebase',
|
|
625
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
626
|
+
runsOn: [...nixosRunsOn],
|
|
627
|
+
});
|
|
628
|
+
expect(rendered).toContain(
|
|
629
|
+
`runs-on:
|
|
630
|
+
\${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
|
631
|
+
fromJSON('["nixos-latest-x64","self-hosted"]') || 'ubuntu-latest' }}`,
|
|
632
|
+
);
|
|
633
|
+
expect(rendered).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
634
|
+
// Only the pre-publish linux job runs on self-hosted
|
|
635
|
+
expect(rendered.split('fromJSON(\'["nixos-latest-x64","self-hosted"]\')').length - 1).toBe(1);
|
|
636
|
+
expect(rendered).not.toContain(' linux-release-candidate:\n runs-on: ubuntu-latest');
|
|
637
|
+
// NPM publishing happens from GitHub runner
|
|
638
|
+
expect(rendered).toContain(
|
|
639
|
+
' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]\n runs-on: ubuntu-latest',
|
|
640
|
+
);
|
|
641
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
2
|
|
|
3
|
+
import { renderRunsOnLine } from './github-runs-on.js';
|
|
4
|
+
|
|
3
5
|
export enum CiWorkflowStepKind {
|
|
4
6
|
Checkout = 'checkout',
|
|
5
7
|
SetupDevenv = 'setup-devenv',
|
|
@@ -26,6 +28,8 @@ export interface CiWorkflowDefinitionOptions {
|
|
|
26
28
|
deploy: boolean;
|
|
27
29
|
deployProvider?: 'cloudflare';
|
|
28
30
|
pushBranches: string[];
|
|
31
|
+
/** Default ubuntu-latest when omitted. */
|
|
32
|
+
runsOn?: string | string[];
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
type CiWorkflowStepInput = Omit<CiWorkflowStep, 'number'>;
|
|
@@ -80,7 +84,7 @@ defaults:
|
|
|
80
84
|
jobs:
|
|
81
85
|
main:
|
|
82
86
|
name: Validate
|
|
83
|
-
|
|
87
|
+
${renderRunsOnLine(options.runsOn)}
|
|
84
88
|
timeout-minutes: 45
|
|
85
89
|
env:
|
|
86
90
|
NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
|
|
@@ -150,7 +154,10 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
150
154
|
' fetch-depth: 0',
|
|
151
155
|
];
|
|
152
156
|
case CiWorkflowStepKind.SetupDevenv:
|
|
157
|
+
// One step everywhere. setup-devenv detects host-nix (GARM) vs ephemeral
|
|
158
|
+
// and skips GH install/cache steps internally — same for ci/publish/managed.
|
|
153
159
|
return [` - name: ${step.name}`, ' id: setup', ' uses: ./.github/actions/setup-devenv'];
|
|
160
|
+
|
|
154
161
|
case CiWorkflowStepKind.SetNxShas:
|
|
155
162
|
return [
|
|
156
163
|
` - name: ${step.name}`,
|
|
@@ -184,7 +191,7 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
184
191
|
// recursion guard, so the default token suffices.
|
|
185
192
|
return [
|
|
186
193
|
` - name: ${step.name}`,
|
|
187
|
-
" if:
|
|
194
|
+
" if: steps.managed-drift.outputs.drifted != '' && steps.managed-drift.outputs.drifted != '0'",
|
|
188
195
|
' run: gh workflow run managed-files.yml --ref "$GITHUB_REF_NAME"',
|
|
189
196
|
];
|
|
190
197
|
case CiWorkflowStepKind.Deploy:
|
|
@@ -212,7 +219,8 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
212
219
|
case CiWorkflowStepKind.UploadTraceDbs:
|
|
213
220
|
return [
|
|
214
221
|
` - name: ${step.name}`,
|
|
215
|
-
'
|
|
222
|
+
' # success() is the default; always() keeps traces after a red build/test.',
|
|
223
|
+
' if: always()',
|
|
216
224
|
' uses: actions/upload-artifact@v7.0.1',
|
|
217
225
|
' with:',
|
|
218
226
|
' name: trace-results-${{ github.run_id }}',
|
|
@@ -224,7 +232,9 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
224
232
|
case CiWorkflowStepKind.SaveNixDevenv:
|
|
225
233
|
return [
|
|
226
234
|
` - name: ${step.name}`,
|
|
227
|
-
'
|
|
235
|
+
' # always() still saves after a red job. Nix NAR is ephemeral-only;',
|
|
236
|
+
' # devenv eval-cache also saves on host-nix when setup missed.',
|
|
237
|
+
' if: always()',
|
|
228
238
|
' uses: ./.github/actions/save-nix-devenv',
|
|
229
239
|
' with:',
|
|
230
240
|
' nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Shared `runs-on:` emission for generated GitHub workflow YAML. */
|
|
2
|
+
|
|
3
|
+
export type WorkflowRunsOn = string | string[] | undefined;
|
|
4
|
+
|
|
5
|
+
export function isNixosRunner(runsOn: WorkflowRunsOn): boolean {
|
|
6
|
+
const labels = runsOn === undefined ? [] : typeof runsOn === 'string' ? [runsOn] : runsOn;
|
|
7
|
+
return labels.some((label) => label === 'nixos' || label.startsWith('nixos-'));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trusted jobs use configured nixos self-hosted labels; fork PRs stay on
|
|
12
|
+
* ubuntu-latest (no access to the private runner fleet).
|
|
13
|
+
*/
|
|
14
|
+
export function githubUsesNixosRunnerExpr(): string {
|
|
15
|
+
return "(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Full ` runs-on: …` line(s), indented for a job under `jobs.<id>:`.
|
|
20
|
+
* Nixos labels emit the multiline prettier-stable fork-gate expression.
|
|
21
|
+
*/
|
|
22
|
+
export function renderRunsOnLine(runsOn: WorkflowRunsOn): string {
|
|
23
|
+
if (!isNixosRunner(runsOn)) {
|
|
24
|
+
let value: string;
|
|
25
|
+
if (runsOn === undefined) {
|
|
26
|
+
value = 'ubuntu-latest';
|
|
27
|
+
} else if (typeof runsOn === 'string') {
|
|
28
|
+
value = runsOn.length > 0 ? runsOn : 'ubuntu-latest';
|
|
29
|
+
} else if (runsOn.length === 0) {
|
|
30
|
+
value = 'ubuntu-latest';
|
|
31
|
+
} else if (runsOn.length === 1) {
|
|
32
|
+
value = runsOn[0] ?? 'ubuntu-latest';
|
|
33
|
+
} else {
|
|
34
|
+
value = `[${runsOn.map((label) => `'${label}'`).join(', ')}]`;
|
|
35
|
+
}
|
|
36
|
+
return ` runs-on: ${value}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const labels = typeof runsOn === 'string' ? [runsOn] : runsOn;
|
|
40
|
+
// Multiline matches prettier so checked-in workflows stay byte-identical.
|
|
41
|
+
const nixosJson = JSON.stringify(labels);
|
|
42
|
+
return ` runs-on:
|
|
43
|
+
\${{ ${githubUsesNixosRunnerExpr()} &&
|
|
44
|
+
fromJSON('${nixosJson}') || 'ubuntu-latest' }}`;
|
|
45
|
+
}
|
package/src/monorepo/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function initMonorepo(root: string, options: InitOptions): Promise<
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
printResults(applyManagedFiles(root, 'update'));
|
|
54
|
+
printResults(await applyManagedFiles(root, 'update'));
|
|
55
55
|
await runInitPacks({ root, syncRuntime: process.env.DEVENV_ROOT !== undefined || options.syncRuntime === true });
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -76,7 +76,7 @@ export async function validateMonorepo(root: string, options: ValidateOptions =
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export async function updateManagedFiles(root: string): Promise<void> {
|
|
79
|
-
printResults(applyManagedFiles(root, 'update'));
|
|
79
|
+
printResults(await applyManagedFiles(root, 'update'));
|
|
80
80
|
// Tool dependency policy (typescript API 6, @typescript/native for ttsc, nx, …)
|
|
81
81
|
// lives next to managed templates — update must install them, not only rewrite files.
|
|
82
82
|
await applyToolConfigDefaults(root);
|
|
@@ -90,20 +90,20 @@ export async function updateManagedFiles(root: string): Promise<void> {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function checkManagedFiles(root: string, options: { warn?: boolean } = {}): void {
|
|
93
|
+
export async function checkManagedFiles(root: string, options: { warn?: boolean } = {}): Promise<void> {
|
|
94
94
|
if (options.warn === true) {
|
|
95
|
-
warnOnManagedFileDrift(root);
|
|
95
|
+
await warnOnManagedFileDrift(root);
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
-
const results = applyManagedFiles(root, 'check');
|
|
98
|
+
const results = await applyManagedFiles(root, 'check');
|
|
99
99
|
printResults(results);
|
|
100
100
|
if (results.some((result) => result.action === 'drifted')) {
|
|
101
101
|
throw new Error('Managed monorepo files are out of date. Run: smoo monorepo update');
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export function diffManagedFiles(root: string): void {
|
|
106
|
-
printResults(applyManagedFiles(root, 'diff'));
|
|
105
|
+
export async function diffManagedFiles(root: string): Promise<void> {
|
|
106
|
+
printResults(await applyManagedFiles(root, 'diff'));
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export function validateCommitMessageFile(
|
|
@@ -5,13 +5,16 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { LINUX_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
6
6
|
import fc from 'fast-check';
|
|
7
7
|
import {
|
|
8
|
+
deployTargetInfoFromProjects,
|
|
8
9
|
extractInlineLocalBlocksForTest,
|
|
9
10
|
INLINE_LOCAL_BEGIN,
|
|
10
11
|
INLINE_LOCAL_END,
|
|
11
12
|
LOCAL_SECTION_MARKER,
|
|
13
|
+
type NxGraphProjectNode,
|
|
12
14
|
platformTargetGlobsForTest,
|
|
13
15
|
reinsertInlineLocalBlocksForTest,
|
|
14
16
|
splitLocalSectionForTest,
|
|
17
|
+
targetNamesFromProjects,
|
|
15
18
|
} from './managed-files.js';
|
|
16
19
|
|
|
17
20
|
const MANAGED = '# managed content\npath merge=driver\n';
|
|
@@ -172,6 +175,52 @@ describe('managed publish platform discovery', () => {
|
|
|
172
175
|
});
|
|
173
176
|
});
|
|
174
177
|
|
|
178
|
+
describe('nx graph project helpers', () => {
|
|
179
|
+
const sampleNodes: Record<string, NxGraphProjectNode> = {
|
|
180
|
+
lib: {
|
|
181
|
+
data: {
|
|
182
|
+
targets: {
|
|
183
|
+
build: {},
|
|
184
|
+
'bundle-linux': {},
|
|
185
|
+
lint: {},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
app: {
|
|
190
|
+
data: {
|
|
191
|
+
targets: {
|
|
192
|
+
deploy: {
|
|
193
|
+
options: { command: 'echo no' },
|
|
194
|
+
configurations: {
|
|
195
|
+
staging: { command: 'wrangler deploy --env staging' },
|
|
196
|
+
production: { options: { command: 'wrangler deploy --env production' } },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
'package-macos': {},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
it('collects target names from graph nodes without project names', () => {
|
|
206
|
+
expect(targetNamesFromProjects(sampleNodes).sort()).toEqual(
|
|
207
|
+
['build', 'bundle-linux', 'deploy', 'lint', 'package-macos'].sort(),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('detects deploy configurations and cloudflare provider from graph nodes', () => {
|
|
212
|
+
expect(deployTargetInfoFromProjects(sampleNodes, 'staging')).toEqual({
|
|
213
|
+
exists: true,
|
|
214
|
+
provider: 'cloudflare',
|
|
215
|
+
});
|
|
216
|
+
expect(deployTargetInfoFromProjects(sampleNodes, 'production')).toEqual({
|
|
217
|
+
exists: true,
|
|
218
|
+
provider: 'cloudflare',
|
|
219
|
+
});
|
|
220
|
+
expect(deployTargetInfoFromProjects(sampleNodes, 'preview')).toEqual({ exists: false });
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
175
224
|
describe('managed cache actions', () => {
|
|
176
225
|
it('renders the checked-in action copies from their managed templates', async () => {
|
|
177
226
|
for (const action of CACHE_ACTIONS) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { dirname, join, resolve } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
6
|
-
|
|
5
|
+
// Nx package exports are CLI-oriented; this is the module the CLI uses for graph + daemon IPC.
|
|
6
|
+
import { createProjectGraphAsync } from 'nx/src/project-graph/project-graph.js';
|
|
7
|
+
import type { PackageJson } from '../lib/json.js';
|
|
7
8
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
8
9
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
9
10
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
@@ -137,6 +138,7 @@ interface ManagedFileContext {
|
|
|
137
138
|
stagingDeployProvider?: 'cloudflare';
|
|
138
139
|
productionDeployProvider?: 'cloudflare';
|
|
139
140
|
ciPushBranches: string[];
|
|
141
|
+
ciRunsOn: string | string[];
|
|
140
142
|
nodeModulesCacheKey: string;
|
|
141
143
|
repoName: string;
|
|
142
144
|
platformTargetGlobs: string[];
|
|
@@ -254,8 +256,8 @@ const managedFiles: ManagedFile[] = [
|
|
|
254
256
|
|
|
255
257
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
256
258
|
|
|
257
|
-
export function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[] {
|
|
258
|
-
const context = getManagedFileContext(root);
|
|
259
|
+
export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
|
|
260
|
+
const context = await getManagedFileContext(root);
|
|
259
261
|
return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
|
|
260
262
|
}
|
|
261
263
|
|
|
@@ -306,6 +308,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
306
308
|
deploy: context.hasStagingDeployTargets,
|
|
307
309
|
deployProvider: context.stagingDeployProvider,
|
|
308
310
|
pushBranches: context.ciPushBranches,
|
|
311
|
+
runsOn: context.ciRunsOn,
|
|
309
312
|
});
|
|
310
313
|
}
|
|
311
314
|
if (file.source === 'publish-workflow') {
|
|
@@ -314,6 +317,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
314
317
|
deployProvider: context.productionDeployProvider,
|
|
315
318
|
repoName: context.repoName,
|
|
316
319
|
platformTargetGlobs: context.platformTargetGlobs,
|
|
320
|
+
runsOn: context.ciRunsOn,
|
|
317
321
|
});
|
|
318
322
|
}
|
|
319
323
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
@@ -327,13 +331,16 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
327
331
|
return renderTemplate(context, content);
|
|
328
332
|
}
|
|
329
333
|
|
|
330
|
-
function getManagedFileContext(root: string): ManagedFileContext {
|
|
334
|
+
async function getManagedFileContext(root: string): Promise<ManagedFileContext> {
|
|
331
335
|
const packageJson = readPackageJson(join(root, 'package.json'));
|
|
332
336
|
const repoName = packageJson?.name ?? 'monorepo';
|
|
333
337
|
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
const
|
|
338
|
+
const ciRunsOn = getCiRunsOn(packageJson?.json);
|
|
339
|
+
// In-process Nx API → daemon socket (no second Node/`nx` CLI process).
|
|
340
|
+
const nxProjects = await loadNxGraphProjects(root);
|
|
341
|
+
const stagingDeploy = deployTargetInfoFromProjects(nxProjects, 'staging');
|
|
342
|
+
const productionDeploy = deployTargetInfoFromProjects(nxProjects, 'production');
|
|
343
|
+
const platformTargetGlobs = platformTargetGlobsForTest(targetNamesFromProjects(nxProjects));
|
|
337
344
|
const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
|
|
338
345
|
? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
|
|
339
346
|
: `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
|
|
@@ -344,6 +351,7 @@ function getManagedFileContext(root: string): ManagedFileContext {
|
|
|
344
351
|
stagingDeployProvider: stagingDeploy.provider,
|
|
345
352
|
productionDeployProvider: productionDeploy.provider,
|
|
346
353
|
ciPushBranches,
|
|
354
|
+
ciRunsOn,
|
|
347
355
|
nodeModulesCacheKey,
|
|
348
356
|
repoName,
|
|
349
357
|
platformTargetGlobs,
|
|
@@ -358,81 +366,109 @@ export function platformTargetGlobsForTest(targetNames: Iterable<string>): strin
|
|
|
358
366
|
});
|
|
359
367
|
}
|
|
360
368
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 ?? {} } };
|
|
384
399
|
}
|
|
400
|
+
return out;
|
|
401
|
+
} finally {
|
|
402
|
+
process.chdir(prevCwd);
|
|
385
403
|
}
|
|
386
|
-
return [...targetNames];
|
|
387
404
|
}
|
|
388
405
|
|
|
389
|
-
function
|
|
390
|
-
return
|
|
391
|
-
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
392
|
-
.replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
|
|
393
|
-
.replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
|
|
406
|
+
function targetsOfProject(node: NxGraphProjectNode): Record<string, NxGraphTarget> {
|
|
407
|
+
return node.data?.targets ?? node.targets ?? {};
|
|
394
408
|
}
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (!projects) {
|
|
404
|
-
return { exists: false };
|
|
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
|
+
}
|
|
405
417
|
}
|
|
418
|
+
return [...targetNames];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Test seam: deploy configuration presence/provider from graph nodes. */
|
|
422
|
+
export function deployTargetInfoFromProjects(
|
|
423
|
+
nodes: Record<string, NxGraphProjectNode>,
|
|
424
|
+
configuration: string,
|
|
425
|
+
): DeployTargetInfo {
|
|
406
426
|
let exists = false;
|
|
407
427
|
let provider: DeployTargetInfo['provider'];
|
|
408
|
-
for (const
|
|
409
|
-
const
|
|
410
|
-
if (!
|
|
428
|
+
for (const node of Object.values(nodes)) {
|
|
429
|
+
const info = deployTargetInfoFromTargets(targetsOfProject(node), configuration);
|
|
430
|
+
if (!info.exists) {
|
|
411
431
|
continue;
|
|
412
432
|
}
|
|
413
433
|
exists = true;
|
|
414
|
-
provider ??=
|
|
434
|
+
provider ??= info.provider;
|
|
415
435
|
}
|
|
416
436
|
return { exists, provider };
|
|
417
437
|
}
|
|
418
438
|
|
|
419
|
-
function
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const projectJson = parseNxProjectJsonText(output);
|
|
426
|
-
const deploy = projectJson?.targets?.deploy;
|
|
427
|
-
const config = deploy?.configurations?.[configuration];
|
|
439
|
+
function deployTargetInfoFromTargets(targets: Record<string, NxGraphTarget>, configuration: string): DeployTargetInfo {
|
|
440
|
+
const deploy = targets.deploy;
|
|
441
|
+
if (!deploy) {
|
|
442
|
+
return { exists: false };
|
|
443
|
+
}
|
|
444
|
+
const config = deploy.configurations?.[configuration];
|
|
428
445
|
if (!config) {
|
|
429
446
|
return { exists: false };
|
|
430
447
|
}
|
|
431
|
-
const commandValue = config.command ?? config.options?.command ?? deploy
|
|
448
|
+
const commandValue = config.command ?? config.options?.command ?? deploy.options?.command ?? deploy.command;
|
|
432
449
|
const command = typeof commandValue === 'string' ? commandValue : '';
|
|
433
450
|
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
434
451
|
}
|
|
435
452
|
|
|
453
|
+
function renderTemplate(context: ManagedFileContext, template: string): string {
|
|
454
|
+
return template
|
|
455
|
+
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
456
|
+
.replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
|
|
457
|
+
.replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getCiRunsOn(packageJson: PackageJson | null | undefined): string | string[] {
|
|
461
|
+
const configured = packageJson?.smoo?.github?.runsOn;
|
|
462
|
+
if (configured === undefined) {
|
|
463
|
+
return 'ubuntu-latest';
|
|
464
|
+
}
|
|
465
|
+
if (typeof configured === 'string') {
|
|
466
|
+
return configured.length > 0 ? configured : 'ubuntu-latest';
|
|
467
|
+
}
|
|
468
|
+
const labels = configured.filter((label) => label.length > 0);
|
|
469
|
+
return labels.length > 0 ? labels : 'ubuntu-latest';
|
|
470
|
+
}
|
|
471
|
+
|
|
436
472
|
function getCiPushBranches(packageJson: PackageJson | null | undefined): string[] {
|
|
437
473
|
const configured = readCiPushBranches(packageJson);
|
|
438
474
|
return configured.length > 0 ? configured : ['main'];
|
|
@@ -458,8 +494,8 @@ export function printResults(results: FileResult[]): void {
|
|
|
458
494
|
}
|
|
459
495
|
}
|
|
460
496
|
|
|
461
|
-
export function validateManagedFiles(root: string): number {
|
|
462
|
-
const results = applyManagedFiles(root, 'check');
|
|
497
|
+
export async function validateManagedFiles(root: string): Promise<number> {
|
|
498
|
+
const results = await applyManagedFiles(root, 'check');
|
|
463
499
|
printResults(results);
|
|
464
500
|
const failures = results.filter((result) => result.action === 'drifted').length;
|
|
465
501
|
if (failures > 0) {
|
|
@@ -478,8 +514,8 @@ export function validateManagedFiles(root: string): number {
|
|
|
478
514
|
* Under GitHub Actions the drifted-file count is published as the step output
|
|
479
515
|
* `drifted`, so downstream steps gate declaratively instead of parsing logs.
|
|
480
516
|
*/
|
|
481
|
-
export function warnOnManagedFileDrift(root: string): void {
|
|
482
|
-
const results = applyManagedFiles(root, 'check');
|
|
517
|
+
export async function warnOnManagedFileDrift(root: string): Promise<void> {
|
|
518
|
+
const results = await applyManagedFiles(root, 'check');
|
|
483
519
|
printResults(results);
|
|
484
520
|
const drifted = results.filter((result) => result.action === 'drifted');
|
|
485
521
|
if (process.env.GITHUB_OUTPUT) {
|
|
@@ -92,7 +92,7 @@ const packs: MonorepoPack[] = [
|
|
|
92
92
|
// Managed-file drift is derived state (CLI template x pinned version) with
|
|
93
93
|
// its own remediation flow; it warns instead of failing so validation only
|
|
94
94
|
// blocks on actual package issues. See warnOnManagedFileDrift.
|
|
95
|
-
warnOnManagedFileDrift(ctx.root);
|
|
95
|
+
await warnOnManagedFileDrift(ctx.root);
|
|
96
96
|
return (
|
|
97
97
|
runtimeFailures +
|
|
98
98
|
validateRootPackagePolicy(ctx.root) +
|