Repository navigation
548 lines (490 loc) · 28.5 KB
/
Copy pathci.yml
File metadata and controls
548 lines (490 loc) · 28.5 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
name: CI
on:
# development is the integration branch (contributor PRs land there) and main the released code:
# both run the full CI, and both require its checks before a merge.
push:
branches: [main, development]
pull_request:
branches: [main, development]
# Coverage gate (--cov-fail-under=40) is enforced via pyproject.toml's
# [tool.pytest.ini_options].addopts; the test job intentionally does not
# duplicate the flag here. FORGELM_OPERATOR is set so future operator-identity
# checks (Phase 3 hardening) do not raise on distroless runners where USER is
# unset.
env:
FORGELM_OPERATOR: ci-smoke
# No workflow-level permissions; each job declares the minimum it needs
# (checkout-only read access), per Sonar githubactions:S8233 / S8264 —
# matches the pattern in nightly.yml / publish.yml / site-deploy.yml.
jobs:
# --- Job 1: Lint (fast, no heavy deps) ---
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install linter
# mypy joins ruff here rather than in the test job because the
# public-surface gate needs no runtime dependency: --follow-imports=silent
# silences errors from followed modules, so `forgelm/__init__.py` and
# `forgelm/_version.py` type-check with mypy alone (verified against a
# venv containing nothing else). Keeping it in the fast job means the
# gate reports in seconds instead of behind a 4-way matrix install.
run: pip install 'ruff>=0.4.0,<0.16.0' 'mypy>=1.8.0,<2.0.0'
- name: Ruff lint
run: ruff check .
- name: Ruff format check
run: ruff format --check .
# Wave 2a Round-2 F-XPR-02-03: project-wide HTTP discipline gate
# (per docs/standards/architecture.md "HTTP discipline"). Every
# outbound HTTP call must go through forgelm/_http.py (safe_post /
# safe_get). This grep guard fails CI if any new module reaches
# for requests / urllib / httpx directly.
# Phase 16: every Pydantic field in forgelm/config.py must carry a
# description= argument so the hand-maintained configuration reference
# (docs/reference/configuration.md + -tr.md mirror) always has
# authoritative field text to mirror. Strict mode exits 1 on any
# undocumented field — a new contributor adding a field forgets the
# description here, not silently drifting from the operator-facing docs.
- name: Pydantic description= guard
run: python tools/check_field_descriptions.py --strict forgelm/config.py
# F-P8-C-19: promoted from an inline grep (which missed
# requests.Session()/aliased-import/whitespace-before-paren forms
# and had no own test) to a tested tool with its own
# tests/test_check_http_discipline.py.
- name: HTTP discipline guard
run: python tools/check_http_discipline.py
# Phase 16 S2 follow-up: mutation testing is how this project proves a
# guard is real (disable it, confirm a test reddens, restore). The
# restore step failed once — a review agent's `if False:` was swept into
# a commit by `git add -A` from a shared working tree and shipped at
# HEAD, disabling the benchmark fail-closed gate, with 4,644 tests and
# 29 guards green. ruff does not flag `if False:`; it is legal Python.
- name: Mutation-scaffolding guard
run: python3 tools/check_no_mutation_artifacts.py --strict
# Phase 16 S2 follow-up: converting four writers did not close the class.
# The step's own review found compliance_report.json — an EU AI Act
# Art. 11 / Annex IV artefact — still emitting bare NaN from the same
# metrics dict, plus safety_results.json and safety_trend.jsonl. This
# names the modules that write measurement-bearing artefacts and
# requires dumps_strict in each, the way check_http_discipline.py does
# for outbound HTTP.
- name: Strict-JSON writer discipline
run: python3 tools/check_strict_json_writers.py --strict
# Phase 16 S1 (F-W20260729-OPS-23): docs/design/library_api.md has
# described this as "a new CI step" since Phase 36. It never existed,
# mypy was in no dependency group, and when the documented command was
# finally run it failed — forgelm.__getattr__ had no return annotation,
# so the "100% typed stable surface" claim was unchecked for three minor
# releases. Scoped to the two public-surface modules deliberately:
# --strict over trainer.py would mean typing the whole transformers/trl
# surface (library_api.md Q3). The package's broader internal typing
# debt is a recorded budget, not a blocker for this gate.
#
# tests/typing/public_surface_probe.py is what makes the gate see the
# surface at all: --follow-imports=silent discards diagnostics inside the
# defining modules and __init__.py calls nothing, so before the probe
# existed you could strip the annotations off load_config or
# verify_integrity and the gate still reported Success. The probe calls
# every forgelm.__all__ callable from an annotated context, so --strict's
# --disallow-untyped-calls fires at the call site. It is never executed.
- name: Public-surface type gate (mypy --strict)
run: mypy --strict --follow-imports=silent forgelm/__init__.py forgelm/_version.py tests/typing/public_surface_probe.py
# --- Job 2: Test (matrix across Python versions) ---
test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package (dev)
# `ingestion-scale` (datasketch) is included so
# tests/test_data_audit_phase12.py::TestMinHashLshDedup — which
# `skipif`s itself when the real library is absent — actually runs
# the genuine MinHash/MinHashLSH backend on every push/PR across
# all four Python versions, not just the pure-Python stub coverage
# in TestMinHashLshBackendExecutedWithStub. datasketch is a light
# numpy/scipy-based dep, not one of the heavy extras (bitsandbytes,
# unsloth, deepspeed, lm-eval, wandb, mergekit) that stay off the
# default test matrix.
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev,ingestion-scale]"
- name: Run tests with coverage
# --cov, --cov-report=term-missing, and --cov-fail-under=40 come from
# pyproject.toml addopts (single source of truth). Only the XML report
# is added here because it is CI-specific (artifact upload).
run: pytest -q --tb=short --cov-report=xml:coverage.xml
- name: Upload coverage (3.11 only)
# Upload coverage from one combo only — avoids 4× artifact duplication; 3.11 is canonical (matches publish.yml build job).
if: matrix.python-version == '3.11' && always()
uses: actions/upload-artifact@v5
with:
name: coverage-report
path: coverage.xml
# --- Job 3: Validate (config, CLI, assets) ---
validate:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v7
with:
node-version: "24"
package-manager-cache: false
- uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
- name: Install package
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
# Asserts the premise every step below rests on: the `forgelm` these
# commands import is the checkout, not a shadowing site-packages copy.
# A non-editable install once made the dry-run step validate a
# weeks-old package and report success on an unrelated working tree.
# Ephemeral runners + `pip install -e` make that unlikely here; the
# guard is what keeps it from silently becoming likely again if an
# install step is ever reworked.
- name: Import-origin guard
run: python3 tools/check_import_origin.py --strict
# testing.md forbids `|| true` in CI, but only the documented gauntlets
# were checked, and only for the literal `|| true`: appending it to the
# Node test command below left every wiring test green. This guard
# parses every run: block and the three gauntlets with a small shell
# lexer, and fails when a required command (pytest, node --test, ruff,
# mypy, the forgelm CLI, a tools/ guard) can fail without failing its
# step: an `||` fallback that can exit 0, a pipe without pipefail,
# set +e, continue-on-error, or a failure errexit ignores inside an
# &&/|| list or a { } group. bandit and pip-audit keep `|| true` only
# when the next command is their severity helper reading the same
# report.
- name: CI failure-masking guard (strict)
run: python3 tools/check_ci_failure_masking.py --strict
- name: Browser wizard regression tests
run: node --test tests/site/*.test.cjs
- name: CLI smoke checks
run: |
forgelm --version
forgelm --config config_template.yaml --dry-run
forgelm --config config_template.yaml --dry-run --output-format json
# Both entry points are public surface and must stay in lockstep:
# the console script above is what PyPI users get, `python -m` is
# what the contributor gauntlet runs (see CONTRIBUTING.md).
python -m forgelm --version
python -m forgelm --config config_template.yaml --dry-run
python -m forgelm --config config_template.yaml --dry-run --output-format json
- name: Config template validation
run: |
python -c "
from forgelm.config import load_config
cfg = load_config('config_template.yaml')
assert cfg.model.name_or_path, 'model.name_or_path is empty'
assert cfg.model.trust_remote_code is False, 'trust_remote_code should default to False'
assert cfg.model.offline is False, 'offline should default to False'
assert cfg.training.trainer_type == 'sft', 'default trainer_type should be sft'
assert cfg.data.dataset_name_or_path, 'data.dataset_name_or_path is empty'
print('Config template validation passed.')
"
- name: DeepSpeed config validation
run: |
python -c "
import json, os
configs_dir = 'configs/deepspeed'
for preset in ['zero2.json', 'zero3.json', 'zero3_offload.json']:
path = os.path.join(configs_dir, preset)
assert os.path.isfile(path), f'Missing: {path}'
with open(path) as f:
data = json.load(f)
assert 'zero_optimization' in data, f'{preset}: missing zero_optimization'
assert data['train_batch_size'] == 'auto', f'{preset}: train_batch_size should be auto'
print(f'{preset}: OK (ZeRO stage {data[\"zero_optimization\"][\"stage\"]})')
"
- name: Notebook validation
run: |
python -c "
import json, os
for nb_file in os.listdir('notebooks'):
if nb_file.endswith('.ipynb'):
path = os.path.join('notebooks', nb_file)
with open(path) as f:
nb = json.load(f)
assert nb['nbformat'] == 4, f'{nb_file}: invalid nbformat'
assert len(nb['cells']) > 0, f'{nb_file}: no cells'
print(f'{nb_file}: OK ({len(nb[\"cells\"])} cells)')
"
- name: Safety prompts validation
run: |
python -c "
import json, os
prompts_dir = 'configs/safety_prompts'
assert os.path.isdir(prompts_dir), 'Missing configs/safety_prompts/'
total = 0
for f in os.listdir(prompts_dir):
if f.endswith('.jsonl'):
path = os.path.join(prompts_dir, f)
with open(path) as fh:
for i, line in enumerate(fh):
data = json.loads(line)
assert 'prompt' in data, f'{f} line {i+1}: missing prompt key'
total += 1
print(f'{f}: OK')
assert total >= 140, f'Expected 140+ safety prompts, found {total}'
print(f'Total safety prompts: {total}')
"
- name: Synthetic module import check
run: |
python -c "
from forgelm.synthetic import SyntheticDataGenerator
print('synthetic.py import: OK')
"
- name: Site-as-tested-surface guard (strict)
# Diffs site/*.html claims against the Python sources of truth
# (compliance artefacts, quickstart templates, GPU profile count,
# pyproject version). Fails the build on any drift; see
# tools/check_site_claims.py for what's checked and why.
run: python3 tools/check_site_claims.py --strict
- name: Doc numerical-claims drift check (strict)
# F-P8-C-06 (W1/H5): re-derives the canonical counts (secret
# families, trainer types, quickstart templates, webhook events)
# from the Python sources of truth and fails the build when a doc
# asserts a stale number — e.g. the "five webhook events" prose that
# drifted after the vocabulary grew to eight. Wired once H5's 5->8
# doc-drift fix made the guard green at HEAD.
run: python3 tools/check_doc_numerical_claims.py --strict
- name: Bilingual doc H2/H3/H4 parity check (strict)
# Phase 24: extended structural parity guard. Where the prior
# inline check counted H2 only and missed reordered / demoted
# sections, ``tools/check_bilingual_parity.py`` walks every
# registered EN/TR pair and compares the full H2 + H3 + H4
# spine. Translated text differs by definition; structural
# depth + ordering must not. Add a new pair: register it in
# ``tools/check_bilingual_parity.py::_PAIRS``; CI picks it up
# automatically the next run.
run: python3 tools/check_bilingual_parity.py --strict
- name: Markdown anchor resolution check (strict)
# Wave 5 / Faz 30 Task N: every Markdown anchor + relative-link
# under docs/ must resolve. Catches renamed/removed targets,
# slug-case drift, and forward-refs to pages that never landed
# (the GH-014/017/019 bug class). Strict mode wired up after
# the Wave 5 cleanup zeroed the live drift count from 36 → 0;
# the tool itself remains usable in advisory mode (no flag) for
# local feedback during a docs PR. See tools/check_anchor_resolution.py
# for what's checked and the GFM slug approximation used.
run: python3 tools/check_anchor_resolution.py --strict
- name: CLI / docs help-consistency drift check (strict)
# Wave 5 / Faz 30 Task J: catches docs that cite ghost flags
# or non-existent subcommands (the GH-008/011/016/018/020
# bug class). Strict mode wired up after the Wave 5 cleanup
# commit zeroed the live drift count from 40 → 0 (matches the
# check_anchor_resolution.py precedent: gate lands advisory,
# baseline cleanup follows, later commit flips to --strict).
# The tool itself remains usable in advisory mode (no flag)
# for local feedback during a docs PR.
run: python3 tools/check_cli_help_consistency.py --strict
- name: CLI --help exit-code prose guard (strict)
# Step 3 follow-up (F-2): the guard above validates invocation
# *syntax*, never exit-code *prose*, so when EXIT_INTEGRITY_FAILURE
# (6) shipped for the four verify-* subcommands, `forgelm
# verify-audit --help` kept telling CI engineers that "1 means
# tampering" — the most authoritative statement of a public
# contract was the one surface with no mechanical reader.
# Asserts one bit per subcommand in both directions (an emitter
# of 6 must document it; a non-emitter must not claim it), with
# both sides derived from source so there is no mapping table to
# maintain. Lands straight in --strict: the live finding count
# is already 0.
run: python3 tools/check_cli_exit_code_prose.py --strict
- name: Unguarded sys.modules.pop guard
# v0.5.7 round-4 absorption: the v0.5.7 round-3 review traced
# 35 spurious full-suite failures to three test sites that
# popped torch / numpy from sys.modules without restoring
# them, half-loading torch._C for every later test in the
# pytest session. This guard fails CI on any new
# ``sys.modules.pop("<heavy-module>")`` /
# ``del sys.modules["<heavy-module>"]`` site, steering authors
# to ``monkeypatch.delitem`` (auto-restores on teardown).
run: python3 tools/check_no_unguarded_sys_modules_pop.py
- name: Audit-event catalog drift check (strict)
# W0/C7 (full-project review): the append-only audit log is a
# public compliance contract (EU AI Act Art. 12). This guard
# cross-checks every dotted audit event emitted in forgelm/
# (log_event / _audit_event / event= / _EVT_* constants) against
# the canonical table in docs/reference/audit_event_catalog.md, in
# both directions — an undocumented emit OR a ghost catalog row
# fails CI. Previously unwired: six pipeline.* stage events drifted
# into the code with zero tripwire. See
# tools/check_audit_event_catalog.py.
run: python3 tools/check_audit_event_catalog.py --strict
- name: TR cross-links prefer the TR mirror (strict)
# W1/H11 (F-P8-C-04): a docs/**/*-tr.md page must route its in-prose
# cross-references to the Turkish sibling when a <stem>-tr.md mirror
# exists — a Turkish operator following a 'Bkz.'/'See also' link must
# stay in Turkish, not silently land on the English page. Neither
# check_anchor_resolution (link resolves) nor check_bilingual_parity
# (heading spine) catches this. The **Ayna:** backlink line is exempt.
# 62 leaks across 19 files at HEAD were swept to zero in H11.
run: python3 tools/check_tr_links_prefer_mirror.py --strict
- name: Top-level README/CONTRIBUTING link check (strict)
# pyproject ships README.md as the PyPI long description with no URL
# rewriting, so a relative href resolves against pypi.org and 404s for
# exactly the reader who arrived via `pip install forgelm`. 38 such
# links shipped that way, invisible to CI: check_anchor_resolution
# defaults to --root docs and never opens README.md, and
# check_source_path_refs inspects backticked source paths, not hrefs.
# Also asserts every in-repo target resolves on disk — on CONTRIBUTING
# too, which is otherwise unguarded. See tools/check_readme_links.py.
run: python3 tools/check_readme_links.py --strict
- name: Library-API doc ↔ __all__ drift check (strict)
# W1/H11 (F-P8-C-07): forgelm.__all__ must match the symbol roster in
# docs/reference/library_api_reference.md in both directions. Previously
# unwired — a renamed/removed public symbol could drift from the doc.
run: python3 tools/check_library_api_doc.py --strict
- name: Bilingual fenced-code-block parity (strict)
# W1/H11 (F-P8-C-13): the Wave 6 fenced-block-count + per-ordinal
# YAML-key parity guard was an orphan (wired nowhere, no own test).
# Catches TR code-block / YAML-key drift the spine guard cannot see.
run: python3 tools/check_bilingual_code_blocks.py --strict
- name: ForgeConfig YAML snippet validation (strict)
# W1/H11 (F-P8-C-07): every ForgeConfig-shaped YAML snippet in the docs
# must pass Pydantic validation. Previously unwired despite catching
# real doc-vs-schema drift.
run: python3 tools/check_yaml_snippets.py --strict
- name: User-manual YAML example schema-key drift
# check_yaml_snippets.py only validates snippets carrying the full
# model+training+data triplet; single-block fragments (the common
# case in a user-manual page explaining one section) are skipped
# there. This guard AST-walks forgelm/config.py's Pydantic schema
# and flags fragment keys that don't exist on it. Runs with --strict:
# the introduction-time fabricated-key backlog (38 instances across 8
# EN/TR page-pairs) has been cleaned, so any new drift now fails CI.
run: python3 tools/check_usermanual_schema_drift.py --strict
- name: Deprecation removal-target drift (strict)
# The removal version for lora.use_dora / lora.use_rslora /
# training.sample_packing was a hardcoded literal duplicated across
# ~20 sites and rotted twice (v0.9.0 -> v0.10.0 -> v1.0.0), leaving
# stragglers that told operators two different versions. The target
# now lives in forgelm/config.py::DEPRECATION_REMOVAL_VERSION; this
# guard reads it by AST and fails on any divergent claim in
# forgelm/, config_template.yaml, docs/ or tests/ — and on a target
# that pyproject.toml's version has already reached (removing a YAML
# field is MAJOR, so the promise must stay ahead of the release).
run: python3 tools/check_deprecation_targets.py --strict
- name: Release-record sync (CHANGELOG -> roadmap)
# The cut-release skill's pre-release step 4.5 ("Write the release
# record — before the tag") asks for a docs/roadmap/releases.md
# section and a refreshed docs/roadmap.md '**Released:**' headline.
# Nothing verified it, and while it still lived in the post-release
# "Close the phase" step it was skipped for two consecutive releases
# (v0.8.0, v0.9.0) — the public roadmap announced v0.7.0 while PyPI
# had v0.9.0. This guard cross-checks every released CHANGELOG heading
# against releases.md (a '(Planned)' section does not count) and pins
# the headline to the newest released version. It also refuses to
# report success on input it could not parse: a near-miss heading
# (##[9.9.0]), a fence that swallows one, or a changelog that yields
# zero releases all fail rather than going quietly green.
run: python3 tools/check_release_record_sync.py --strict
- name: Skill mirror parity (.claude/skills <-> .agents/skills)
# The agent skills ship twice — once per harness — and are edited by
# hand, so a one-copy edit is silent: the agent reading the un-edited
# copy keeps following the old procedure. Nothing compared the trees
# until the cut-release ritual reorder had to be applied to two files
# in two directories. This guard requires the same skills, the same
# files, and identical content once the documented .claude/ <->
# .agents/ substitution allowlist (paths, CLAUDE.md/AGENTS.md, agent
# name) is applied.
run: python3 tools/check_skill_mirror_parity.py --strict
- name: Dead source-path references in prose (strict)
# The forgelm/safety.py -> forgelm/safety/ split moved the file
# cleanly (AST symbol diff + 4,100-input differential fuzz) and then
# shipped 39 dangling forgelm/safety.py references across 16 files —
# in the very commit whose purpose was moving that file. Nothing
# noticed because nothing could see them: check_anchor_resolution.py
# validates [text](href) links under docs/ only, while the dead refs
# lived in backticked inline paths, .claude//.agents/ skill
# checklists, site/*.html copy, notebook JSON and CLAUDE.md's
# repository-structure tree. This guard resolves every forgelm/,
# tools/ and tests/ path named in prose against the filesystem.
# Scope was set by measurement: all top-level dirs gave 266 findings
# on a clean tree, the three source trees give 14 (all genuine). It
# skips fenced blocks, docs/roadmap/ and docs/design/ (records and
# unbuilt proposals), and Markdown link hrefs — so a broken link is
# reported by exactly one guard, never both.
run: python3 tools/check_source_path_refs.py --strict
- name: Site chrome (EN/TR translation) parity
# W1/H11 (F-P8-C-07): the active-tier (EN<->TR) translation-key sets in
# site/js/translations.js must stay in lockstep. Run WITHOUT --strict:
# --strict additionally gates the deferred de/fr/es/zh tiers, which are
# a known v0.6.x backlog the guard's own help text says is "NOT wired
# into CI". Default mode enforces only the active-tier parity rule.
run: python3 tools/check_site_chrome_parity.py
- name: Module-size ceiling guard (strict)
# W1/H11 (F-P8-C-07): no forgelm/ module may drift past the
# architecture-doc ~1000-LOC sub-package-split ceiling. Previously
# unwired — module-size regressions could merge with green CI.
run: python3 tools/check_module_size.py --strict
- name: Wizard defaults schema-sync (strict)
# W1/H11 (F-P8-C-07): the wizard's shipped defaults JSON must match the
# Pydantic schema source of truth. Was gauntlet-only (human discipline).
run: python3 tools/check_wizard_defaults_sync.py
- name: No public-tree refs into gitignored working-memory
# W1/H11 (F-P8-C-07): public-tree files must not cite docs/analysis/ or
# docs/marketing/. Was gauntlet-only (human discipline) until now.
run: python3 tools/check_no_analysis_refs.py
- name: Marketing-site version sync (strict)
# W1/H11 (F-P8-C-07): the marketing site's displayed version must match
# CHANGELOG's latest released header. Was gauntlet-only.
run: python3 tools/update_site_version.py --check
- name: Notebook forgelm pin lockstep (strict)
# W2/M7 (F-P8-C-09): every notebooks/*.ipynb !pip install forgelm pin
# must target a shipping wheel — the exact pyproject version or the
# latest released CHANGELOG version (so onboarding notebooks never
# point users at a pre-release rc). Previously unwired; the pins had
# drifted two minors (0.5.7 vs released 0.7.0) undetected.
run: python3 tools/check_notebook_pins.py --strict
- name: User-manual self-contained link guard (strict)
# Post-v0.7.0 cycle: the docs/usermanuals/ tree is the source of
# truth for the static-site SPA viewer (site/usermanual.html +
# site/js/guide.js). The viewer only resolves SPA hash-router
# routes ``#/<section>/<page>`` and external HTTPS URLs.
# Anything else — repo-relative ``../../../guides/...`` paths,
# intra-manual ``../section/page.md`` paths, SPA routes that
# point at a non-existent page — 404s when the user clicks.
# This guard walks every *.md under docs/usermanuals/ and
# fails the gate on any link that would break in the SPA.
# See docs/standards/documentation.md "User-manual link
# discipline" for the full ruleset.
run: python3 tools/check_usermanual_self_contained.py --strict
- name: License check
run: |
test -f LICENSE || (echo "LICENSE file missing" && exit 1)
head -1 LICENSE | grep -q "Apache" || (echo "Expected Apache License" && exit 1)
echo "License: OK"
# Wave 4 / Faz 23: bandit static-security analysis on forgelm/.
# tests/ is excluded via [tool.bandit] in pyproject.toml because
# test fixtures legitimately use insecure patterns (assert, dummy
# secrets). HIGH → fail; MEDIUM → ::warning::; LOW → silent.
# (Mirrors nightly.yml supply-chain-security severity policy.)
# See tools/check_bandit.py for the severity-tiering helper.
- name: bandit (static security analysis)
run: |
python -m pip install 'bandit[toml]>=1.7.0,<2.0.0'
# bandit returns exit 1 on ANY findings. We capture the JSON
# report and apply our own severity policy via the tiering
# helper (tools/check_bandit.py).
bandit -c pyproject.toml -r forgelm/ -f json -o /tmp/bandit.json || true
python3 tools/check_bandit.py /tmp/bandit.json