code-foundry 0.1.3 → 0.2.0

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 (37) hide show
  1. package/.githooks/pre-commit +20 -5
  2. package/.github/CODEOWNERS +4 -2
  3. package/.github/CONTRIBUTING.md +5 -1
  4. package/.github/actions/cache/action.yml +43 -0
  5. package/.github/actions/codeql/action.yml +29 -0
  6. package/.github/actions/setup/action.yml +756 -0
  7. package/.github/scripts/bootstrap.sh +7 -0
  8. package/.github/scripts/changed-files.sh +96 -0
  9. package/.github/scripts/ci.sh +421 -28
  10. package/.github/scripts/codeql-languages.sh +71 -5
  11. package/.github/scripts/doctor.sh +1 -1
  12. package/.github/scripts/format-fast-path.sh +80 -0
  13. package/.github/scripts/init-repo.sh +39 -11
  14. package/.github/scripts/profile.sh +179 -0
  15. package/.github/scripts/security.sh +180 -20
  16. package/.github/scripts/sync-codeowners.sh +76 -0
  17. package/.github/scripts/sync-protection.sh +15 -2
  18. package/.github/scripts/sync-template.sh +222 -15
  19. package/.github/scripts/turbo-cache-probe.sh +102 -0
  20. package/.github/template.yml +14 -0
  21. package/.github/template.yml.example +10 -0
  22. package/.github/workflows/ci.yml +108 -7
  23. package/.github/workflows/codeql.yml +126 -17
  24. package/.github/workflows/draft-pr.yml +4 -7
  25. package/.github/workflows/release-pr.yml +20 -7
  26. package/.github/workflows/release.yml +99 -7
  27. package/.github/workflows/security.yml +201 -6
  28. package/.github/workflows/test.yml +100 -3
  29. package/.gitignore +4 -0
  30. package/.mise.toml +0 -2
  31. package/AGENTS.md +1 -0
  32. package/CHANGELOG.md +12 -0
  33. package/README.md +92 -10
  34. package/package.json +11 -1
  35. package/release-please-config.json +16 -0
  36. package/src/cli.mjs +20 -8
  37. package/.github/workflows/publish.yml +0 -25
@@ -1,30 +1,104 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ source .github/scripts/changed-files.sh
5
+
4
6
  if [ -d .venv/bin ]; then export PATH="$PWD/.venv/bin:$PATH"; fi
5
7
 
8
+ # Build one tracked-file inventory for the detectors below. These functions are
9
+ # called repeatedly by parallel workflow jobs, so avoid starting Git for each
10
+ # language check.
11
+ tracked_files="$(git ls-files)"
12
+
6
13
  has_script() {
7
14
  [ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.[process.argv[1]] ? 0 : 1)' "$1"
8
15
  }
9
16
 
17
+ has_turbo_script() {
18
+ [ -f package.json ] && node -e 'const p=require("./package.json"); const script=p.scripts?.[process.argv[1]] || ""; process.exit(/(^|[\s;&|])turbo(\s|$)/.test(script) ? 0 : 1)' "$1"
19
+ }
20
+
10
21
  has_bun_native_coverage() {
11
22
  [ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.["test:coverage"]?.includes("bun test") ? 0 : 1)'
12
23
  }
13
24
 
14
25
  has_javascript() {
15
- git ls-files -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .
26
+ printf '%s\n' "$tracked_files" | grep -Eq '(^|/)[^/]+\.(js|jsx|mjs|cjs|ts|tsx|mts|cts)$'
16
27
  }
17
28
 
18
29
  has_python() {
19
30
  [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] || \
20
- git ls-files -- '*.py' ':!.github/**' | grep -q .
31
+ printf '%s\n' "$tracked_files" | awk '!/^\.github\// && /(^|\/)[^\/]+\.py$/ { found=1; exit } END { exit !found }'
32
+ }
33
+
34
+ has_python_dependencies() {
35
+ [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] ||
36
+ [ -f Pipfile.lock ] || [ -f poetry.lock ] || [ -f uv.lock ]
37
+ }
38
+
39
+ needs_python_environment() {
40
+ has_python_dependencies || ! command -v ruff >/dev/null 2>&1
41
+ }
42
+
43
+ has_javascript_dependencies() {
44
+ [ -f bun.lock ] || [ -f bun.lockb ] || [ -f pnpm-lock.yaml ] ||
45
+ [ -f yarn.lock ] || [ -f package-lock.json ] || {
46
+ [ -f package.json ] || return 1
47
+ REPO_FOUNDRY_TRACKED_FILES="$tracked_files" node <<'NODE'
48
+ const files = (process.env.REPO_FOUNDRY_TRACKED_FILES || '')
49
+ .split('\n')
50
+ .filter((file) => /(^|\/)package\.json$/.test(file));
51
+ const groups = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
52
+ const hasDependencies = files.some((file) => {
53
+ try {
54
+ const packageJson = JSON.parse(require('node:fs').readFileSync(file, 'utf8'));
55
+ return groups.some((group) => packageJson[group] && Object.keys(packageJson[group]).length > 0) ||
56
+ Array.isArray(packageJson.workspaces) ||
57
+ (packageJson.workspaces && typeof packageJson.workspaces === 'object');
58
+ } catch {
59
+ return true;
60
+ }
61
+ });
62
+ process.exit(hasDependencies ? 0 : 1);
63
+ NODE
64
+ }
21
65
  }
22
66
 
23
67
  has_graph_project() {
24
68
  [ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.devDependencies?.["@graphprotocol/graph-cli"] ? 0 : 1)'
25
69
  }
26
70
 
71
+ has_browser_dependencies() {
72
+ if [ -f package.json ] && node <<'NODE'
73
+ const fs = require('node:fs');
74
+ const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
75
+ const groups = [p.dependencies, p.devDependencies, p.optionalDependencies, p.peerDependencies];
76
+ const names = groups.flatMap((group) => Object.keys(group || {}));
77
+ const scripts = Object.values(p.scripts || {});
78
+ process.exit([...names, ...scripts].some((value) => /playwright|cypress|puppeteer/i.test(value)) ? 0 : 1);
79
+ NODE
80
+ then
81
+ return 0
82
+ fi
83
+
84
+ for manifest in pyproject.toml requirements.txt requirements-dev.txt; do
85
+ if [ -f "$manifest" ] && grep -Eiq 'playwright|cypress|puppeteer' "$manifest"; then
86
+ return 0
87
+ fi
88
+ done
89
+ return 1
90
+ }
91
+
27
92
  package_manager() {
93
+ local configured=""
94
+ if [ -x .github/scripts/profile.sh ]; then
95
+ configured="$(bash .github/scripts/profile.sh get package_manager 2>/dev/null || true)"
96
+ elif [ -f .github/template.yml ]; then
97
+ configured="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)"
98
+ fi
99
+ case "$configured" in
100
+ bun|pnpm|yarn|npm) echo "$configured"; return ;;
101
+ esac
28
102
  if [ -f bun.lock ] || [ -f bun.lockb ]; then echo bun
29
103
  elif [ -f pnpm-lock.yaml ]; then echo pnpm
30
104
  elif [ -f yarn.lock ]; then echo yarn
@@ -33,8 +107,32 @@ package_manager() {
33
107
  fi
34
108
  }
35
109
 
110
+ cargo_run() {
111
+ local command="$1"
112
+ shift
113
+ local -a cargo_args=("$@")
114
+ if [ -f Cargo.lock ]; then
115
+ cargo_args+=(--locked)
116
+ fi
117
+ if [ "${CARGO_NET_OFFLINE:-false}" = true ]; then
118
+ cargo_args+=(--offline)
119
+ cargo "$command" "${cargo_args[@]}"
120
+ else
121
+ cargo "$command" "${cargo_args[@]}"
122
+ fi
123
+ }
124
+
36
125
  run_script() {
37
126
  if ! has_script "$1"; then echo "Skipping $1 (script not defined)"; return; fi
127
+ if [ "${REPO_FOUNDRY_TURBO_REMOTE_ONLY:-false}" = true ] && has_turbo_script "$1"; then
128
+ case "$(package_manager)" in
129
+ bun) bun run "$1" -- --cache=remote:rw ;;
130
+ pnpm) corepack pnpm run "$1" -- --cache=remote:rw ;;
131
+ yarn) corepack yarn run "$1" -- --cache=remote:rw ;;
132
+ npm) npm run "$1" -- --cache=remote:rw ;;
133
+ esac
134
+ return
135
+ fi
38
136
  case "$(package_manager)" in
39
137
  bun) bun run "$1" ;;
40
138
  pnpm) corepack pnpm run "$1" ;;
@@ -90,25 +188,151 @@ PY
90
188
  fi
91
189
  }
92
190
 
93
- install() {
94
- if [ -f package.json ]; then
191
+ install_javascript() {
192
+ if [ -n "${REPO_FOUNDRY_TASK:-}" ] && ! task_uses_javascript "$REPO_FOUNDRY_TASK"; then
193
+ echo "Skipping JavaScript dependency install (no $REPO_FOUNDRY_TASK JavaScript suite)"
194
+ return
195
+ fi
196
+ if [ "${REPO_FOUNDRY_INSTALL_JAVASCRIPT:-true}" = true ] && [ -f package.json ] && has_javascript_dependencies &&
197
+ { [ "${REPO_FOUNDRY_JAVASCRIPT_CACHE_HIT:-false}" != true ] || [ ! -d node_modules ]; }; then
95
198
  case "$(package_manager)" in
96
199
  bun) bun install --frozen-lockfile ;;
97
- pnpm) corepack pnpm install --frozen-lockfile ;;
200
+ pnpm) corepack pnpm install --frozen-lockfile --prefer-offline ;;
98
201
  yarn) corepack yarn install --immutable ;;
99
- npm) npm ci ;;
202
+ npm) npm ci --prefer-offline --no-audit --fund=false ;;
100
203
  esac
204
+ elif [ -f package.json ] && ! has_javascript_dependencies; then
205
+ echo "Skipping JavaScript dependency install (no dependencies found)"
206
+ elif [ -f package.json ]; then
207
+ echo "Using cached JavaScript dependencies"
208
+ fi
209
+ }
210
+
211
+ install_rust() {
212
+ if [ -n "${REPO_FOUNDRY_TASK:-}" ] && ! task_uses_rust "$REPO_FOUNDRY_TASK"; then
213
+ echo "Skipping Rust dependency install (no $REPO_FOUNDRY_TASK Rust suite)"
214
+ return
215
+ fi
216
+ if [ "${REPO_FOUNDRY_INSTALL_RUST:-true}" = true ] && [ -f Cargo.toml ] && [ "${REPO_FOUNDRY_RUST_CACHE_HIT:-false}" != true ]; then
217
+ cargo_run fetch
218
+ elif [ "${REPO_FOUNDRY_INSTALL_RUST:-true}" = true ] && [ -f Cargo.toml ]; then
219
+ echo "Using cached Rust packages"
101
220
  fi
102
- if [ -f Cargo.toml ]; then cargo fetch --locked; fi
103
- if has_python; then python -m venv .venv; .venv/bin/python -m pip install --disable-pip-version-check --quiet ruff; fi
104
- if [ -f requirements.txt ]; then .venv/bin/python -m pip install --disable-pip-version-check -r requirements.txt; fi
105
- if [ -f requirements-dev.txt ]; then .venv/bin/python -m pip install --disable-pip-version-check -r requirements-dev.txt; fi
221
+ }
222
+
223
+ install_python() {
224
+ if [ -n "${REPO_FOUNDRY_TASK:-}" ] && ! task_uses_python "$REPO_FOUNDRY_TASK"; then
225
+ echo "Skipping Python dependency install (no $REPO_FOUNDRY_TASK Python suite)"
226
+ return
227
+ fi
228
+ if [ "${REPO_FOUNDRY_INSTALL_PYTHON:-true}" = true ] && has_python &&
229
+ needs_python_environment &&
230
+ { [ "${REPO_FOUNDRY_PYTHON_CACHE_HIT:-false}" != true ] || [ ! -x .venv/bin/python ]; }; then
231
+ install_python_with_uv() {
232
+ if [ -f pyproject.toml ] && [ -f uv.lock ]; then
233
+ uv sync --frozen --python "$(command -v python)"
234
+ return
235
+ fi
236
+ uv venv --python "$(command -v python)" .venv
237
+ local -a python_packages=()
238
+ command -v ruff >/dev/null 2>&1 || python_packages+=(ruff)
239
+ if [ -f requirements.txt ]; then python_packages+=(-r requirements.txt); fi
240
+ if [ -f requirements-dev.txt ]; then python_packages+=(-r requirements-dev.txt); fi
241
+ if [ "${#python_packages[@]}" -gt 0 ]; then
242
+ uv pip install --python .venv/bin/python "${python_packages[@]}"
243
+ fi
244
+ }
245
+ if command -v uv >/dev/null 2>&1 && install_python_with_uv; then
246
+ echo "Installed Python dependencies with uv"
247
+ else
248
+ python -m venv .venv
249
+ local -a python_packages=()
250
+ command -v ruff >/dev/null 2>&1 || python_packages+=(ruff)
251
+ if [ -f requirements.txt ]; then python_packages+=(-r requirements.txt); fi
252
+ if [ -f requirements-dev.txt ]; then python_packages+=(-r requirements-dev.txt); fi
253
+ if [ "${#python_packages[@]}" -gt 0 ]; then
254
+ .venv/bin/python -m pip install --disable-pip-version-check "${python_packages[@]}"
255
+ fi
256
+ fi
257
+ elif [ "${REPO_FOUNDRY_INSTALL_PYTHON:-true}" = true ] && has_python && needs_python_environment; then
258
+ echo "Using cached Python environment"
259
+ elif [ "${REPO_FOUNDRY_INSTALL_PYTHON:-true}" = true ] && has_python; then
260
+ echo "Using shared Python tools; project environment not required"
261
+ fi
262
+ }
263
+
264
+ task_uses_javascript() {
265
+ local task="$1"
266
+ case "$task" in
267
+ unit) has_script test:unit || has_script test:coverage || { has_script test && ! has_script test:integration; } ;;
268
+ integration) has_script test:integration ;;
269
+ e2e) has_script test:e2e || has_script e2e ;;
270
+ smoke) has_script test:smoke || has_script smoke ;;
271
+ *) return 0 ;;
272
+ esac
273
+ }
274
+
275
+ task_uses_rust() {
276
+ local task="$1"
277
+ [ -f Cargo.toml ] || return 1
278
+ case "$task" in
279
+ unit) has_rust_target lib || has_rust_target bin ;;
280
+ integration) [ -d tests ] ;;
281
+ e2e|smoke) return 1 ;;
282
+ *) return 0 ;;
283
+ esac
284
+ }
285
+
286
+ task_uses_python() {
287
+ local task="$1"
288
+ has_python_tests() {
289
+ local directory="$1"
290
+ [ -d "$directory" ] && find "$directory" -type f -name '*.py' -print -quit | grep -q .
291
+ }
292
+ has_python || return 1
293
+ case "$task" in
294
+ unit) has_python_tests tests/unit || { [ ! -d tests/unit ] && [ ! -d tests/integration ] && has_python_tests tests; } ;;
295
+ integration) has_python_tests tests/integration ;;
296
+ e2e) has_python_tests tests/e2e ;;
297
+ smoke) has_python_tests tests/smoke ;;
298
+ *) return 0 ;;
299
+ esac
300
+ }
301
+
302
+ task_profile() {
303
+ local task="${1:?missing test task}"
304
+ local output applicable
305
+ case "$task" in
306
+ unit|integration|e2e|smoke) ;;
307
+ *) echo "unknown test task: $task" >&2; return 2 ;;
308
+ esac
309
+ output="$(should_run "$task")"
310
+ applicable="$(printf '%s\n' "$output" | awk -F= '$1 == "applicable" {print $2; exit}')"
311
+ printf 'applicable=%s\n' "${applicable:-false}"
312
+ if [ "$task" = e2e ]; then
313
+ printf 'browser=%s\n' "$(printf '%s\n' "$output" | awk -F= '$1 == "browser" {print $2; exit}' | awk 'NF {print; found=1} END {if (!found) print "false"}')"
314
+ fi
315
+ if [ "${applicable:-false}" = true ]; then
316
+ task_uses_javascript "$task" && printf 'javascript=true\n' || printf 'javascript=false\n'
317
+ task_uses_python "$task" && printf 'python=true\n' || printf 'python=false\n'
318
+ task_uses_rust "$task" && printf 'rust=true\n' || printf 'rust=false\n'
319
+ else
320
+ printf '%s\n' 'javascript=false' 'python=false' 'rust=false'
321
+ fi
322
+ }
323
+
324
+ install() {
325
+ run_parallel install_javascript install_rust install_python
106
326
  }
107
327
 
108
328
  rust_component() {
109
329
  local component="$1"
110
330
  if command -v rustup >/dev/null 2>&1; then
111
331
  local toolchain="${RUSTUP_TOOLCHAIN:-$(rustup show active-toolchain | awk '{print $1}')}"
332
+ if rustup component list --toolchain "$toolchain" 2>/dev/null |
333
+ grep -q "^${component}.*(installed)"; then
334
+ return
335
+ fi
112
336
  rustup component add --toolchain "$toolchain" "$component" >/dev/null
113
337
  fi
114
338
  }
@@ -119,29 +343,74 @@ has_rust_target() {
119
343
  jq -e --arg kind "$kind" 'any(.packages[].targets[]; (.kind | index($kind)) != null)' >/dev/null
120
344
  }
121
345
 
122
- format() {
346
+ run_parallel() {
347
+ local status=0 pid task
348
+ local -a pids=()
349
+ for task in "$@"; do
350
+ "$task" &
351
+ pids+=("$!")
352
+ done
353
+ for pid in "${pids[@]}"; do
354
+ if ! wait "$pid"; then status=1; fi
355
+ done
356
+ return "$status"
357
+ }
358
+
359
+ format_javascript() {
123
360
  if has_script format:check; then run_script format:check
124
- elif has_javascript; then run_package_tool prettier --check .
361
+ elif has_javascript; then run_package_tool prettier --check . --cache --cache-strategy content
125
362
  else echo "Skipping JavaScript/TypeScript formatting (no formatter script or source found)"; fi
363
+ }
364
+
365
+ format_rust() {
126
366
  if [ -f Cargo.toml ]; then rust_component rustfmt; cargo fmt --check; fi
367
+ }
368
+
369
+ format_python() {
127
370
  if has_python && command -v ruff >/dev/null 2>&1; then ruff format --check .; fi
128
371
  }
129
372
 
130
- lint() {
373
+ format() {
374
+ run_parallel format_javascript format_rust format_python
375
+ }
376
+
377
+ lint_javascript() {
131
378
  if has_script lint; then run_script lint
132
- elif has_javascript; then run_package_tool eslint .
379
+ elif has_javascript; then run_package_tool eslint --cache --cache-strategy content .
133
380
  else echo "Skipping JavaScript/TypeScript lint (no lint script or source found)"; fi
134
- if [ -f Cargo.toml ]; then rust_component clippy; cargo clippy --all-targets -- -D warnings; fi
381
+ }
382
+
383
+ lint_rust() {
384
+ if [ -f Cargo.toml ]; then
385
+ rust_component clippy
386
+ clippy_args=(--all-targets)
387
+ if [ -f Cargo.lock ]; then clippy_args+=(--locked); fi
388
+ if [ "${CARGO_NET_OFFLINE:-false}" = true ]; then clippy_args+=(--offline); fi
389
+ cargo clippy "${clippy_args[@]}" -- -D warnings
390
+ fi
391
+ }
392
+
393
+ lint_python() {
135
394
  if has_python && command -v ruff >/dev/null 2>&1; then ruff check .; fi
136
395
  }
137
396
 
138
- type_check() {
397
+ lint() {
398
+ run_parallel lint_javascript lint_rust lint_python
399
+ }
400
+
401
+ typecheck_javascript() {
139
402
  if has_graph_project; then
140
403
  echo "Skipping TypeScript type-check (Graph AssemblyScript project uses graph build/codegen)"
141
404
  elif has_script type-check; then run_script type-check
142
405
  elif has_script typecheck; then run_script typecheck
143
406
  else echo "Skipping type-check (script not defined)"; fi
144
- if [ -f Cargo.toml ]; then cargo check; fi
407
+ }
408
+
409
+ typecheck_rust() {
410
+ if [ -f Cargo.toml ]; then cargo_run check; fi
411
+ }
412
+
413
+ typecheck_python() {
145
414
  if has_python && command -v python >/dev/null 2>&1; then
146
415
  py_dirs=()
147
416
  for dir in tests src scripts; do [ -d "$dir" ] && py_dirs+=("$dir"); done
@@ -149,17 +418,28 @@ type_check() {
149
418
  fi
150
419
  }
151
420
 
421
+ type_check() {
422
+ run_parallel typecheck_javascript typecheck_rust typecheck_python
423
+ }
424
+
425
+ build_javascript() {
426
+ run_script build
427
+ }
428
+
429
+ build_rust() {
430
+ if [ -f Cargo.toml ]; then cargo_run build --all-targets --all-features; fi
431
+ }
432
+
152
433
  build() {
153
434
  # Some frameworks validate session secrets while statically collecting pages.
154
435
  # Keep CI builds deterministic without weakening runtime/deployment validation.
155
436
  if [ "${CI:-}" = true ] && [ -z "${NEXTAUTH_SECRET:-}" ]; then
156
437
  export NEXTAUTH_SECRET="ci-only-build-secret-not-for-runtime-0123456789"
157
438
  fi
158
- run_script build
159
- if [ -f Cargo.toml ]; then cargo build --all-targets --all-features; fi
439
+ run_parallel build_javascript build_rust
160
440
  }
161
441
 
162
- unit() {
442
+ unit_javascript() {
163
443
  # Bun repositories should expose test scripts backed by Bun's native runner.
164
444
  # Specialized repositories may keep their native runner (for example Matchstick or Hardhat).
165
445
  if has_script test:unit; then
@@ -175,11 +455,17 @@ unit() {
175
455
  else
176
456
  echo "Skipping JavaScript/TypeScript unit tests (script not defined)"
177
457
  fi
458
+ }
459
+
460
+ unit_rust() {
178
461
  if [ -f Cargo.toml ]; then
179
- if has_rust_target lib; then cargo test --lib --all-features
180
- elif has_rust_target bin; then cargo test --bins --all-features
462
+ if has_rust_target lib; then cargo_run test --lib --all-features
463
+ elif has_rust_target bin; then cargo_run test --bins --all-features
181
464
  else echo "Skipping Rust unit tests (no library or binary target)"; fi
182
465
  fi
466
+ }
467
+
468
+ unit_python() {
183
469
  if [ -d tests/unit ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
184
470
  coverage_args=()
185
471
  while IFS= read -r arg; do [ -n "$arg" ] && coverage_args+=("$arg"); done < <(python_coverage_args)
@@ -195,13 +481,23 @@ unit() {
195
481
  fi
196
482
  }
197
483
 
198
- integration() {
484
+ unit() {
485
+ run_parallel unit_javascript unit_rust unit_python
486
+ }
487
+
488
+ integration_javascript() {
199
489
  if has_script test:integration; then
200
490
  run_script test:integration
201
491
  else
202
492
  echo "Skipping JavaScript/TypeScript integration tests (script not defined)"
203
493
  fi
204
- if [ -f Cargo.toml ] && [ -d tests ]; then cargo test --tests --all-features; fi
494
+ }
495
+
496
+ integration_rust() {
497
+ if [ -f Cargo.toml ] && [ -d tests ]; then cargo_run test --tests --all-features; fi
498
+ }
499
+
500
+ integration_python() {
205
501
  if [ -d tests/integration ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
206
502
  python -m pytest -q tests/integration
207
503
  else
@@ -209,7 +505,11 @@ integration() {
209
505
  fi
210
506
  }
211
507
 
212
- e2e() {
508
+ integration() {
509
+ run_parallel integration_javascript integration_rust integration_python
510
+ }
511
+
512
+ e2e_javascript() {
213
513
  if has_script test:e2e; then
214
514
  run_script test:e2e
215
515
  elif has_script e2e; then
@@ -217,6 +517,9 @@ e2e() {
217
517
  else
218
518
  echo "Skipping JavaScript/TypeScript E2E tests (script not defined)"
219
519
  fi
520
+ }
521
+
522
+ e2e_python() {
220
523
  if [ -d tests/e2e ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
221
524
  python -m pytest -q tests/e2e
222
525
  else
@@ -224,7 +527,11 @@ e2e() {
224
527
  fi
225
528
  }
226
529
 
227
- smoke() {
530
+ e2e() {
531
+ run_parallel e2e_javascript e2e_python
532
+ }
533
+
534
+ smoke_javascript() {
228
535
  if has_script test:smoke; then
229
536
  run_script test:smoke
230
537
  elif has_script smoke; then
@@ -232,6 +539,9 @@ smoke() {
232
539
  else
233
540
  echo "Skipping JavaScript/TypeScript smoke tests (script not defined)"
234
541
  fi
542
+ }
543
+
544
+ smoke_python() {
235
545
  if [ -d tests/smoke ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
236
546
  python -m pytest -q tests/smoke
237
547
  else
@@ -239,7 +549,90 @@ smoke() {
239
549
  fi
240
550
  }
241
551
 
552
+ smoke() {
553
+ run_parallel smoke_javascript smoke_python
554
+ }
555
+
556
+ should_run() {
557
+ local task="$1"
558
+ if repo_foundry_governance_only; then
559
+ printf '%s\n' 'applicable=false'
560
+ [ "$task" = e2e ] && printf '%s\n' 'browser=false'
561
+ return 0
562
+ fi
563
+ case "$task" in
564
+ format)
565
+ if has_script format:check || has_javascript || [ -f Cargo.toml ] || has_python; then
566
+ printf '%s\n' 'applicable=true'
567
+ else
568
+ printf '%s\n' 'applicable=false'
569
+ fi
570
+ ;;
571
+ lint)
572
+ if has_script lint || has_javascript || [ -f Cargo.toml ] || has_python; then
573
+ printf '%s\n' 'applicable=true'
574
+ else
575
+ printf '%s\n' 'applicable=false'
576
+ fi
577
+ ;;
578
+ type_check)
579
+ if has_script type-check || has_script typecheck || [ -f Cargo.toml ] || has_python; then
580
+ printf '%s\n' 'applicable=true'
581
+ else
582
+ printf '%s\n' 'applicable=false'
583
+ fi
584
+ ;;
585
+ build)
586
+ if has_script build || [ -f Cargo.toml ]; then
587
+ printf '%s\n' 'applicable=true'
588
+ else
589
+ printf '%s\n' 'applicable=false'
590
+ fi
591
+ ;;
592
+ unit)
593
+ if has_script test:unit || has_script test:coverage || has_script test || [ -f Cargo.toml ] ||
594
+ [ -d tests ] || [ -d test ]; then
595
+ printf '%s\n' 'applicable=true'
596
+ else
597
+ printf '%s\n' 'applicable=false'
598
+ fi
599
+ ;;
600
+ integration)
601
+ if has_script test:integration || [ -f Cargo.toml ] && [ -d tests ] || [ -d tests/integration ]; then
602
+ printf '%s\n' 'applicable=true'
603
+ else
604
+ printf '%s\n' 'applicable=false'
605
+ fi
606
+ ;;
607
+ e2e)
608
+ if has_script test:e2e || has_script e2e || [ -d tests/e2e ]; then
609
+ printf '%s\n' 'applicable=true'
610
+ if has_browser_dependencies; then
611
+ printf '%s\n' 'browser=true'
612
+ else
613
+ printf '%s\n' 'browser=false'
614
+ fi
615
+ else
616
+ printf '%s\n' 'applicable=false'
617
+ printf '%s\n' 'browser=false'
618
+ fi
619
+ ;;
620
+ smoke)
621
+ if has_script test:smoke || has_script smoke || [ -d tests/smoke ]; then
622
+ printf '%s\n' 'applicable=true'
623
+ else
624
+ printf '%s\n' 'applicable=false'
625
+ fi
626
+ ;;
627
+ *)
628
+ echo "unknown CI task: $task" >&2
629
+ return 2
630
+ ;;
631
+ esac
632
+ }
633
+
242
634
  case "${1:-}" in
243
- install|format|lint|type_check|build|unit|integration|e2e|smoke) "$1" ;;
244
- *) echo "usage: $0 {install|format|lint|type_check|build|unit|integration|e2e|smoke}" >&2; exit 2 ;;
635
+ install|format|lint|type_check|build|unit|integration|e2e|smoke|should_run) "$1" "${2:-}" ;;
636
+ task_profile) task_profile "${2:-}" ;;
637
+ *) echo "usage: $0 {install|format|lint|type_check|build|unit|integration|e2e|smoke|should_run|task_profile}" >&2; exit 2 ;;
245
638
  esac
@@ -2,21 +2,87 @@
2
2
  set -euo pipefail
3
3
 
4
4
  languages=()
5
+ changed_files=""
6
+
7
+ # Scheduled and manually dispatched scans are always full scans. For pushes
8
+ # and pull requests, a shallow commit diff lets the matrix skip analyzers for
9
+ # languages untouched by the change while keeping every matrix check visible.
10
+ case "${GITHUB_EVENT_NAME:-}" in
11
+ schedule|workflow_dispatch) ;;
12
+ *)
13
+ base_sha="${CODEQL_BASE_SHA:-}"
14
+ if [ -n "$base_sha" ] && [ "$base_sha" != "0000000000000000000000000000000000000000" ]; then
15
+ if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
16
+ git fetch --no-tags --filter=blob:none --depth=1 origin "$base_sha" >/dev/null 2>&1 || true
17
+ fi
18
+ changed_files="$(git diff --name-only "$base_sha" "${GITHUB_SHA:-HEAD}" 2>/dev/null || true)"
19
+ fi
20
+ [ -n "$changed_files" ] || changed_files="$(git diff --name-only HEAD^ HEAD 2>/dev/null || true)"
21
+ ;;
22
+ esac
23
+
5
24
  if find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print -quit | grep -q .; then languages+=(actions); fi
6
25
  configured=""
7
- if [ -f .github/template.yml ]; then
26
+ if [ -x .github/scripts/profile.sh ]; then
27
+ configured="$(bash .github/scripts/profile.sh get languages 2>/dev/null || true)"
28
+ elif [ -f .github/template.yml ]; then
8
29
  configured="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
9
30
  fi
10
31
 
11
32
  if [ "$configured" = auto ] || [ "$configured" = all ] || [ -z "$configured" ]; then
12
- if git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' 'package.json' 'tsconfig*.json' | grep -q .; then languages+=(javascript-typescript); fi
13
- if git ls-files -- '*.py' 'pyproject.toml' 'requirements*.txt' 'setup.py' ':!.github/**' | grep -q .; then languages+=(python); fi
14
- if git ls-files -- '*.rs' 'Cargo.toml' 'Cargo.lock' | grep -q .; then languages+=(rust); fi
33
+ tracked_files="$(git ls-files)"
34
+ if printf '%s\n' "$tracked_files" | grep -Eq '(^|/)([^/]+\.(ts|tsx|js|jsx)|package\.json|tsconfig[^/]*\.json)$'; then
35
+ languages+=(javascript-typescript)
36
+ fi
37
+ if printf '%s\n' "$tracked_files" | awk '!/^\.github\// && /(^|\/)([^\/]+\.py|pyproject\.toml|requirements[^\/]*\.txt|setup\.py)$/ { found=1; exit } END { exit !found }'; then
38
+ languages+=(python)
39
+ fi
40
+ if printf '%s\n' "$tracked_files" | grep -Eq '(^|/)([^/]+\.rs|Cargo\.toml|Cargo\.lock)$'; then
41
+ languages+=(rust)
42
+ fi
15
43
  else
16
44
  case ",$configured," in *,typescript,*) languages+=(javascript-typescript) ;; esac
17
45
  case ",$configured," in *,python,*) languages+=(python) ;; esac
18
46
  case ",$configured," in *,rust,*) languages+=(rust) ;; esac
19
47
  fi
20
48
 
21
- json=$(printf '%s\n' "${languages[@]}" | jq -Rsc 'split("\n") | map(select(length > 0) | {language: ., name: (if . == "javascript-typescript" then "TypeScript" elif . == "actions" then "Actions" else (. | ascii_upcase[0:1] + .[1:]) end), "build-mode": "none"})')
49
+ changed_json="$(printf '%s\n' "$changed_files" | jq -Rsc 'split("\n") | map(select(length > 0))')"
50
+ json=$(printf '%s\n' "${languages[@]}" | jq -Rsc --argjson changed "$changed_json" '
51
+ split("\n")
52
+ | map(select(length > 0) | {
53
+ language: .,
54
+ name: (if . == "javascript-typescript" then "TypeScript" elif . == "actions" then "Actions" else (. | ascii_upcase[0:1] + .[1:]) end),
55
+ "build-mode": "none",
56
+ changed: (
57
+ if ($changed | length) == 0 then true
58
+ elif . == "actions" then any($changed[]; test("(^|/)(\\.github/workflows/|action\\.yml$)"))
59
+ elif . == "javascript-typescript" then any($changed[]; test("(^|/)(.*\\.(js|jsx|mjs|cjs|ts|tsx|mts|cts)|package\\.json|tsconfig[^/]*\\.json|((bun|pnpm|yarn|package-lock)\\.lock))$"))
60
+ elif . == "python" then any($changed[]; test("(^|/)(.*\\.py|pyproject\\.toml|requirements[^/]*\\.txt|setup\\.py|Pipfile\\.lock|poetry\\.lock)$"))
61
+ elif . == "rust" then any($changed[]; test("(^|/)(.*\\.rs|Cargo\\.toml|Cargo\\.lock)$"))
62
+ else true
63
+ end
64
+ )
65
+ })
66
+ ')
22
67
  printf 'languages=%s\n' "$json" >> "$GITHUB_OUTPUT"
68
+
69
+ # Expose stable, shell-friendly outputs for explicit analyzer jobs. Keeping
70
+ # these separate from the JSON matrix lets workflow-level conditions avoid
71
+ # allocating a runner for languages that are absent or unchanged.
72
+ for codeql_language in actions javascript-typescript python rust; do
73
+ output_prefix="$codeql_language"
74
+ case "$codeql_language" in
75
+ javascript-typescript) output_prefix="javascript" ;;
76
+ esac
77
+
78
+ entry="$(jq -c --arg language "$codeql_language" '.[] | select(.language == $language)' <<< "$json")"
79
+ if [ -n "$entry" ]; then
80
+ printf '%s_available=true\n' "$output_prefix" >> "$GITHUB_OUTPUT"
81
+ printf '%s_changed=%s\n' "$output_prefix" "$(jq -r '.changed' <<< "$entry")" >> "$GITHUB_OUTPUT"
82
+ printf '%s_build_mode=%s\n' "$output_prefix" "$(jq -r '."build-mode"' <<< "$entry")" >> "$GITHUB_OUTPUT"
83
+ else
84
+ printf '%s_available=false\n' "$output_prefix" >> "$GITHUB_OUTPUT"
85
+ printf '%s_changed=false\n' "$output_prefix" >> "$GITHUB_OUTPUT"
86
+ printf '%s_build_mode=none\n' "$output_prefix" >> "$GITHUB_OUTPUT"
87
+ fi
88
+ done