@smoothbricks/cli 0.10.2 → 0.10.4

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.
Files changed (133) hide show
  1. package/README.md +31 -23
  2. package/dist/bun/preload.d.ts +13 -0
  3. package/dist/bun/preload.d.ts.map +1 -0
  4. package/dist/bun/preload.js +12 -0
  5. package/dist/cli.js +36 -4
  6. package/dist/generate/index.d.ts.map +1 -1
  7. package/dist/generate/index.js +0 -6
  8. package/dist/github-ci/index.d.ts +17 -2
  9. package/dist/github-ci/index.d.ts.map +1 -1
  10. package/dist/github-ci/index.js +192 -26
  11. package/dist/github-ci/outputs.d.ts +22 -0
  12. package/dist/github-ci/outputs.d.ts.map +1 -0
  13. package/dist/github-ci/outputs.js +648 -0
  14. package/dist/lib/cli-package.js +2 -2
  15. package/dist/lib/json.d.ts +118 -11
  16. package/dist/lib/json.d.ts.map +1 -1
  17. package/dist/lib/json.js +317 -29
  18. package/dist/lib/workspace.d.ts +7 -14
  19. package/dist/lib/workspace.d.ts.map +1 -1
  20. package/dist/lib/workspace.js +76 -82
  21. package/dist/monorepo/ci-workflow.d.ts +2 -0
  22. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  23. package/dist/monorepo/ci-workflow.js +28 -2
  24. package/dist/monorepo/git-config.js +2 -2
  25. package/dist/monorepo/index.d.ts +3 -1
  26. package/dist/monorepo/index.d.ts.map +1 -1
  27. package/dist/monorepo/index.js +6 -2
  28. package/dist/monorepo/lockfile.d.ts +13 -1
  29. package/dist/monorepo/lockfile.d.ts.map +1 -1
  30. package/dist/monorepo/lockfile.js +33 -21
  31. package/dist/monorepo/managed-files.d.ts +12 -0
  32. package/dist/monorepo/managed-files.d.ts.map +1 -1
  33. package/dist/monorepo/managed-files.js +86 -28
  34. package/dist/monorepo/package-policy.d.ts.map +1 -1
  35. package/dist/monorepo/package-policy.js +78 -45
  36. package/dist/monorepo/packed-manifest.d.ts +5 -2
  37. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  38. package/dist/monorepo/packed-manifest.js +16 -47
  39. package/dist/monorepo/packed-package.d.ts.map +1 -1
  40. package/dist/monorepo/packed-package.js +28 -16
  41. package/dist/monorepo/packs/index.d.ts +3 -0
  42. package/dist/monorepo/packs/index.d.ts.map +1 -1
  43. package/dist/monorepo/packs/index.js +18 -6
  44. package/dist/monorepo/publish-workflow.d.ts +1 -0
  45. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  46. package/dist/monorepo/publish-workflow.js +242 -3
  47. package/dist/monorepo/runtime.d.ts +15 -0
  48. package/dist/monorepo/runtime.d.ts.map +1 -1
  49. package/dist/monorepo/runtime.js +66 -12
  50. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  51. package/dist/monorepo/tool-validation.js +32 -23
  52. package/dist/monorepo/wrangler.d.ts.map +1 -1
  53. package/dist/monorepo/wrangler.js +13 -13
  54. package/dist/nx/index.d.ts +12 -4
  55. package/dist/nx/index.d.ts.map +1 -1
  56. package/dist/nx/index.js +86 -19
  57. package/dist/pr/index.d.ts.map +1 -1
  58. package/dist/pr/index.js +28 -32
  59. package/dist/release/candidates.d.ts +3 -2
  60. package/dist/release/candidates.d.ts.map +1 -1
  61. package/dist/release/candidates.js +4 -5
  62. package/dist/release/github-release.d.ts +3 -0
  63. package/dist/release/github-release.d.ts.map +1 -1
  64. package/dist/release/github-release.js +11 -0
  65. package/dist/release/index.d.ts +20 -1
  66. package/dist/release/index.d.ts.map +1 -1
  67. package/dist/release/index.js +221 -89
  68. package/dist/release/orchestration.d.ts +8 -1
  69. package/dist/release/orchestration.d.ts.map +1 -1
  70. package/dist/release/orchestration.js +38 -1
  71. package/dist/wrangler/prepare-env.d.ts +0 -1
  72. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  73. package/dist/wrangler/prepare-env.js +100 -51
  74. package/dist/wrangler/scaffold.d.ts.map +1 -1
  75. package/dist/wrangler/scaffold.js +12 -12
  76. package/managed/raw/git-format-staged.yml +19 -1
  77. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
  78. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  79. package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
  80. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  81. package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
  82. package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
  83. package/managed/templates/github/actions/cache-nx/action.yml +2 -2
  84. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  85. package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
  86. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  87. package/managed/templates/github/workflows/managed-files.yml +80 -0
  88. package/package.json +18 -4
  89. package/src/bun/preload.ts +12 -0
  90. package/src/cli.ts +49 -11
  91. package/src/generate/index.ts +0 -6
  92. package/src/github-ci/index.test.ts +486 -0
  93. package/src/github-ci/index.ts +220 -30
  94. package/src/github-ci/outputs.ts +505 -0
  95. package/src/lib/cli-package.ts +2 -2
  96. package/src/lib/json.ts +291 -32
  97. package/src/lib/workspace.ts +88 -100
  98. package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
  99. package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
  100. package/src/monorepo/ci-workflow.ts +28 -2
  101. package/src/monorepo/git-config.ts +2 -2
  102. package/src/monorepo/index.ts +6 -2
  103. package/src/monorepo/lockfile.test.ts +35 -36
  104. package/src/monorepo/lockfile.ts +48 -24
  105. package/src/monorepo/managed-files.test.ts +63 -0
  106. package/src/monorepo/managed-files.ts +94 -34
  107. package/src/monorepo/package-policy.test.ts +70 -51
  108. package/src/monorepo/package-policy.ts +104 -59
  109. package/src/monorepo/packed-manifest.ts +18 -52
  110. package/src/monorepo/packed-package.ts +43 -22
  111. package/src/monorepo/packs/index.test.ts +18 -1
  112. package/src/monorepo/packs/index.ts +20 -7
  113. package/src/monorepo/publish-workflow.ts +482 -3
  114. package/src/monorepo/runtime.test.ts +42 -3
  115. package/src/monorepo/runtime.ts +90 -12
  116. package/src/monorepo/tool-validation.ts +35 -25
  117. package/src/monorepo/wrangler.test.ts +11 -15
  118. package/src/monorepo/wrangler.ts +15 -15
  119. package/src/nx/index.test.ts +43 -0
  120. package/src/nx/index.ts +110 -23
  121. package/src/pr/index.ts +42 -36
  122. package/src/release/__tests__/candidates.test.ts +5 -4
  123. package/src/release/__tests__/fixture-repo.test.ts +18 -16
  124. package/src/release/__tests__/github-release.test.ts +11 -0
  125. package/src/release/__tests__/orchestration.test.ts +57 -2
  126. package/src/release/__tests__/trust-publisher.test.ts +98 -2
  127. package/src/release/candidates.ts +10 -17
  128. package/src/release/github-release.ts +12 -0
  129. package/src/release/index.ts +321 -112
  130. package/src/release/orchestration.ts +55 -3
  131. package/src/wrangler/prepare-env.ts +75 -54
  132. package/src/wrangler/scaffold.test.ts +9 -12
  133. package/src/wrangler/scaffold.ts +13 -13
@@ -1,8 +1,29 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { appendFile, mkdtemp, realpath, rename, rm } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { $ } from 'bun';
5
+ import typia from 'typia';
6
+ import { parseStringArrayText } from '../lib/json.js';
5
7
  import { decode, run, runStatus } from '../lib/run.js';
8
+ import { type ProjectTargets, readProjectTargets } from '../nx/index.js';
9
+ import type { NxTargetRun } from './outputs.js';
10
+
11
+ interface GithubActionsEventPayload {
12
+ repository?: {
13
+ default_branch?: string;
14
+ };
15
+ }
16
+
17
+ interface NxProjectDeployConfigurations {
18
+ targets?: {
19
+ deploy?: {
20
+ configurations?: Record<string, unknown>;
21
+ };
22
+ };
23
+ }
24
+
25
+ const parseGithubActionsEvent = typia.json.createIsParse<GithubActionsEventPayload>();
26
+ const parseNxProjectDeployConfigurations = typia.json.createIsParse<NxProjectDeployConfigurations>();
6
27
 
7
28
  type NxSmartMode = 'auto' | 'affected' | 'run-many';
8
29
 
@@ -113,6 +134,15 @@ async function addReferencesFrom(roots: Set<string>, path: string, cwd: string):
113
134
  }
114
135
  }
115
136
 
137
+ export function nxSmartArgs(target: string, mode: 'affected' | 'run-many', configuration?: string): string[] {
138
+ const args = [mode, '-t', target];
139
+ if (configuration) {
140
+ args.push(`--configuration=${configuration}`);
141
+ }
142
+ args.push(`--exclude=tag:ci:skip:${target}`, '--parallel=5');
143
+ return args;
144
+ }
145
+
116
146
  export async function githubCiNxSmart(
117
147
  root: string,
118
148
  options: { target: string; name?: string; step?: string; mode?: NxSmartMode; configuration?: string },
@@ -121,11 +151,7 @@ export async function githubCiNxSmart(
121
151
  const step = options.step ?? '';
122
152
  await createGithubStatus(name, step);
123
153
  const mode = resolveNxSmartMode(options.mode ?? 'auto');
124
- const nxArgs = mode === 'run-many' ? ['run-many', '-t', options.target] : ['affected', '-t', options.target];
125
- if (options.configuration) {
126
- nxArgs.push(`--configuration=${options.configuration}`);
127
- }
128
- nxArgs.push('--parallel=5');
154
+ const nxArgs = nxSmartArgs(options.target, mode, options.configuration);
129
155
  const status = await runStatus('nx', nxArgs, root);
130
156
  await updateGithubStatus(name, status === 0 ? 'success' : 'failure', step);
131
157
  if (status !== 0) {
@@ -133,18 +159,170 @@ export async function githubCiNxSmart(
133
159
  }
134
160
  }
135
161
 
136
- export async function githubCiNxRunMany(
162
+ export interface NxRunManyOptions {
163
+ targets: string;
164
+ projects?: string;
165
+ configuration?: string;
166
+ collectOutputs?: string;
167
+ allowEmptyProjects?: boolean;
168
+ }
169
+
170
+ export interface ExpandedNxTargetRuns {
171
+ runs: NxTargetRun[];
172
+ unmatchedGlobs: string[];
173
+ }
174
+
175
+ export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunManyOptions): ExpandedNxTargetRuns {
176
+ const selectedProjects = selectProjects(projects, options.projects);
177
+ if (options.projects !== undefined && selectedProjects.length === 0 && options.allowEmptyProjects !== true) {
178
+ throw new Error(`No Nx projects matched --projects ${options.projects}.`);
179
+ }
180
+ const selectedTargetNames = [...new Set(selectedProjects.flatMap((project) => project.targets))].sort((a, b) =>
181
+ a.localeCompare(b),
182
+ );
183
+ const runs: NxTargetRun[] = [];
184
+ const unmatchedGlobs: string[] = [];
185
+ const addedTargets = new Set<string>();
186
+ for (const targetPattern of commaSeparatedValues(options.targets)) {
187
+ const isGlob = isGlobPattern(targetPattern);
188
+ const targets = isGlob
189
+ ? selectedTargetNames.filter((target) => new Bun.Glob(targetPattern).match(target))
190
+ : [targetPattern];
191
+ if (isGlob && targets.length === 0) {
192
+ unmatchedGlobs.push(targetPattern);
193
+ }
194
+ for (const target of targets) {
195
+ if (addedTargets.has(target)) {
196
+ continue;
197
+ }
198
+ const owners = selectedProjects.filter((project) => project.targets.includes(target));
199
+ const runProjects = owners.length > 0 ? owners : selectedProjects;
200
+ if (runProjects.length === 0) {
201
+ continue;
202
+ }
203
+ addedTargets.add(target);
204
+ runs.push({ target, projects: runProjects });
205
+ }
206
+ }
207
+ return { runs, unmatchedGlobs };
208
+ }
209
+
210
+ export function expandNxTargetDependencyRuns(runs: NxTargetRun[]): NxTargetRun[] {
211
+ const expanded: NxTargetRun[] = [];
212
+ const added = new Set<string>();
213
+ const visiting = new Set<string>();
214
+
215
+ const visit = (project: ProjectTargets, target: string): void => {
216
+ const key = `${project.project}:${target}`;
217
+ if (added.has(key)) {
218
+ return;
219
+ }
220
+ if (visiting.has(key)) {
221
+ throw new Error(`Nx target dependency cycle detected at ${key}.`);
222
+ }
223
+ visiting.add(key);
224
+ for (const dependency of project.targetDependencies?.get(target) ?? []) {
225
+ if (dependency.startsWith('^')) {
226
+ continue;
227
+ }
228
+ const dependencyTargets = isGlobPattern(dependency)
229
+ ? project.targets.filter((candidate) => new Bun.Glob(dependency).match(candidate))
230
+ : project.targets.includes(dependency)
231
+ ? [dependency]
232
+ : [];
233
+ for (const dependencyTarget of dependencyTargets.sort((left, right) => left.localeCompare(right))) {
234
+ visit(project, dependencyTarget);
235
+ }
236
+ }
237
+ visiting.delete(key);
238
+ added.add(key);
239
+ if ((project.targetOutputs?.get(target)?.length ?? 0) > 0) {
240
+ expanded.push({ target, projects: [project] });
241
+ }
242
+ };
243
+
244
+ for (const run of runs) {
245
+ for (const project of run.projects) {
246
+ visit(project, run.target);
247
+ }
248
+ }
249
+ return expanded;
250
+ }
251
+
252
+ export function nxRunManyArgs(run: NxTargetRun, configuration?: string): string[] {
253
+ if (run.projects.length === 0) {
254
+ throw new Error(`Nx target ${run.target} has no selected projects.`);
255
+ }
256
+ const nxArgs = [
257
+ 'run-many',
258
+ '-t',
259
+ run.target,
260
+ `--projects=${run.projects.map((project) => project.project).join(',')}`,
261
+ ];
262
+ if (configuration) {
263
+ nxArgs.push(`--configuration=${configuration}`);
264
+ }
265
+ nxArgs.push('--parallel=5');
266
+ return nxArgs;
267
+ }
268
+
269
+ export async function readGitHeadSha(root: string): Promise<string> {
270
+ // invariant throw: GitHub CI commands require a valid repository root.
271
+ return decode((await $`git rev-parse HEAD`.cwd(root).quiet()).stdout).trim();
272
+ }
273
+
274
+ export async function githubCiNxRunMany(root: string, options: NxRunManyOptions): Promise<void> {
275
+ const expanded = expandNxTargetRuns(await readProjectTargets(root), options);
276
+ if (expanded.unmatchedGlobs.length > 0) {
277
+ console.log(`No Nx targets matched target glob(s): ${expanded.unmatchedGlobs.join(', ')}; skipping.`);
278
+ }
279
+ for (const targetRun of expanded.runs) {
280
+ await run('nx', nxRunManyArgs(targetRun, options.configuration), root);
281
+ }
282
+ if (options.collectOutputs) {
283
+ const { collectNxOutputs } = await loadOutputBoundary();
284
+ const sourceSha = await readGitHeadSha(root);
285
+ await collectNxOutputs(root, options.collectOutputs, expandNxTargetDependencyRuns(expanded.runs), sourceSha);
286
+ }
287
+ }
288
+
289
+ export async function githubCiApplyOutputs(
137
290
  root: string,
138
- options: { targets: string; projects?: string; configuration?: string },
291
+ directories: string[],
292
+ expectedSourceSha: string,
139
293
  ): Promise<void> {
140
- const nxArgs = ['run-many', '-t', options.targets, '--parallel=5'];
141
- if (options.projects) {
142
- nxArgs.push(`--projects=${options.projects}`);
294
+ const { applyCollectedOutputs } = await loadOutputBoundary();
295
+ await applyCollectedOutputs(root, directories, expectedSourceSha);
296
+ }
297
+
298
+ async function loadOutputBoundary() {
299
+ if (import.meta.url.endsWith('/src/github-ci/index.ts')) {
300
+ // The source self-hosting shim has no Typia transform; register it before loading manifest validators.
301
+ await import('@smoothbricks/cli/bun/preload');
143
302
  }
144
- if (options.configuration) {
145
- nxArgs.push(`--configuration=${options.configuration}`);
303
+ // This boundary must stay lazy because the transformed dist and source self-hosting paths initialize Typia differently.
304
+ return import('./outputs.js');
305
+ }
306
+
307
+ function isGlobPattern(value: string): boolean {
308
+ return /[*?{[]/.test(value);
309
+ }
310
+
311
+ function selectProjects(projects: ProjectTargets[], selectors: string | undefined): ProjectTargets[] {
312
+ if (selectors === undefined) {
313
+ return projects.slice().sort((left, right) => left.project.localeCompare(right.project));
146
314
  }
147
- await run('nx', nxArgs, root);
315
+ const patterns = commaSeparatedValues(selectors);
316
+ return projects
317
+ .filter((project) => patterns.some((pattern) => new Bun.Glob(pattern).match(project.project)))
318
+ .sort((left, right) => left.project.localeCompare(right.project));
319
+ }
320
+
321
+ function commaSeparatedValues(value: string): string[] {
322
+ return value
323
+ .split(',')
324
+ .map((entry) => entry.trim())
325
+ .filter(Boolean);
148
326
  }
149
327
 
150
328
  export async function githubCiNxDeploy(
@@ -182,7 +360,31 @@ function resolveNxSmartMode(mode: NxSmartMode): 'affected' | 'run-many' {
182
360
  if (mode === 'affected' || mode === 'run-many') {
183
361
  return mode;
184
362
  }
185
- return process.env.GITHUB_EVENT_NAME === 'push' && process.env.GITHUB_REF_NAME === 'main' ? 'run-many' : 'affected';
363
+ const defaultBranch = eventDefaultBranch() ?? 'main';
364
+ if (process.env.GITHUB_EVENT_NAME === 'push') {
365
+ return process.env.GITHUB_REF_NAME === defaultBranch ? 'run-many' : 'affected';
366
+ }
367
+ // A PR into a NON-default branch is an integration surface (e.g. a mirror-sync
368
+ // review branch): its base moves outside the default-branch workflow that
369
+ // affected scoping is calibrated against, so under-selection can pass PR CI
370
+ // and only fail after merge. Validate those PRs in full.
371
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
372
+ const base = process.env.GITHUB_BASE_REF;
373
+ return base && base !== defaultBranch ? 'run-many' : 'affected';
374
+ }
375
+ return 'affected';
376
+ }
377
+
378
+ /** The repository default branch from the Actions event payload. */
379
+ function eventDefaultBranch(): string | undefined {
380
+ const eventPath = process.env.GITHUB_EVENT_PATH;
381
+ if (!eventPath) return undefined;
382
+ try {
383
+ const payload = parseGithubActionsEvent(readFileSync(eventPath, 'utf8'));
384
+ return payload?.repository?.default_branch || undefined;
385
+ } catch {
386
+ return undefined;
387
+ }
186
388
  }
187
389
 
188
390
  async function deployProjectsWithConfiguration(
@@ -207,24 +409,12 @@ async function deployProjectsWithConfiguration(
207
409
 
208
410
  async function deployTargetHasConfiguration(root: string, project: string, configuration: string): Promise<boolean> {
209
411
  const result = await $`nx show project ${project} --json`.cwd(root).quiet();
210
- const parsed: unknown = JSON.parse(decode(result.stdout));
211
- const targets = recordValue(parsed)?.targets;
212
- const deploy = recordValue(targets)?.deploy;
213
- const configurations = recordValue(deploy)?.configurations;
214
- return recordValue(configurations)?.[configuration] !== undefined;
412
+ const parsed = parseNxProjectDeployConfigurations(decode(result.stdout));
413
+ return parsed?.targets?.deploy?.configurations?.[configuration] !== undefined;
215
414
  }
216
415
 
217
416
  function nxProjectList(output: string): string[] {
218
- const parsed: unknown = JSON.parse(output);
219
- return Array.isArray(parsed) ? parsed.filter((project): project is string => typeof project === 'string') : [];
220
- }
221
-
222
- function recordValue(value: unknown): Record<string, unknown> | undefined {
223
- return isRecord(value) ? value : undefined;
224
- }
225
-
226
- function isRecord(value: unknown): value is Record<string, unknown> {
227
- return value !== null && typeof value === 'object' && !Array.isArray(value);
417
+ return parseStringArrayText(output) ?? [];
228
418
  }
229
419
 
230
420
  async function createGithubStatus(name: string, step: string): Promise<void> {