-
Notifications
You must be signed in to change notification settings - Fork 121
391 lines (334 loc) · 15.3 KB
/
Copy pathci.yml
File metadata and controls
391 lines (334 loc) · 15.3 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
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install ruff black nbconvert nbformat jupyter-client ipykernel
- name: Lint
run: |
ruff check .
black --check .
- name: Source package integrity check (Issue #540)
run: python scripts/check_package_integrity.py
- name: Environment contract docs drift check (Issue #544)
run: python scripts/generate_env_contract_docs.py --check
- name: Dead-path detection report (Issue #547)
# Informational — surfaces retirement candidates without failing the
# build. Use `make dead-path-report` locally, or pass --strict to
# gate CI once the initial candidate backlog above has been triaged.
run: python scripts/detect_dead_paths.py
- name: Check verify_chain called after every joblib.load in detection/
run: |
python - <<'EOF'
import re, sys, pathlib
issues = []
for f in pathlib.Path("detection").rglob("*.py"):
lines = f.read_text().splitlines()
for i, line in enumerate(lines):
if re.search(r"joblib\.load\(", line):
# Check the next 5 lines for verify_chain
window = "\n".join(lines[i : i + 6])
if "verify_chain" not in window:
issues.append(f"{f}:{i+1}: joblib.load without nearby verify_chain call")
if issues:
print("verify_chain enforcement failures:")
for issue in issues:
print(" ", issue)
sys.exit(1)
else:
print("verify_chain check passed.")
EOF
- name: Validate Grafana dashboard JSON schema (Issue #242)
run: |
python - <<'EOF'
import json, pathlib, sys
REQUIRED_KEYS = {"schemaVersion", "title", "panels"}
errors = []
for f in pathlib.Path("monitoring/grafana/dashboards").glob("*.json"):
try:
data = json.loads(f.read_text())
except json.JSONDecodeError as e:
errors.append(f"{f}: invalid JSON — {e}")
continue
missing = REQUIRED_KEYS - data.keys()
if missing:
errors.append(f"{f}: missing required keys {missing}")
if not isinstance(data.get("panels"), list):
errors.append(f"{f}: 'panels' must be a list")
if errors:
print("Grafana dashboard validation failures:")
for e in errors:
print(" ", e)
sys.exit(1)
else:
print(f"All Grafana dashboards valid ({len(list(pathlib.Path('monitoring/grafana/dashboards').glob('*.json')))} files).")
EOF
- name: Validate markdown links (Issue #263)
run: |
npm install -g markdown-link-check
markdown-link-check docs/security_threat_model.md
markdown-link-check docs/security.md
markdown-link-check CONTRIBUTING.md
- name: Check import cycles (Issue #546)
run: |
python scripts/check_import_cycles.py
# Exit code 2 = cycles found → fail CI.
# Exit code 1 = fatal error (unreadable file, bad args) → also fail.
- name: Probe optional dependencies (Issue #542)
run: |
python -m utils.dependency_probe --groups ml_core cryptography prometheus
# Probes core required groups; missing optional deps are warnings only.
# Add --require <group> here if a group becomes mandatory.
- name: Validate README bash examples (Issue #548)
run: |
python scripts/validate_readme_examples.py --docs README.md docs/
# Exits 2 if any python -m or python <file> reference is broken.
# make <target> mismatches are always warnings (never block CI).
- name: Validate notebooks — structure (Issue #549)
run: |
python scripts/validate_notebooks.py
# Checks JSON validity, nbformat ≥ 4, kernelspec, empty cells.
# Does NOT enforce output clearing here — that's the --check-outputs flag.
- name: Validate notebooks — outputs cleared (Issue #549)
run: |
python scripts/validate_notebooks.py --check-outputs
# Notebooks must not commit cell outputs (diff bloat / data leakage).
# Add metadata.keep_outputs=true in the notebook to opt out.
- name: Validate environment configuration contracts (config/contracts.py)
# Exercises every runtime mode's contract (config/contracts.py) end-to-end
# against a fully-populated environment, the same way `make check-env`
# does for a developer. Catches contract/entry-point drift — e.g. a
# renamed Config attribute a check still references — that unit tests
# mocking individual attributes could miss.
run: |
openssl genpkey -algorithm ed25519 -out /tmp/ci_signing_key.pem
openssl pkey -in /tmp/ci_signing_key.pem -pubout -out /tmp/ci_jwt_public_key.pem
MODEL_DIR=./models \
RISK_SCORE_DB_URL=sqlite:///:memory: \
WATCHED_ASSET_PAIRS=USDC:GA5ZSEJYBY3RJRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN \
API_KEYS=ci-placeholder-hash \
LEDGERLENS_CONTRACT_ID=ci-placeholder-contract \
LEDGERLENS_SUBMITTER_SECRET=ci-placeholder-secret \
JWT_PUBLIC_KEY_PATH=/tmp/ci_jwt_public_key.pem \
python -m scripts.check_env --all
- name: Execute Benford explainer notebook
run: |
cd notebooks
jupyter nbconvert --to notebook --execute \
--ExecutePreprocessor.timeout=120 \
--inplace benford_explainer.ipynb
- name: Check typed service boundaries (utils/boundaries.py)
run: python scripts/check_service_boundaries.py
- name: Check module dependency rules (config/module_boundaries.yml)
run: python scripts/check_module_dependencies.py
- name: Check public API compatibility (tests/fixtures/api_baseline.json)
run: python scripts/check_api_compatibility.py
- name: Check CLI command contracts (scripts/cli_contracts.py)
run: python scripts/check_cli_contracts.py
- name: Test
run: pytest -q --junitxml=reports/ci/junit_results.xml
continue-on-error: true
- name: Triage build failures (Issue #539)
if: always()
run: |
mkdir -p reports/ci
if [ -f reports/ci/junit_results.xml ]; then
python -m scripts.triage_build_failures \
--input reports/ci/junit_results.xml \
--format junit-xml \
--output-dir reports/triage \
--baseline reports/triage/triage_baseline.json \
--compare || true
fi
- name: Feature compatibility check (Issue #532)
if: always()
run: |
python -m scripts.check_feature_compat \
--model-dir models/ \
--no-report || true
schema-compatibility:
name: Avro Schema Compatibility
runs-on: ubuntu-latest
# A separate job rather than a step in `test`: this needs fetch-depth: 0 to
# read the baseline from git, and the test matrix runs checkout twice
# (3.11 and 3.12). Running it once here avoids doubling the clone cost.
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Fetch the base branch
# On a pull_request the baseline is the target branch; on a push to
# main it is main's previous state, which the full clone already has.
env:
BASE_REF: ${{ github.event.pull_request.base.ref || github.ref_name }}
run: git fetch --no-tags origin "$BASE_REF"
- name: Check Avro schema compatibility
run: make check-schema-compatibility
# -----------------------------------------------------------------------
# Issue #545 — Static analysis gates (mypy + bandit + radon)
# -----------------------------------------------------------------------
- name: Install static analysis tools (issue #545)
run: pip install bandit>=1.7.9 radon>=6.0.1
- name: Static analysis gate — mypy type checking
run: |
python -m mypy \
--config-file pyproject.toml \
--no-error-summary \
detection ingestion streaming ci_metrics benchmarks utils config.py \
|| true
# Non-blocking on first introduction — existing code is not mypy-strict.
# Tracked as follow-up: tighten to exit 1 once all modules are annotated.
- name: Static analysis gate — bandit security linting
run: |
python -m bandit \
-r detection ingestion streaming ci_metrics benchmarks \
--severity-level high \
--confidence-level medium \
--quiet \
|| true
# Non-blocking on first introduction.
- name: Static analysis gate — radon cyclomatic complexity
run: |
python scripts/static_analysis_gate.py \
--skip-mypy \
--skip-bandit \
--complexity-max 10 \
--targets detection ingestion streaming ci_metrics benchmarks
# -----------------------------------------------------------------------
# Issue #541 — Dependency lockfile verification
# -----------------------------------------------------------------------
- name: Verify dependency lockfile (issue #541)
run: |
python scripts/verify_lockfile.py --check-unpinned
# -----------------------------------------------------------------------
# Issue #537 — Benchmark dataset integrity check
# -----------------------------------------------------------------------
- name: Validate benchmark datasets (issue #537)
run: |
python - <<'EOF'
import json, sys
from benchmarks.datasets import BenchmarkRegistry
registry = BenchmarkRegistry()
manifest = registry.manifest()
errors = []
for entry in manifest:
name = entry["name"]
ds = registry.get(name)
if not len(ds.trades) > 0:
errors.append(f"{name}: zero trades")
if ds.labels.dtype != bool:
errors.append(f"{name}: labels dtype is {ds.labels.dtype}")
# Verify checksum is stable
c1 = registry.checksum(name)
c2 = registry.checksum(name)
if c1 != c2:
errors.append(f"{name}: checksum not deterministic")
if errors:
print("Benchmark dataset validation failures:")
for e in errors:
print(" ", e)
sys.exit(1)
print(json.dumps(manifest, indent=2, default=str))
print(f"\n{len(manifest)} benchmark datasets validated.", file=sys.stderr)
EOF
# -----------------------------------------------------------------------
# Issue #538 — CI metrics regression monitoring self-test
# -----------------------------------------------------------------------
- name: CI metrics regression monitoring self-test (issue #538)
run: |
python - <<'EOF'
from ci_metrics import CIRunRecord, MetricSnapshot, record_run
from ci_metrics.store import MetricsStore
from pathlib import Path
import tempfile, os
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "test_history.jsonl"
# Simulate 5 runs with stable metrics
for i in range(5):
record = CIRunRecord(
run_id=f"ci-selftest-{i}",
commit_sha="abc123",
branch="ci-selftest",
timestamp_utc="2024-01-01T00:00:00Z",
metrics=[
MetricSnapshot(name="test_pass_rate", value=1.0),
MetricSnapshot(name="runtime_s", value=10.0, higher_is_better=False),
],
)
record_run(record, store_path=store_path)
store = MetricsStore(store_path)
assert len(store) == 5, f"Expected 5 records, got {len(store)}"
# Verify no regression on stable series
from ci_metrics.regression import RegressionDetector
detector = RegressionDetector(store)
latest = CIRunRecord(
run_id="ci-selftest-latest",
commit_sha="def456",
branch="ci-selftest",
timestamp_utc="2024-01-02T00:00:00Z",
metrics=[
MetricSnapshot(name="test_pass_rate", value=1.0),
],
)
store.append(latest)
alerts = detector.check(latest)
assert not any(a.severity == "critical" for a in alerts), \
f"Unexpected critical alert on stable metrics: {alerts}"
print("CI metrics self-test passed.")
EOF
mutation-test:
name: Mutation Testing (≥80% score)
runs-on: ubuntu-latest
# Run mutation testing only on Python 3.11 to keep CI time under 15 minutes.
# It runs in parallel with the test matrix so it does not block PR feedback.
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run mutation tests on core scoring path
# --paths-to-mutate limits scope to the three core modules so the job
# completes in < 15 minutes. Mutmut restores every mutant after testing,
# so no mutated code is ever persisted to disk or the workspace.
run: |
mutmut run \
--paths-to-mutate "detection/benford_engine.py,detection/feature_engineering.py,detection/model_inference.py" \
--runner "python -m pytest -x -q --timeout=30 -m 'not integration and not slow' \
tests/test_benford.py \
tests/test_benford_ci.py \
tests/test_feature_engineering.py \
tests/test_model_inference.py" \
--no-progress || true
- name: Print mutation results summary
run: mutmut results || true
- name: Enforce ≥80% mutation score threshold
# Exits 1 (failing the CI step) if fewer than 80% of mutations are killed.
# Prints surviving mutations so they can be opened as follow-up issues.
run: python scripts/check_mutation_score.py --threshold 80