@smoothbricks/cli 0.10.5 → 0.10.7
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 +3 -2
- package/dist/github-ci/index.d.ts +1 -0
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +28 -5
- package/dist/lib/json.d.ts +3 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +5 -20
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +1 -0
- 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 +14 -3
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +54 -74
- package/dist/monorepo/package-policy.js +2 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +3 -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/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 +20 -23
- 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/managed/raw/tooling/devenv +71 -0
- 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 +4 -4
- package/src/cli.ts +10 -3
- package/src/github-ci/index.test.ts +53 -4
- package/src/github-ci/index.ts +37 -5
- package/src/lib/json.ts +3 -1
- package/src/lib/workspace.ts +1 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
- package/src/monorepo/__tests__/publish-workflow.test.ts +39 -4
- 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 +58 -0
- package/src/monorepo/managed-files.ts +59 -76
- package/src/monorepo/package-policy.test.ts +31 -0
- package/src/monorepo/package-policy.ts +2 -2
- package/src/monorepo/packs/index.ts +3 -1
- package/src/monorepo/publish-workflow.ts +28 -15
- package/src/monorepo/runtime.test.ts +23 -17
- package/src/monorepo/runtime.ts +12 -17
- package/src/monorepo/tool-validation.test.ts +15 -8
- package/src/monorepo/tool-validation.ts +20 -25
- package/src/nx/index.test.ts +45 -14
- package/src/nx/index.ts +133 -64
- package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
|
@@ -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(
|
|
@@ -4,11 +4,14 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
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
|
+
import { type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
7
8
|
import {
|
|
9
|
+
deployTargetInfoFromProjects,
|
|
8
10
|
extractInlineLocalBlocksForTest,
|
|
9
11
|
INLINE_LOCAL_BEGIN,
|
|
10
12
|
INLINE_LOCAL_END,
|
|
11
13
|
LOCAL_SECTION_MARKER,
|
|
14
|
+
managedFileTargetsForTest,
|
|
12
15
|
platformTargetGlobsForTest,
|
|
13
16
|
reinsertInlineLocalBlocksForTest,
|
|
14
17
|
splitLocalSectionForTest,
|
|
@@ -172,6 +175,61 @@ describe('managed publish platform discovery', () => {
|
|
|
172
175
|
});
|
|
173
176
|
});
|
|
174
177
|
|
|
178
|
+
describe('nx graph project helpers', () => {
|
|
179
|
+
const sampleProjects: NxProjects = {
|
|
180
|
+
lib: {
|
|
181
|
+
targets: {
|
|
182
|
+
build: {},
|
|
183
|
+
'bundle-linux': {},
|
|
184
|
+
lint: {},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
app: {
|
|
188
|
+
targets: {
|
|
189
|
+
deploy: {
|
|
190
|
+
options: { command: 'echo no' },
|
|
191
|
+
configurations: {
|
|
192
|
+
staging: { command: 'wrangler deploy --env staging' },
|
|
193
|
+
production: { options: { command: 'wrangler deploy --env production' } },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
'package-macos': {},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
it('collects target names from graph nodes without project names', () => {
|
|
202
|
+
expect(targetNamesFromProjects(sampleProjects).sort()).toEqual(
|
|
203
|
+
['build', 'bundle-linux', 'deploy', 'lint', 'package-macos'].sort(),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('detects deploy configurations and cloudflare provider from graph nodes', () => {
|
|
208
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({
|
|
209
|
+
exists: true,
|
|
210
|
+
provider: 'cloudflare',
|
|
211
|
+
});
|
|
212
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'production')).toEqual({
|
|
213
|
+
exists: true,
|
|
214
|
+
provider: 'cloudflare',
|
|
215
|
+
});
|
|
216
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('managed raw files', () => {
|
|
221
|
+
it('manages the devenv wrapper as an executable byte-exact copy', async () => {
|
|
222
|
+
expect(managedFileTargetsForTest).toContainEqual({ target: 'tooling/devenv', executable: true });
|
|
223
|
+
|
|
224
|
+
const [source, generated] = await Promise.all([
|
|
225
|
+
readFile(join(REPO_ROOT, 'packages', 'cli', 'managed', 'raw', 'tooling', 'devenv'), 'utf8'),
|
|
226
|
+
readFile(join(REPO_ROOT, 'tooling', 'devenv'), 'utf8'),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
expect(generated).toBe(source);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
175
233
|
describe('managed cache actions', () => {
|
|
176
234
|
it('renders the checked-in action copies from their managed templates', async () => {
|
|
177
235
|
for (const action of CACHE_ACTIONS) {
|
|
@@ -1,10 +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
|
-
import
|
|
5
|
+
import type { NxTargetConfig, PackageJson } from '../lib/json.js';
|
|
7
6
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
7
|
+
import { loadNxProjects, type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
8
8
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
9
9
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
10
10
|
|
|
@@ -137,6 +137,7 @@ interface ManagedFileContext {
|
|
|
137
137
|
stagingDeployProvider?: 'cloudflare';
|
|
138
138
|
productionDeployProvider?: 'cloudflare';
|
|
139
139
|
ciPushBranches: string[];
|
|
140
|
+
ciRunsOn: string | string[];
|
|
140
141
|
nodeModulesCacheKey: string;
|
|
141
142
|
repoName: string;
|
|
142
143
|
platformTargetGlobs: string[];
|
|
@@ -153,6 +154,12 @@ const managedFiles: ManagedFile[] = [
|
|
|
153
154
|
source: 'envrc',
|
|
154
155
|
target: '.envrc',
|
|
155
156
|
},
|
|
157
|
+
{
|
|
158
|
+
kind: 'raw',
|
|
159
|
+
source: 'tooling/devenv',
|
|
160
|
+
target: 'tooling/devenv',
|
|
161
|
+
executable: true,
|
|
162
|
+
},
|
|
156
163
|
{
|
|
157
164
|
kind: 'raw',
|
|
158
165
|
source: 'tooling/direnv/repo-path',
|
|
@@ -199,11 +206,6 @@ const managedFiles: ManagedFile[] = [
|
|
|
199
206
|
target: 'tooling/direnv/merge-newer-pins.sh',
|
|
200
207
|
executable: true,
|
|
201
208
|
},
|
|
202
|
-
{
|
|
203
|
-
kind: 'raw',
|
|
204
|
-
source: 'patches/ttsc@0.19.3.patch',
|
|
205
|
-
target: 'patches/ttsc@0.19.3.patch',
|
|
206
|
-
},
|
|
207
209
|
{
|
|
208
210
|
kind: 'generated',
|
|
209
211
|
source: 'ci-workflow',
|
|
@@ -252,10 +254,12 @@ const managedFiles: ManagedFile[] = [
|
|
|
252
254
|
},
|
|
253
255
|
];
|
|
254
256
|
|
|
257
|
+
export const managedFileTargetsForTest = managedFiles.map(({ target, executable }) => ({ target, executable }));
|
|
258
|
+
|
|
255
259
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
256
260
|
|
|
257
|
-
export function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[] {
|
|
258
|
-
const context = getManagedFileContext(root);
|
|
261
|
+
export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
|
|
262
|
+
const context = await getManagedFileContext(root);
|
|
259
263
|
return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -306,6 +310,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
306
310
|
deploy: context.hasStagingDeployTargets,
|
|
307
311
|
deployProvider: context.stagingDeployProvider,
|
|
308
312
|
pushBranches: context.ciPushBranches,
|
|
313
|
+
runsOn: context.ciRunsOn,
|
|
309
314
|
});
|
|
310
315
|
}
|
|
311
316
|
if (file.source === 'publish-workflow') {
|
|
@@ -314,6 +319,7 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
314
319
|
deployProvider: context.productionDeployProvider,
|
|
315
320
|
repoName: context.repoName,
|
|
316
321
|
platformTargetGlobs: context.platformTargetGlobs,
|
|
322
|
+
runsOn: context.ciRunsOn,
|
|
317
323
|
});
|
|
318
324
|
}
|
|
319
325
|
throw new Error(`Unknown generated managed file source ${file.source}`);
|
|
@@ -327,13 +333,16 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
327
333
|
return renderTemplate(context, content);
|
|
328
334
|
}
|
|
329
335
|
|
|
330
|
-
function getManagedFileContext(root: string): ManagedFileContext {
|
|
336
|
+
async function getManagedFileContext(root: string): Promise<ManagedFileContext> {
|
|
331
337
|
const packageJson = readPackageJson(join(root, 'package.json'));
|
|
332
338
|
const repoName = packageJson?.name ?? 'monorepo';
|
|
333
339
|
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
const
|
|
340
|
+
const ciRunsOn = getCiRunsOn(packageJson?.json);
|
|
341
|
+
// In-process Nx API → daemon socket (no second Node/`nx` CLI process).
|
|
342
|
+
const nxProjects = await loadNxProjects(root);
|
|
343
|
+
const stagingDeploy = deployTargetInfoFromProjects(nxProjects, 'staging');
|
|
344
|
+
const productionDeploy = deployTargetInfoFromProjects(nxProjects, 'production');
|
|
345
|
+
const platformTargetGlobs = platformTargetGlobsForTest(targetNamesFromProjects(nxProjects));
|
|
337
346
|
const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
|
|
338
347
|
? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
|
|
339
348
|
: `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
|
|
@@ -344,6 +353,7 @@ function getManagedFileContext(root: string): ManagedFileContext {
|
|
|
344
353
|
stagingDeployProvider: stagingDeploy.provider,
|
|
345
354
|
productionDeployProvider: productionDeploy.provider,
|
|
346
355
|
ciPushBranches,
|
|
356
|
+
ciRunsOn,
|
|
347
357
|
nodeModulesCacheKey,
|
|
348
358
|
repoName,
|
|
349
359
|
platformTargetGlobs,
|
|
@@ -358,81 +368,54 @@ export function platformTargetGlobsForTest(targetNames: Iterable<string>): strin
|
|
|
358
368
|
});
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
cwd: root,
|
|
364
|
-
encoding: 'utf8',
|
|
365
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
366
|
-
});
|
|
367
|
-
const projects = parseStringArrayText(output);
|
|
368
|
-
if (!projects) {
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
const targetNames = new Set<string>();
|
|
372
|
-
for (const project of projects) {
|
|
373
|
-
const projectOutput = execFileSync('nx', ['show', 'project', project, '--json'], {
|
|
374
|
-
cwd: root,
|
|
375
|
-
encoding: 'utf8',
|
|
376
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
377
|
-
});
|
|
378
|
-
const projectJson = parseNxProjectJsonText(projectOutput);
|
|
379
|
-
const targets = projectJson?.targets;
|
|
380
|
-
if (targets) {
|
|
381
|
-
for (const targetName of Object.keys(targets)) {
|
|
382
|
-
targetNames.add(targetName);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return [...targetNames];
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function renderTemplate(context: ManagedFileContext, template: string): string {
|
|
390
|
-
return template
|
|
391
|
-
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
392
|
-
.replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
|
|
393
|
-
.replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function getDeployTargetInfo(root: string, configuration: string): DeployTargetInfo {
|
|
397
|
-
const output = execFileSync('nx', ['show', 'projects', '--withTarget', 'deploy', '--json'], {
|
|
398
|
-
cwd: root,
|
|
399
|
-
encoding: 'utf8',
|
|
400
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
401
|
-
});
|
|
402
|
-
const projects = parseStringArrayText(output);
|
|
403
|
-
if (!projects) {
|
|
404
|
-
return { exists: false };
|
|
405
|
-
}
|
|
371
|
+
/** Test seam: deploy configuration presence/provider from graph nodes. */
|
|
372
|
+
export function deployTargetInfoFromProjects(projects: NxProjects, configuration: string): DeployTargetInfo {
|
|
406
373
|
let exists = false;
|
|
407
374
|
let provider: DeployTargetInfo['provider'];
|
|
408
|
-
for (const project of projects) {
|
|
409
|
-
const
|
|
410
|
-
if (!
|
|
375
|
+
for (const project of Object.values(projects)) {
|
|
376
|
+
const info = deployTargetInfoFromTargets(project.targets ?? {}, configuration);
|
|
377
|
+
if (!info.exists) {
|
|
411
378
|
continue;
|
|
412
379
|
}
|
|
413
380
|
exists = true;
|
|
414
|
-
provider ??=
|
|
381
|
+
provider ??= info.provider;
|
|
415
382
|
}
|
|
416
383
|
return { exists, provider };
|
|
417
384
|
}
|
|
418
385
|
|
|
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];
|
|
386
|
+
function deployTargetInfoFromTargets(targets: Record<string, NxTargetConfig>, configuration: string): DeployTargetInfo {
|
|
387
|
+
const deploy = targets.deploy;
|
|
388
|
+
if (!deploy) {
|
|
389
|
+
return { exists: false };
|
|
390
|
+
}
|
|
391
|
+
const config = deploy.configurations?.[configuration];
|
|
428
392
|
if (!config) {
|
|
429
393
|
return { exists: false };
|
|
430
394
|
}
|
|
431
|
-
const commandValue = config.command ?? config.options?.command ?? deploy
|
|
395
|
+
const commandValue = config.command ?? config.options?.command ?? deploy.options?.command ?? deploy.command;
|
|
432
396
|
const command = typeof commandValue === 'string' ? commandValue : '';
|
|
433
397
|
return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
|
|
434
398
|
}
|
|
435
399
|
|
|
400
|
+
function renderTemplate(context: ManagedFileContext, template: string): string {
|
|
401
|
+
return template
|
|
402
|
+
.replaceAll('{{REPO_NAME}}', context.repoName)
|
|
403
|
+
.replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
|
|
404
|
+
.replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getCiRunsOn(packageJson: PackageJson | null | undefined): string | string[] {
|
|
408
|
+
const configured = packageJson?.smoo?.github?.runsOn;
|
|
409
|
+
if (configured === undefined) {
|
|
410
|
+
return 'ubuntu-latest';
|
|
411
|
+
}
|
|
412
|
+
if (typeof configured === 'string') {
|
|
413
|
+
return configured.length > 0 ? configured : 'ubuntu-latest';
|
|
414
|
+
}
|
|
415
|
+
const labels = configured.filter((label) => label.length > 0);
|
|
416
|
+
return labels.length > 0 ? labels : 'ubuntu-latest';
|
|
417
|
+
}
|
|
418
|
+
|
|
436
419
|
function getCiPushBranches(packageJson: PackageJson | null | undefined): string[] {
|
|
437
420
|
const configured = readCiPushBranches(packageJson);
|
|
438
421
|
return configured.length > 0 ? configured : ['main'];
|
|
@@ -458,8 +441,8 @@ export function printResults(results: FileResult[]): void {
|
|
|
458
441
|
}
|
|
459
442
|
}
|
|
460
443
|
|
|
461
|
-
export function validateManagedFiles(root: string): number {
|
|
462
|
-
const results = applyManagedFiles(root, 'check');
|
|
444
|
+
export async function validateManagedFiles(root: string): Promise<number> {
|
|
445
|
+
const results = await applyManagedFiles(root, 'check');
|
|
463
446
|
printResults(results);
|
|
464
447
|
const failures = results.filter((result) => result.action === 'drifted').length;
|
|
465
448
|
if (failures > 0) {
|
|
@@ -478,8 +461,8 @@ export function validateManagedFiles(root: string): number {
|
|
|
478
461
|
* Under GitHub Actions the drifted-file count is published as the step output
|
|
479
462
|
* `drifted`, so downstream steps gate declaratively instead of parsing logs.
|
|
480
463
|
*/
|
|
481
|
-
export function warnOnManagedFileDrift(root: string): void {
|
|
482
|
-
const results = applyManagedFiles(root, 'check');
|
|
464
|
+
export async function warnOnManagedFileDrift(root: string): Promise<void> {
|
|
465
|
+
const results = await applyManagedFiles(root, 'check');
|
|
483
466
|
printResults(results);
|
|
484
467
|
const drifted = results.filter((result) => result.action === 'drifted');
|
|
485
468
|
if (process.env.GITHUB_OUTPUT) {
|
|
@@ -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
|
}
|
|
@@ -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) +
|
|
@@ -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
|
]),
|