-
Notifications
You must be signed in to change notification settings - Fork 1
798 lines (704 loc) · 35.1 KB
/
Copy pathci.yml
File metadata and controls
798 lines (704 loc) · 35.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Weekly Sunday midnight UTC — catches newly-published CVEs without a push
- cron: "0 0 * * 0"
workflow_dispatch:
# Global minimum permissions; individual jobs override where needed
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
env:
UV_FROZEN: "true"
MPLBACKEND: Agg
# ─────────────────────────────────────────────────────────────────────────────
# Job 1 — Lint & Type Check
# ─────────────────────────────────────────────────────────────────────────────
jobs:
# Presence-detection for OPTIONAL/rotating projects. `hashFiles()` is NOT
# available in a job-level `if:` (only in step contexts) — using it there is
# a workflow-validation error that makes GitHub reject the ENTIRE workflow
# at startup (zero jobs run, surfaced as "workflow file issue"). This tiny
# job computes the existence flags in a step (valid) and exposes them as
# outputs that the optional jobs gate on via `needs.detect.outputs.*`.
detect:
name: Detect optional projects
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
setup_hook: ${{ steps.d.outputs.setup_hook }}
fep_lean: ${{ steps.d.outputs.fep_lean }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- id: d
shell: bash
run: |
shopt -s globstar nullglob
hooks=(projects/**/scripts/setup_hook.py)
if [ ${#hooks[@]} -gt 0 ]; then
echo "setup_hook=true" >> "$GITHUB_OUTPUT"
else
echo "setup_hook=false" >> "$GITHUB_OUTPUT"
fi
if [ -f projects/fep_lean/lean/lean-toolchain ]; then
echo "fep_lean=true" >> "$GITHUB_OUTPUT"
else
echo "fep_lean=false" >> "$GITHUB_OUTPUT"
fi
# Derive the public-exemplar matrix for `test-project` from the canonical
# roster (`infrastructure.project.public_scope`) instead of a hard-coded list,
# so adding/retiring an exemplar under projects/templates/ extends or trims the
# matrix automatically (CI-MATRIX-DYNAMIC-1). The `project-names-json` command
# emits a one-line JSON array which `test-project` consumes via
# `fromJSON(needs.detect-projects.outputs.projects)`. No network calls — pure
# local filesystem discovery.
detect-projects:
name: Detect public exemplars
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
projects: ${{ steps.matrix.outputs.projects }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Emit exemplar matrix
id: matrix
run: |
set -euo pipefail
projects="$(uv run python -m infrastructure.project.public_scope project-names-json)"
echo "projects=${projects}" >> "$GITHUB_OUTPUT"
# ───────────────────────────────────────────────────────────────────────────
# Job 0b — Actionlint (workflow syntax gate; independent of project setup)
#
# GH-ACTIONLINT-1: catch workflow-expression and `uses:` errors early, before
# a small mistake silently disables CI coverage or fails every PR. Read-only,
# no `uv sync`, no project dependencies, no `needs:` — it runs in parallel with
# everything else. The actionlint binary is fetched with its official installer
# script pinned to a release commit SHA (immutable); the runner's preinstalled
# shellcheck is picked up automatically to also lint embedded `run:` scripts.
# ───────────────────────────────────────────────────────────────────────────
actionlint:
name: Actionlint
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Run actionlint
env:
# rhysd/actionlint @ v1.7.12 — bump REF and VERSION together on upgrade
ACTIONLINT_REF: 914e7df21a07ef503a81201c76d2b11c789d3fca
ACTIONLINT_VERSION: "1.7.12"
run: |
set -euo pipefail
curl -fsSL \
"https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_REF}/scripts/download-actionlint.bash" \
-o "${RUNNER_TEMP}/download-actionlint.bash"
bash "${RUNNER_TEMP}/download-actionlint.bash" "${ACTIONLINT_VERSION}" "${RUNNER_TEMP}"
"${RUNNER_TEMP}/actionlint" -color
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Resolve public CI source paths
id: public-scope
run: echo "paths=$(uv run python -m infrastructure.project.public_scope source-paths)" >> "$GITHUB_OUTPUT"
- name: Ruff lint
run: uvx ruff check ${{ steps.public-scope.outputs.paths }}
- name: Ruff format check
run: uvx ruff format --check ${{ steps.public-scope.outputs.paths }}
- name: Type checking
run: uv run mypy ${{ steps.public-scope.outputs.paths }}
# MED5 gate: every re-exporting module under infrastructure/ must
# declare __all__ — see docs/rules/api_design.md. Prevents regression
# of the [attr-defined] mypy class of bug.
- name: Audit __all__ on re-exporting modules
run: uv run python -m infrastructure.skills check-all-exports
- name: Reject tracked generated artifacts
run: uv run python scripts/check_tracked_generated_artifacts.py
- name: Confidentiality guard — only template projects tracked
run: uv run python scripts/check_tracked_projects.py
# Enforce the module-size composability budget as a hard gate (fails at
# >=950 infra / >=250 project-script lines). It also runs inside the
# informational health job below, but that job is wrapped in ``|| true``
# and never blocks — so without this step an oversized module could land
# on main unnoticed. Advisory warnings (>=800) still only warn.
- name: Module line-count gate — composability budget
run: uv run python scripts/gates/module_line_count_check.py
# ─────────────────────────────────────────────────────────────────────────────
# Job 1b — Unified health report (informational, no fail)
#
# MED2: ``infrastructure.core.health`` aggregates every quality gate into a
# single typed ``HealthReport``. The job runs after ``lint`` and uploads the
# JSON for triage; ``|| true`` keeps the workflow green even if individual
# gates regress here (their dedicated jobs above are the blocking ones).
# ─────────────────────────────────────────────────────────────────────────────
health:
name: Unified Health Report (informational)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [lint]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Run unified health checks (informational)
run: |
uv run python -m infrastructure.core.health --json --quiet \
> health-report.json || true
- name: Upload health report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: health-report
path: health-report.json
if-no-files-found: warn
# ─────────────────────────────────────────────────────────────────────────────
# Job 2 — No-Mocks Policy Verification
# ─────────────────────────────────────────────────────────────────────────────
verify-no-mocks:
name: Verify No Mocks Policy
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [lint]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Verify no mock usage in tests
run: uv run python scripts/verify_no_mocks.py
# ─────────────────────────────────────────────────────────────────────────────
# Job 2b — Setup hook on Windows (conditional smoke)
#
# When any active project ships ``projects/**/scripts/setup_hook.py``, verify
# hook discovery and subprocess paths on ``windows-latest`` (``.sh`` hooks are
# intentionally skipped there — see TO-DO.md / infrastructure.project.setup_hook).
# ─────────────────────────────────────────────────────────────────────────────
setup-hook-windows-smoke:
name: Setup hook (Windows smoke)
runs-on: windows-latest
timeout-minutes: 15
needs: [verify-no-mocks, detect]
if: needs.detect.outputs.setup_hook == 'true'
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Run setup_hook tests (Windows)
env:
PYTHONUTF8: "1"
run: >-
uv run pytest tests/infra_tests/project/test_setup_hook.py -v
--timeout=120
# ─────────────────────────────────────────────────────────────────────────────
# Job 3 — Infrastructure Tests (matrix: ubuntu + macos × Python 3.10–3.12)
# ─────────────────────────────────────────────────────────────────────────────
test-infra:
name: "Infra Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})"
runs-on: ${{ matrix.os }}
timeout-minutes: 30
needs: [verify-no-mocks]
permissions:
contents: read
strategy:
fail-fast: false
matrix:
# ubuntu covers all three Python versions; macOS runs only the 3.12
# smoke (macOS legs are ~10x cost and rarely surface OS-specific project
# breakage beyond what the 3.12 cell catches).
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12"]
include:
- os: macos-latest
python-version: "3.12"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies (dev + optional test groups)
run: uv sync --group rendering --group monitoring
- name: Fix macOS socket.getfqdn timeout
if: runner.os == 'macOS'
run: sudo scutil --set HostName "$(hostname)"
- name: Install pandoc
uses: pandoc/actions/setup@86321b6dd4675f5014c611e05088e10d4939e09e # v1.1.1
# TeX Live so the xelatex/bibtex-gated rendering tests run instead of
# skipping (they assert a real PDF lands on disk). Linux lanes only:
# MacTeX is a multi-GB install and macOS breadth is a 3.12 smoke lane.
- name: Install TeX Live (xelatex + bibtex)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
texlive-xetex texlive-latex-extra texlive-bibtex-extra \
texlive-fonts-recommended lmodern
- name: Run infrastructure tests
env:
COVERAGE_FILE: .coverage.infra
# macOS runners have socket.getfqdn timeout issues with werkzeug/httpserver
continue-on-error: ${{ runner.os == 'macOS' }}
# -n auto parallelizes across runner cores (pytest-xdist); the suite is
# parallel-safe (no-mocks tests use per-test tmp_path + random-port
# httpserver). pytest-cov combines per-worker data before the gate.
run: >-
uv run pytest tests/infra_tests/
-n auto
--cov=infrastructure
--cov-report=term-missing
--cov-report=xml:coverage-infra.xml
--cov-fail-under=60
--durations=10
-m "not requires_ollama and not slow and not bench"
--timeout=120
- name: Upload infrastructure coverage to Codecov
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: coverage-infra.xml
flags: infrastructure
name: infra-coverage
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# ─────────────────────────────────────────────────────────────────────────────
# Job 4 — Project Tests (one parallel job per public exemplar)
# ─────────────────────────────────────────────────────────────────────────────
test-project:
name: "Project Tests (${{ matrix.project }}, py${{ matrix.python-version }})"
runs-on: ubuntu-latest
# Each public exemplar runs in its OWN parallel job, so wall-clock is the
# slowest single project (~active_inference) instead of the ~45 min sequential
# sum of all nine. Each job enforces that project's own 90% floor via
# ``scripts/01_run_tests.py --project`` (authoritative per CLAUDE.md), which
# also removes the old code_project/fep_lean conftest plugin-name collision
# (every project is already isolated in its own job). py3.10 (floor) + py3.12
# (ceiling) give cross-version coverage; macOS breadth is handled by test-infra.
# 60 min backstop — active_inference on py3.10 has exceeded 45 min on loaded runners.
timeout-minutes: 60
needs: [verify-no-mocks, detect-projects]
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12"]
# Public-exemplar list is derived at runtime from the canonical roster
# (infrastructure.project.public_scope) by the detect-projects job, so
# adding/retiring an exemplar under projects/templates/ extends or trims
# this matrix without editing any literal here (CI-MATRIX-DYNAMIC-1).
project: ${{ fromJSON(needs.detect-projects.outputs.projects) }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies (dev + optional test groups)
run: uv sync --group rendering --group monitoring --group discopy
# 01_run_tests.py --project runs pytest with cwd=<project> and enforces that
# project's own --cov-fail-under=90 internally, writing coverage into the
# project dir (projects/<name>/.coverage.project + coverage_project.json).
# The gate is therefore authoritative here; no repo-root coverage step.
- name: Run project tests
env:
PYTHONPATH: .
run: |
set -euo pipefail
uv run python scripts/01_run_tests.py \
--project "${{ matrix.project }}" --project-only --include-slow
- name: Upload project coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: projects/${{ matrix.project }}/coverage_project.json
flags: projects
name: project-coverage
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# ─────────────────────────────────────────────────────────────────────────────
# Job 4b — fep_lean (real math-inc gauss + elan lake/lean; excluded from matrix above)
# This job uses projects/fep_lean/ paths. When fep_lean is not checked out
# (it lives in the private repo's working/ pool), the detect job emits
# fep_lean=false and this job is skipped. Check it out flat under projects/ to
# activate CI:
# cp -r <private-projects>/working/fep_lean projects/fep_lean
# ─────────────────────────────────────────────────────────────────────────────
fep-lean:
name: fep_lean (gauss + lake)
if: needs.detect.outputs.fep_lean == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
needs: [verify-no-mocks, detect]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies (dev + optional test groups)
run: uv sync --group rendering --group monitoring
- name: Sync fep_lean project (explicit venv + dev extras)
run: uv sync --directory projects/fep_lean --extra dev
- name: Install elan (lean + lake)
run: |
set -euxo pipefail
curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y
echo "$HOME/.elan/bin" >> "$GITHUB_PATH"
- name: Pin Lean toolchain and warm Lake build
working-directory: projects/fep_lean/lean
run: |
set -euxo pipefail
TOOLCHAIN=$(tr -d '[:space:]' < lean-toolchain)
elan override set "$TOOLCHAIN"
lean --version
lake --version
lake build
- name: Reject sorry in Lean sketches
working-directory: projects/fep_lean/lean/FepSketches
run: |
set -euo pipefail
SORRY_LINES=$(grep -n 'sorry' fep_all.lean Basic.lean \
| grep -Ev ':[[:space:]]*--' || true)
if [ -n "$SORRY_LINES" ]; then
echo "ERROR: 'sorry' found in non-comment Lean lines:"
echo "$SORRY_LINES"
exit 1
fi
echo "No sorry in Lean sketches."
- name: Install Open Gauss CLI
timeout-minutes: 25
env:
OPEN_GAUSS_AUTO_ATTACH: "0"
run: |
set -euxo pipefail
export PATH="$HOME/.local/bin:$PATH"
git clone --depth 1 https://github.com/math-inc/OpenGauss.git "$RUNNER_TEMP/OpenGauss"
cd "$RUNNER_TEMP/OpenGauss"
# Pin to the reviewed HEAD SHA; update this value intentionally when upgrading.
EXPECTED_SHA="f87633900ae185b8037bf451a914fe7eeae1eb08"
ACTUAL_SHA="$(git rev-parse HEAD)"
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "ERROR: OpenGauss HEAD SHA mismatch."
echo " Expected: $EXPECTED_SHA"
echo " Got: $ACTUAL_SHA"
echo "Update EXPECTED_SHA in .github/workflows/ci.yml after reviewing the diff."
exit 1
fi
./scripts/install.sh --plain --noninteractive --skip-system-packages
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
command -v gauss
gauss doctor
- name: Run fep_lean tests (real gauss doctor + lake build)
working-directory: projects/fep_lean
env:
COVERAGE_FILE: ../../.coverage.fep_lean
run: >-
uv run pytest tests/
--timeout=1200
--cov=src
--cov-report=term-missing
--cov-fail-under=89
--durations=10
-m "not requires_ollama"
# ─────────────────────────────────────────────────────────────────────────────
# Job 5 — Validate Manuscripts
# ─────────────────────────────────────────────────────────────────────────────
validate:
name: Validate Manuscripts
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [lint]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Resolve public project names
id: project-scope
run: echo "names=$(uv run python -m infrastructure.project.public_scope project-names)" >> "$GITHUB_OUTPUT"
- name: Validate manuscript markdown
# The `markdown` subcommand takes ONE directory; project globs
# shell-globs to multiple dirs (CLI then errors "unrecognized
# arguments"). Validate each public project's manuscript in turn.
run: |
set -euo pipefail
for project in ${{ steps.project-scope.outputs.names }}; do
d="projects/$project/manuscript"
[ -d "$d" ] || continue
echo "::group::validate $d"
uv run python -m infrastructure.validation.cli markdown "$d"
echo "::endgroup::"
done
- name: Verify api-reference.md is in sync with __all__
# Auto-generated from each `infrastructure/<pkg>/__init__.py` `__all__`
# by `scripts/generate_api_reference_doc.py`. Drift fails the build;
# regenerate locally with `--write` and commit the result.
run: uv run python scripts/generate_api_reference_doc.py --check
- name: Verify exemplar_roster.md is in sync with the live roster
# Auto-generated from the public exemplar roster by
# `scripts/generate_exemplar_roster_doc.py`. Adding a test file to an
# exemplar changes its row; without this gate the committed roster
# drifted silently and was only caught by the full infra pytest run.
# Drift fails the build; regenerate locally and commit the result.
run: uv run python scripts/generate_exemplar_roster_doc.py --check
- name: Verify COUNTS.md is in sync with the live tree
# Auto-generated from live repo state by `scripts/generate_counts.py`
# (tracked infrastructure .py count, project/publishing test-collection
# totals, exemplar roster, module list). Replaces the formerly
# hand-maintained COUNTS.md whose drift was chased across ~40
# commits. Drift fails the build; regenerate locally with `--write` and
# commit the result.
run: uv run python scripts/generate_counts.py --check
- name: Verify project imports
env:
PUBLIC_PROJECTS: ${{ steps.project-scope.outputs.names }}
# Public project names are now QUALIFIED (``templates/<name>``) and the
# projects/ tree is not a Python package chain (no projects/__init__.py),
# so a dotted ``projects.<name>.src`` import cannot resolve. Import each
# project's ``src`` package in an ISOLATED subprocess with the project's
# own sys.path (repo root for ``infrastructure``, project dir, and its
# ``src``) — the same way pytest's pythonpath is configured — which also
# avoids the top-level ``src`` name collision across projects.
run: |
uv run python -c "
import os
import subprocess
import sys
repo = os.getcwd()
projects = os.environ['PUBLIC_PROJECTS'].split()
if not projects:
print('No projects found — skipping import check')
sys.exit(0)
failed = False
for project in projects:
proj_dir = os.path.join(repo, 'projects', *project.split('/'))
env = dict(os.environ)
env['PYTHONPATH'] = os.pathsep.join([repo, proj_dir, os.path.join(proj_dir, 'src')])
result = subprocess.run([sys.executable, '-c', 'import src'], env=env, capture_output=True, text=True)
if result.returncode == 0:
print(f' OK: {project} (src)')
else:
tail = (result.stderr.strip().splitlines() or [str(result.returncode)])[-1]
print(f' FAIL: {project}: {tail}')
failed = True
if failed:
sys.exit(1)
print(f'All {len(projects)} project(s) imported successfully')
"
# ─────────────────────────────────────────────────────────────────────────────
# Job 6 — Security Scan
# ─────────────────────────────────────────────────────────────────────────────
security:
name: Security Scan
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [lint]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Dependency audit (pip-audit)
run: |
set -euo pipefail
IGNORE_FILE=".github/pip-audit-ignore.txt"
PIP_AUDIT_ARGS=()
while IFS= read -r raw || [ -n "$raw" ]; do
[[ "$raw" =~ ^[[:space:]]*# ]] && continue
line="${raw%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
PIP_AUDIT_ARGS+=(--ignore-vuln "$line")
done < "$IGNORE_FILE"
for attempt in 1 2 3; do
if uv run pip-audit "${PIP_AUDIT_ARGS[@]}"; then
exit 0
fi
echo "pip-audit attempt ${attempt} failed; retrying in 15s..." >&2
sleep 15
done
exit 1
- name: Code security scan (Bandit MEDIUM+ severity)
run: >-
uv run bandit -c bandit.yaml -r -ll
infrastructure/
scripts/
projects/
# ─────────────────────────────────────────────────────────────────────────────
# Job 6b — Documentation Lint (mermaid + cross-links + consistency + doc pairs)
#
# Rules enforced:
# 1. Every fenced ```mermaid block in long-lived docs must render with the
# real `mmdc` (mermaid-cli) binary backed by chrome-headless-shell.
# 2. Every relative Markdown link must resolve on disk (skipping fenced and
# inline-code spans).
# 3. ``N Python (sub)packages`` claims in long-lived docs must match the
# live count under ``infrastructure/``; rotating project names must not
# appear unconditionally hard-coded outside ``docs/_generated/``.
# 4. Permanent-template content folders must carry paired ``AGENTS.md`` and
# ``README.md`` files.
#
# This job intentionally fails LOUDLY when mmdc / chrome-headless-shell is
# missing instead of skipping — see ``infrastructure/validation/docs/mermaid_lint.py``.
# ─────────────────────────────────────────────────────────────────────────────
docs-lint:
name: Documentation Lint
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [lint]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
- name: Sync Python dependencies
run: uv sync
- name: Install mermaid-cli
run: npm install -g @mermaid-js/mermaid-cli
- name: Install chrome-headless-shell for puppeteer
run: npx --yes puppeteer browsers install chrome-headless-shell
- name: Locate chrome-headless-shell binary
id: chrome
run: |
set -euo pipefail
CHROME_PATH=$(find "$HOME/.cache/puppeteer/chrome-headless-shell" -type f -name 'chrome-headless-shell' | head -n1)
if [ -z "$CHROME_PATH" ]; then
echo "ERROR: chrome-headless-shell binary not found"
exit 1
fi
echo "CHROME_EXECUTABLE_PATH=$CHROME_PATH" >> "$GITHUB_ENV"
echo "Resolved chrome at: $CHROME_PATH"
- name: Run docs linters (mermaid + cross-links + consistency + doc pairs)
run: uv run python scripts/lint_docs.py --quiet
- name: Check template exemplar drift (strict — dead links gate)
run: uv run python scripts/check_template_drift.py --strict
# ─────────────────────────────────────────────────────────────────────────────
# Job 7 — Performance Check
# ─────────────────────────────────────────────────────────────────────────────
performance:
name: Performance Check
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [test-infra, test-project]
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/setup-python-env
- name: Sync dependencies
run: uv sync
- name: Run import benchmarks
run: |
uv run python -c "
import time
import sys
import importlib
from pathlib import Path
from infrastructure.project.public_scope import public_project_names
THRESHOLD_SECONDS = 5.0
results = {}
# Infrastructure core
t0 = time.perf_counter()
try:
import infrastructure.core
results['infrastructure.core'] = time.perf_counter() - t0
except ImportError as e:
print(f'SKIP infrastructure.core: {e}')
# Public project scope only; runtime discovery remains broader.
# project names contain '/' (e.g. 'templates/template_code_project'),
# which makes them invalid Python module identifiers. Use a subprocess
# with PYTHONPATH set to the project dir so 'import src' resolves
# correctly and wall-clock time is actually recorded.
import subprocess
import os
projects = public_project_names(Path('.'))
for project in projects:
project_dir = str(Path('.') / 'projects' / project)
label = f'projects/{project}/src'
env = {**os.environ, 'PYTHONPATH': project_dir}
t0 = time.perf_counter()
proc = subprocess.run(
[sys.executable, '-c', 'import src'],
env=env, capture_output=True,
)
elapsed = time.perf_counter() - t0
if proc.returncode == 0:
results[label] = elapsed
else:
print(f'SKIP {label}: {proc.stderr.decode().strip()}')
total = sum(results.values())
print('Import timing:')
for name, elapsed in results.items():
print(f' {elapsed:.3f}s {name}')
print(f'Total: {total:.3f}s (threshold: {THRESHOLD_SECONDS}s)')
if total > THRESHOLD_SECONDS:
print('ERROR: Import time exceeds threshold')
sys.exit(1)
print('Performance check passed')
"
# Informational microbench harness for setup_hook + analysis_pipeline (MED6).
# `|| true` keeps this strictly informational — a slow bench will never
# fail the build. Results are uploaded as a CI artifact for trend analysis.
- name: Run setup_hook + analysis_pipeline microbenches (informational)
run: |
uv run pytest tests/infra_tests/benchmark/ -m bench --benchmark-only \
--benchmark-min-rounds=3 --benchmark-json=bench-results.json \
--timeout=180 || true
- name: Upload microbench results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: bench-results
path: bench-results.json
if-no-files-found: warn
# MED3 — coverage trend dashboard. Pulls last 30 days of CI artefacts
# via `gh run download` and renders ``docs/_generated/coverage_history.md``.
# Strictly informational (`|| true`); the result is uploaded as the
# ``coverage-history`` artefact for trend review.
- name: Generate coverage history dashboard (informational)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
uv run python scripts/generate_coverage_history.py --from-gh --days=30 || true
- name: Upload coverage history dashboard
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-history
path: docs/_generated/coverage_history.md
if-no-files-found: warn