AGENTS.md@.github/workflows · git:20261013.67580c5 · 2026-10-13 · sha256 d0d9f7bff157a99e
AGENTS.md@.github/workflows git:20261013.67580c5A
Immutable. This exact content never changes and is served at /api/v1/blob/d0d9f7bff157a99e.
## Fast functional CI
`ci.yml` runs on main pushes, pull requests and manual dispatch. It uses Linux
and Python 3.14: lint/types and public data/security guards, pipeline smoke,
a standalone wheel build and focused behavioral tests for every public template,
and the required `CI Gate`. `.github/project-smoke.json` names the actual tests;
the smoke runner refuses missing projects, empty selections and escaping paths.
These are functional checks, with no full-suite coverage or native acceptance claim.
Manual CI dispatch optionally selects coverage-project by its exact canonical
public qualified name. The separate own-exemplar-coverage job runs the existing
own locked release-profile measurement and retains its commit/input-hash bound
artifact. Verify that job, the whole workflow and the retrieved artifact before
admitting its percentage. CI Gate covers all six declared jobs: five functional
successes, plus coverage success when requested or its skip when unrequested.
The former cross-version/full-suite matrix remains in `hardening.yml`, invoked
manually. `platform-required-acceptance.yml` is also manual-only; its original
custody requirements remain mandatory for formal release acceptance. Further
hardening choices and the local commands are in [`docs/development/ci.md`](../../docs/development/ci.md).
# CI/CD Workflows
## Overview
The `workflows/` directory contains GitHub Actions workflows that automate the continuous integration and delivery pipeline for the Research Project Template. These workflows ensure code quality, test reliability, and compatibility across environments.
## Directory Structure
```mermaid
flowchart LR
W[.github/workflows/]
W --> META[AGENTS.md · README.md]
W --> FAST[ci.yml<br/>Linux/Python 3.14 functional checks · CI Gate]
W --> CI[hardening.yml<br/>20 jobs · 2 conditional via detect outputs<br/>manual-only lanes: test-infra-slow, public-matrix-receipt]
W --> STALE[stale.yml<br/>Auto-label/close stale issues/PRs]
W --> REL[release.yml<br/>Create GitHub Releases on version tags]
W --> REQUIRED[platform-required-acceptance.yml<br/>Original observations + strict candidate collector]
W --> DA[dependabot-automerge.yml<br/>Auto-merge safe Dependabot PRs]
classDef d fill:#0f172a,stroke:#0f172a,color:#fff
classDef code fill:#1e3a8a,stroke:#0f172a,color:#fff
classDef doc fill:#0f766e,stroke:#0f172a,color:#fff
class W d
class FAST,CI,STALE,REL,REQUIRED code
class META doc
```
## Optional exhaustive hardening (`hardening.yml`)
### Triggers
`workflow_dispatch` only. Each manual run uses its unique run ID and preserves
its evidence independently. This lane is not a requirement for ordinary main
integration. Its aggregate check is named **Hardening Gate**.
**Global env:** `UV_FROZEN=true`, `MPLBACKEND=Agg` (non-interactive matplotlib backend).
### Job Graph
`health`, `validate`, `security`, and `docs-lint` run independently alongside **`lint`**; each uses its own checkout and setup. `setup-hook-windows-smoke` depends on **`verify-no-mocks`** and **`detect`** and is skipped unless `needs.detect.outputs.setup_hook == 'true'`. The test jobs run through the independent `verify-no-mocks` subtree; `test-infra` also consumes `detect-projects`. An individual job can pass when lint fails. The aggregate `ci-gate` must still fail when a required job fails or is unexpectedly skipped. This graph describes [`hardening.yml`](hardening.yml); workflow checks and review verify its current dependencies and aggregate policy.
```mermaid
flowchart TB
DET[detect<br/>optional-project outputs]
DETP[detect-projects<br/>validated capability matrix output]
ACTLINT[actionlint<br/>lints workflow YAML · standalone]
LINT[lint]
HEALTH[health<br/>unified JSON artefact]
VNM[verify-no-mocks<br/>parallel with lint · no needs:]
VAL[validate]
SEC[security]
DL[docs-lint<br/>mermaid + cross-links + consistency<br/>installs mmdc + chrome-headless-shell]
VNM --> SHW[setup-hook-windows-smoke<br/>skipped if no setup_hook.py]
DETP -.->|infra_matrix output| TI
VNM --> TI[test-infra<br/>dynamic matrix from detect-projects<br/>manual: ubuntu × 3.12-3.14 + macOS × 3.14<br/>full supported matrix · four complete shards per environment]
TI --> IC[coverage-infra<br/>complete 3.14 Linux union ≥60%]
VNM --> TR[test-regression<br/>claim-binding pins · tests/regression/]
VNM --> TP[test-project<br/>capability manifest roster × canonical Python versions<br/>stage_01_test.py --project per cell]
VNM --> FL[fep-lean<br/>ubuntu-only · skipped if no lean-toolchain]
DET --> SHW
DET --> FL
DETP --> TP
PERF[performance<br/>no needs · CI-FAST-PERF-1]
classDef gate fill:#1e3a8a,stroke:#0f172a,color:#fff
classDef matrix fill:#0f766e,stroke:#0f172a,color:#fff
classDef terminal fill:#7c2d12,stroke:#0f172a,color:#fff
classDef info fill:#334155,stroke:#0f172a,color:#fff
class DET,DETP,LINT,VNM gate
class TI,TR,TP,FL,SHW matrix
class VAL,SEC,DL,PERF,ACTLINT terminal
class HEALTH info
```
### Shared setup — local composite actions
Jobs that need Python share one local composite action,
[`.github/actions/setup-python-env`](../actions/setup-python-env/action.yml),
which runs `astral-sh/setup-uv` (with `uv.lock` cache) + `actions/setup-python`.
The pinned action SHAs and cache config live there once instead of being
copy-pasted per job. Usage (checkout **must** come first — a local action's
files only exist after checkout):
```yaml
steps:
- uses: actions/checkout@<sha>
- uses: ./.github/actions/setup-python-env # defaults to Python 3.14
# with: { python-version: ${{ matrix.python-version }} } # matrix jobs only
- run: uv sync # per-job groups stay explicit
```
`detect` and `actionlint` are checkout-only and do not use it. Each job keeps
its own `uv sync …` line so optional groups (`rendering`/`monitoring`/`discopy`)
remain visible per job. When bumping a setup action's SHA, edit the composite
action — not each job.
The `health` and `docs-lint` jobs additionally share
[`.github/actions/setup-docs-lint`](../actions/setup-docs-lint/action.yml),
which provisions the pinned Node runtime/cache plus real `mmdc` and
`chrome-headless-shell`. This keeps unified health's `docs-lint` constituent
behaviorally equivalent to the dedicated documentation job.
### Job Details
#### 1. Lint & Type Check (`lint`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **Tools:** `uv run ruff check`, `uv run ruff format --check`, `uv run mypy`, `uv run python -m infrastructure.skills check-all-exports`, `uv run python scripts/audit/check_template_drift.py --strict`
- **Scope:** Ruff uses public lint paths from `infrastructure.project.public_scope lint-paths`; mypy uses its narrower `source-paths` output.
#### 2. Static Health Report (`health`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **Depends on:** nothing; the job performs its own checkout and setup alongside `lint`.
A red static gate turns this job red and blocks `ci-gate`; this page does not certify hosted-runner behavior beyond that.
- **Purpose:** Runs `uv run python -m infrastructure.core.health --json --quiet` → `health-report.json`; every represented static gate blocks, while behavioral and platform matrices remain separate jobs; a gate exiting non-zero must turn the job red, and warnings alone never satisfy it. Negative control: when any represented static gate fails, `infrastructure.core.health` exits non-zero and blocks this job rather than reporting green alongside a failure.
A negative control backs this contract: deliberately breaking any represented static check locally flips the aggregate health report away from healthy status, which blocks the job.
- **Negative control:** removing or weakening a represented static gate in `infrastructure/core/health.py` must make the `health` job fail on the next run — a green report with a missing gate is invalid.
#### 3. Verify No Mocks Policy (`verify-no-mocks`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **Script:** [`scripts/audit/verify_no_mocks.py`](../../scripts/audit/verify_no_mocks.py) (repository root)
- **Enforced policy:** no configured prohibited mock-framework imports/calls
(`MagicMock`, `mocker.patch`, `unittest.mock`, and related lexical forms) in
test files.
- **Boundary:** `--inventory` separately classifies permitted environment
isolation and semantic dependency replacement. CI enforces a zero ceiling
for dependency replacements; environment isolation remains permitted.
#### 3b. Setup hook — Windows smoke (`setup-hook-windows-smoke`)
- **Runner:** `windows-latest` / Python 3.14
- **Depends on:** `verify-no-mocks`
- **Conditional:** `if: needs.detect.outputs.setup_hook == 'true'` — no-op skip when no project ships [`infrastructure.project.setup_hook`](../../infrastructure/project/setup_hook.py). The `detect` job computes this because job-level `hashFiles()` is invalid in GitHub Actions.
- **Step:** `uv run pytest tests/infra_tests/project/test_setup_hook.py` with `PYTHONUTF8=1`
#### 4. Infrastructure Tests (`test-infra`)
- **Pinned source setup:** `test-infra` and `coverage-infra` both initialize the direct submodule declared by the actual Root gitlink and `.gitmodules` using the pinned checkout action's `submodules: true`. This checks out the committed kmyth object; the declaration's branch is not used to advance it. Initialization is an explicit network-capable setup boundary before Python setup and source observation. Tests subsequently clone this actual local checkout without network. Both jobs use `persist-credentials: false`, so setup credentials are removed before test or merge code runs. The merge runner receives the same complete source bytes as the coverage workers; missing pins remain failures, with no fixture fabrication or skip. The canonical workflow guard in `tests/infra_tests/core/test_ci_infra_partition.py` checks both setup declarations against the actual native Git index and public submodule declaration. `test-project`, `test-infra-slow`, and `public-matrix-receipt` use the same pinned, credential-free checkout contract before their genuine standalone export controls.
- **Sync:** `uv sync --locked --group public-exemplars --group installed-build`. Genuine wheel/sdist controls use the prepared, locked build backend.
- **Offline warm:** a scratch `uv sync --frozen` against the selected interpreter re-populates the uv cache with every locked default-group wheel (setup-uv prunes registry wheels from the saved cache), so infra tests that build fresh environments with `uv sync --locked --offline` do not depend on a cold or pruned cache. The scratch environment is deleted.
- **Document tools:** Both platforms invoke [`setup-acceptance-documents`](../actions/setup-acceptance-documents/action.yml) once. It verifies exact pinned Pandoc/crossref/font archives, prepares the complete TeX and native-reader package union, and probes the selected executables. Linux installs `poppler-utils`; macOS installs `poppler`. Both lanes directly check `pdftoppm` and `pdffonts` before the actual PDF controls. Package caches cannot certify preparation or acceptance.
- **Matrix:** Every existing environment is retained: Linux Python 3.12–3.14 and macOS Python 3.14. The source-declared `ci_infra_partition.py` emits four complete module shards per environment (16 cells). The four measured C4 families are assigned distinct shards; every other current or future collected module receives a stable SHA-256 assignment. No test is marked slow or removed by partitioning.
- **Selection:** `ci_infra_shard_plugin` partitions actual pytest collection after the unchanged ordinary marker expression. Each selected case belongs to exactly one shard; empty/duplicate/noncanonical inventory, worker disagreement and incomplete actual completion fail. The plugin is explicitly loaded for this invocation, so nested pytest jobs retain their own selection.
- **Diagnostics:** On the xdist controller the plugin also keeps a bounded, diagnostics-only record (`output/readiness/ordinary-infra-ci/diagnostics/shard-N.json`) of active cases, recent events, first failures and worker loss, labeled by declared public test family and member hash only. Every shard uploads it with `if: always()` as `infra-diagnostics-<os>-<python>-<shard>`; the manual `test-infra-slow` job loads the plugin with `--infra-ci-observer-only slow` to retain `shard-slow.json` as `infra-diagnostics-slow` without partition or selection admission. These records identify where a crashed or cancelled job was, and cannot replace a selection receipt or authorize coverage.
- **Budgets:** The job is 90 minutes, pytest remains `--timeout=120`, and every existing per-case timeout override remains effective. Linux retains `-n auto`, macOS retains `-n 2`, and both use `--dist loadscope`. Faster completion is a hypothesis until fresh hosted timings establish it; additional jobs repeat preparation and may increase total runner minutes.
- **Coverage:** Only the four Python 3.14/Linux shards instrument execution. Per-shard thresholds are replaced by the required `coverage-infra` complete-union gate at the same ≥60% floor. The gate refuses missing/extra/duplicate/stale/failed selection records, incomplete case unions and changed coverage bytes, then merges genuine branch measurements in memory and writes confined JSON/XML. Codecov upload remains nonblocking after this required local threshold passes.
- **Receipt scope:** Ordinary selection records bind infrastructure/scripts/infra tests/CI configuration source bytes. They do not replace full public source custody, installed-wheel, live, platform or release acceptance; missing Root release policy remains blocked.
- **MathJax live viewport:** The manual hardening browser spec runs once on shard 0 of Python 3.14/Linux. Its existing tool provisioning, completed-typesetting checks and failure policy remain unchanged.
For an unpartitioned local check, the existing full `pytest tests/infra_tests/` command remains supported without this explicit plugin. An exact shard invocation requires a fresh owned checkout and uses `-p infrastructure.core.testing.ci_infra_shard_plugin --infra-ci-shard N` with the workflow's marker expression; the slow lane's observer-only form is `-p infrastructure.core.testing.ci_infra_shard_plugin --infra-ci-observer-only slow -m slow`. The selection receipt is local execution state, not a cache or resume authorization.
#### 4b. Regression Tier — claim-binding pins (`test-regression`)
- **Depends on:** `verify-no-mocks`, `timeout-minutes: 20`, ubuntu-only.
- **Sync:** `uv sync --group public-exemplars`.
- **What it runs:** `uv run pytest tests/regression/ -q --no-cov --timeout=120 --junitxml=<fresh job-local report>`, serial (no `-n auto`) — see [`docs/maintenance/regression-testing.md`](../../docs/maintenance/regression-testing.md) for why (exemplars ship colliding top-level `src` packages resolved via per-project aliases + temporary `sys.meta_path` finders whose isolation is collection-order-sensitive).
- **Collection contract:** the ordinary run must exit zero, including successful collection. "Assert regression tier is not empty" then reads that run's fresh JUnit report and requires at least 3 actual case entries with no failures or errors. Missing/malformed reports and empty or undersized suites block. This replaces a second collection-only invocation while keeping the original selector and three-case minimum.
#### 4c. Integration Tier — repository wiring (`test-integration`)
- **Depends on:** `verify-no-mocks`, `timeout-minutes: 20`, ubuntu-only.
- **Sync:** `uv sync --group public-exemplars`.
- **What it runs:** `uv run pytest tests/integration/ -q --no-cov --timeout=120 --junitxml=<fresh job-local report>`, serial.
- **Collection contract:** the same successful-run JUnit census requires at least 3 case entries and no failures or errors. Collection errors retain pytest's nonzero exit; missing/malformed, empty, or undersized reports block. The shell and pipeline suite selector stays unchanged.
#### 5. Project Tests (`test-project`)
- **Sync:** `uv sync --locked --group public-exemplars --group installed-build` — the deterministic dependency union includes DisCoPy, monitoring, scientific, LLM-client, and PPTX dependencies plus the prepared backend for offline genuine-wheel controls. **Hypothesis** comes from the **dev** group (see root `pyproject.toml` `[dependency-groups]` and `default-groups`).
- **Standalone preparation:** the Autoresearch cell separately exports the root `installed-build` group and its own exact nonlocal runtime lock, both with hashes, and installs each into a disposable environment with `uv pip sync --no-config --no-build --require-hashes` before tests. The lockfile `uv sync` above fetches wheels by locked URL and does not record the registry entries that the offline genuine-wheel acceptance (`uv pip sync --offline`) resolves from, so this explicit setup is what fills the cache for it; the root and exemplar locks retain their independent versions.
- **Matrix:** **Per-project split** — the `detect-projects` job runs `scripts/gates/public_capabilities.py --ci-matrix-json`, which validates unique normalized package identities, full-minor Python compatibility, source/test syntax, format declarations, compiled/confined direct hydration, analysis declarations, reason-bearing skips, exact roster membership, and exact matrix parity before emitting the canonical `project × Python` include list. The current source of truth yields 25 exemplars × Python 3.12/3.13/3.14 = **75 matrix cells** on `ubuntu-latest`; no project or Python literal is duplicated in workflow YAML. Both matrix jobs set `UV_PYTHON` and assert the selected runtime minor so the repository `.python-version` cannot override a matrix cell. Job `timeout-minutes: 135`; its direct Stage-01 invocation owns one 115-minute generic-or-declared project budget, leaving 20 minutes for setup, descendant cleanup, and uploads. The full pipeline's separate 120-minute stage wrapper is not present on this hosted direct path. These limits provide execution capacity; they do not show that a project suite passed or emitted valid evidence.
- **Coverage threshold:** Each job enforces **that project's own ≥ 90%** floor on its `src/` (per CLAUDE.md). There is **no longer** a combined-union run or `--cov-append` — every project is isolated in its own job, which also removes the old `code_project`/`fep_lean` conftest plugin-name collision.
- **Coverage files:** project-local `.coverage.project` for the generic pytest path or `.coverage` for a declared structured verifier; both are isolated, cleaned before the run, and independently produce the same project-local `coverage_project.json`.
- **Scope:** [`scripts/pipeline/stage_01_test.py`](../../scripts/pipeline/stage_01_test.py) `--project <name> --project-only --include-slow` (one invocation per matrix cell). The workflow uploads the resulting project-local JSON directly; it does not synthesize a separate XML report. Rotating local projects are not part of this public-repo gate; dedicated project jobs own their own toolchains.
- **External render tools:** every lane unconditionally invokes [`setup-acceptance-documents`](../actions/setup-acceptance-documents/action.yml) for exact pinned Pandoc/crossref preparation. EDA selects `native` because its ordinary tests directly require XeLaTeX and embedded JuliaMono faces; other ordinary project cells select `converter`. Infrastructure and all genuine acceptance callers retain the default native package/font preparation. Test selections, coverage floors and required native acceptance remain unchanged; no runtime or cost improvement is established until genuine execution is measured.
- **Codecov upload:** On Python 3.14 / `ubuntu-latest` only; upload failures do not fail CI
#### 6. fep_lean — real Open Gauss + Lake (`fep-lean`)
- **Conditional:** Job is **skipped** unless `projects/fep_lean/lean/lean-toolchain` exists and the `detect` job emits `fep_lean == 'true'`. When fep_lean lives under `projects/working/`, `detect` reports `false` and the job is skipped. Promote with `mv projects/working/fep_lean projects/fep_lean` to activate.
- **Runner:** `ubuntu-latest` / Python 3.14 only; job `timeout-minutes: 60`
- **Depends on:** `verify-no-mocks`
- **Working directory (when present):** `projects/fep_lean` for pytest; `projects/fep_lean/lean` for Lake warm-up
- **Toolchain:** SHA-pinned elan installer (with checksum verification) + pinned
`lean-toolchain`, `lake build` warm-up
- **Open Gauss:** clone [math-inc/OpenGauss](https://github.com/math-inc/OpenGauss), `./scripts/install.sh --plain --noninteractive --skip-system-packages`, `gauss doctor`
- **Tests:** `uv run pytest tests/ --timeout=1200 --cov=src --cov-fail-under=89` with `COVERAGE_FILE: ../../.coverage.fep_lean`
- **Scaling:** Full catalogue × Lean is expensive; if runtime grows past the job budget, split slow integration tests behind a pytest marker or shard topics in a follow-up workflow.
#### 7. Validate Manuscripts (`validate`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **Steps:**
1. `infrastructure.validation.cli markdown projects/*/manuscript/` — validates all active project manuscripts
2. `scripts/docgen/api_reference.py --check` — API reference drift gate
3. Dynamic project import check — imports the public project source paths from `infrastructure.project.public_scope`
#### 8. Security Scan (`security`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **pip-audit:** blocking; audits the root all-groups/all-extras export and every canonical public-exemplar all-extras export, builds `--ignore-vuln` args from [`.github/pip-audit-ignore.txt`](../pip-audit-ignore.txt), and retries each export up to **3** times with backoff on failure (transient OSV/network issues)
- **bandit:** `bandit -c bandit.yaml -r -ll`, covers `infrastructure/`, `scripts/`, `projects/`; path exclusions are in `bandit.yaml` (`exclude_dirs`, including archive/WIP roots and `.venv` / `site-packages` so local trees are not scanned)
#### 9. Documentation Lint (`docs-lint`)
- **Runner:** `ubuntu-latest` / Python 3.14 / Node 20
- **Depends on:** nothing; the job performs its own checkout and setup alongside `lint`.
- **Timeout:** 15 minutes
- **External tools (real, not mocked):**
- `mmdc` (mermaid-cli) — pinned in the root `package.json`; run `npm ci`
- `chrome-headless-shell` — `npx --no-install puppeteer browsers install chrome-headless-shell`, exported via `CHROME_EXECUTABLE_PATH`
- **Linters (thin orchestrator [`scripts/audit/lint_docs.py`](../../scripts/audit/lint_docs.py)):**
1. **Mermaid** — every fenced \`\`\`mermaid block in `docs/`, `infrastructure/`, `.github/`, `scripts/`, and root `*.md` is rendered with the real `mmdc` binary. Failure exits non-zero.
2. **Cross-links** — every relative Markdown link must resolve on disk; fenced and inline-code spans are skipped.
3. **Consistency** — `N Python (sub)packages` claims must match the live count under `infrastructure/`; rotating project names (`fep_lean`, `cogant`, …) must be conditionally framed in long-lived docs.
Skip lists narrow the scan — they do not certify the excluded trees; anything outside the skips still fails the lint job.
4. **Doc pairs** — permanent-template content folders must carry paired `AGENTS.md` and `README.md`; generated/local paths and rotating projects are excluded.
- **Escape hatch:** append `<!-- noqa: docs-lint -->` to a Markdown line to suppress consistency or broken-link warnings on that line. Negative control: adding a deliberately broken relative link to a tracked doc makes `uv run python scripts/audit/lint_docs.py` exit non-zero — the run fails when any unresolved cross-link is reported.
This skip list bounds what the linter checks: it scans tracked documentation only, and checking skip rules does not prove correctness of excluded generated trees.
- **Scope guarantees:** the linter scans only tracked Markdown outside generated/local paths such as `output/`, `.venv/`, `.claude/`, `projects/archive/`, `projects/working/`, `htmlcov/`, and `node_modules/`; inside those trees it enforces nothing. Known-wrong input: an unlisted build tree such as a fresh `dist/` holding broken links is NOT skipped by these guarantees — the exclusion list is closed, and excluding a new path requires an explicit entry in `infrastructure/validation/docs/scan_scope.py`.
- **Module:** [`infrastructure/validation/docs/`](../../infrastructure/validation/docs/) — `mermaid_lint.py`, `cross_link_lint.py`, `consistency_lint.py`, `doc_pair_lint.py`.
#### 10. Performance Check (`performance`)
- **Runner:** `ubuntu-latest` / Python 3.14
- **Depends on:** nothing (CI-FAST-PERF-1 — the job consumes no upstream artifacts, so no `needs:`; `ci-gate` still requires it). It therefore runs in the first wave instead of after the slowest project cell.
- **Threshold:** each `infrastructure.core` or public project `src` cold import from `infrastructure.project.public_scope` must complete in ≤ 5 seconds
- **Per-module timing** and the roster-dependent total are reported to stdout for trend analysis
### Quality Gates
| Gate | Threshold | Enforced by |
|---|---|---|
| Ruff linting | zero violations | `lint` job |
| Ruff formatting | zero diffs | `lint` job |
| mypy strict gate | zero errors across the generated public source scope | `lint` job |
| Mock-framework lexical gate | zero prohibited imports/calls | `verify-no-mocks` job |
| Public capability parity | exact roster/matrix; compatible Python floors; valid package/render/hydration/analysis declarations | `detect-projects` + unified `health` |
| Infrastructure coverage | ≥ 60% | `coverage-infra` complete-union job |
| Per-project coverage (standalone) | ≥ 90% | each project's own pytest gate |
| Combined-union public-project coverage | ≥ 75% | `test-project` job (`DEFAULT_FAIL_UNDER`) |
| fep_lean coverage | ≥ 89% | `fep-lean` job (skipped if `projects/fep_lean/lean/lean-toolchain` absent) |
| pip-audit | no unignored vulnerabilities | `security` job |
| Bandit MEDIUM+ (`bandit.yaml`) | zero findings | `security` job |
| Import time | ≤ 5 seconds total | `performance` job |
---
## Stale Workflow (`stale.yml`)
Runs daily at 01:00 UTC using `actions/stale@v10.3.0`.
| Item | Stale after | Closed after |
|---|---|---|
| Issues | 60 days inactive | + 14 days |
| Pull Requests | 30 days inactive | + 14 days |
**Exempt labels:** `pinned`, `security`, `in-progress`, `blocked`, `do-not-close`
---
## Release Workflow (`release.yml`)
Triggers on `v*.*.*` tag push or `workflow_dispatch` (with tag input). Both triggers serialize by the selected tag and preserve in-flight publication; a manual dispatch from a different branch cannot race the same tag push.
1. Resolves the requested tag before checkout, initializes its pinned submodules, and proves `HEAD` equals the dereferenced tag commit
2. Prepares supported interpreters, the locked release environment and identical acceptance fixture inputs; audits pristine published exemplars before the installed handoff
3. For root 4+, requires the exact successful hosted run ID, `platform-required-acceptance` artifact ID and aggregate receipt SHA256; authenticates the same-repository exact-revision run and bounded artifact, then uses an independently installed current reader to authenticate every candidate requirement, the complete retained installed matrix and its exact admitted wheel/receipt. Tag pushes without that explicit handoff stop
4. Runs the tagged-SHA no-mocks and bounded pipeline-smoke gates, root release contract and fail-closed public capability manifest
5. Earlier major tags retain the clean-export, install and import-smoke matrix on Python 3.12/3.13/3.14. Root 4+ already authenticated its retained complete matrix in step 3. Preparation and acceptance stay distinct
6. Builds the final source distribution and admits its exact final bytes alongside the wheel already accepted by the installed matrix; the pristine published audit has already passed before derived execution
7. For root 4+, selects `docs/releases/<tag>.md` for the actual tag, such as the [4.0.1 candidate notes](../../docs/releases/v4.0.1.md), and adds accepted-commit, hosted-run and receipt-hash bindings; earlier major tags use the commit-based git-log excerpt
8. Creates a GitHub Release using `softprops/action-gh-release@v3.0.3` with **`generate_release_notes: false`** and attaches the admitted distributions, tag-selected overview PNG and byte manifest
9. Retrieves the published assets and requires exact admitted hashes; auto-marks as pre-release if the tag contains `-rc`, `-beta`, or `-alpha`
The candidate [`platform-required-acceptance.yml`](platform-required-acceptance.yml)
implements six original Root minor/platform units plus common and native
observations and a strict independent collector. Only its passed complete
exact-source bundle can supply the root 4+ handoff. Implementation presence or
a dated version heading is not proof of successful acceptance, integration into
`main`, or publication. Ordinary CI and partial/static results cannot replace it.
The pinned [checkout action](https://raw.githubusercontent.com/actions/checkout/3d3c42e5aac5ba805825da76410c181273ba90b1/action.yml) and
[release action](https://raw.githubusercontent.com/softprops/action-gh-release/efb35369e0ad2afab669f228072c1b0d510eae64/action.yml) use the Node 24 JavaScript action runtime.
This is separate from the Node 20 document toolchain selected by
[`setup-docs-lint`](../actions/setup-docs-lint/action.yml).
GitHub-hosted runners satisfy the documented runtime requirement; self-hosted
runners must be Actions runner `v2.327.1` or newer.
## Dependabot Automerge Workflow (`dependabot-automerge.yml`)
Triggers on `pull_request_target` (opened, reopened, synchronize, ready_for_review) — but only acts when `github.actor == 'dependabot[bot]'`.
1. Fetches Dependabot update metadata via `dependabot/fetch-metadata@v3.1.0`
2. Enables GitHub native auto-merge (`gh pr merge --auto --squash`) for `semver-minor` and `semver-patch` updates only; major bumps are left for human review
3. Never checks out or executes PR HEAD code — only reads metadata and calls the GitHub API — so the elevated `pull_request_target` trigger does not expose secrets to untrusted code
4. Auto-merge must also be enabled in repository settings (Settings → General → "Allow auto-merge")
---
## Local CI Simulation
```bash
# Reproduce lint locally
uv sync
uv run python -m infrastructure.project.public_scope lint-paths | xargs uv run ruff check
uv run python -m infrastructure.project.public_scope lint-paths | xargs uv run ruff format --check
uv run python -m infrastructure.project.public_scope source-paths | xargs uv run mypy
# Reproduce infrastructure tests locally
COVERAGE_FILE=.coverage.infra uv run pytest tests/infra_tests/ \
--cov=infrastructure \
--cov-fail-under=60 \
-m "not requires_ollama"
# Reproduce project tests locally (matrix job ignores fep_lean). The root
# default groups include the deterministic public-exemplar dependency union.
uv sync
COVERAGE_FILE=.coverage.project uv run python scripts/pipeline/stage_01_test.py --project-only --all-projects --public-projects --non-strict --include-slow
uv run coverage xml -o coverage-project.xml
# fep_lean only — requires gauss, lake, lean on PATH (see that project's tests/AGENTS.md when present)
(cd projects/fep_lean && COVERAGE_FILE=../../.coverage.fep_lean uv run pytest tests/ \
--timeout=900 \
--cov=src \
--cov-fail-under=89 \
-m "not requires_ollama")
# Reproduce security scan locally (mirror CI — build ignores from file)
IGNORE_ARGS=()
while IFS= read -r raw || [ -n "$raw" ]; do
[[ "$raw" =~ ^[[:space:]]*# ]] && continue
line="${raw%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
IGNORE_ARGS+=(--ignore-vuln "$line")
done < .github/pip-audit-ignore.txt
AUDIT_DIR="$(mktemp -d /tmp/template-public-audit.XXXXXX)"
uv export --all-groups --all-extras --frozen --no-hashes \
--no-emit-project --output-file "$AUDIT_DIR/root.txt"
uv run pip-audit --requirement "$AUDIT_DIR/root.txt" \
--no-deps --disable-pip "${IGNORE_ARGS[@]}"
for project in $(uv run python -m infrastructure.project.public_scope project-names); do
slug="${project//\//-}"
uv export --project "projects/$project" --all-extras --frozen --no-hashes \
--no-emit-project --output-file "$AUDIT_DIR/${slug}.txt"
uv run pip-audit --requirement "$AUDIT_DIR/${slug}.txt" \
--no-deps --disable-pip "${IGNORE_ARGS[@]}"
done
uv run bandit -c bandit.yaml -r -ll infrastructure/ scripts/ projects/
```
---
## Troubleshooting
### Linting failures
```bash
uv run python -m infrastructure.project.public_scope lint-paths | xargs uv run ruff check --fix
uv run python -m infrastructure.project.public_scope lint-paths | xargs uv run ruff format
```
### Test failures
```bash
# Infrastructure
uv run pytest tests/infra_tests/ -v --tb=long -s
# Project tests — prefer the orchestrator (runs one pytest per project; avoids conftest/package collisions):
uv run python scripts/pipeline/stage_01_test.py --project template_code_project
# Advanced / blanket globs — running **all** `projects/*/tests/` in **one** pytest process can fail when multiple projects ship `tests/conftest` packages with identical names; use per-project directories instead.
uv run pytest projects/*/tests/ -v --tb=long -s
# Single test
uv run pytest tests/infra_tests/test_foo.py::TestClass::test_method -s --pdb
```
### Coverage below threshold
```bash
# Infrastructure report
COVERAGE_FILE=.coverage.infra uv run pytest tests/infra_tests/ \
--cov=infrastructure --cov-report=html
open htmlcov/index.html
# Project report — replace SRC_PATH with `projects/<name>/src` from _generated/active_projects.md when benchmarking coverage manually:
COVERAGE_FILE=.coverage.project uv run pytest projects/templates/template_code_project/tests/ \
--cov=projects/templates/template_code_project/src --cov-report=html
open htmlcov/index.html
```
### Performance check slow
```bash
# Profile imports
uv run python -c "
import cProfile, pstats, io
pr = cProfile.Profile()
pr.enable()
import infrastructure.core
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats(20)
print(s.getvalue())
"
```
---
## See Also
- [`../README.md`](../README.md) — `.github/` human guide (workflows, Dependabot, templates)
- [`../AGENTS.md`](../AGENTS.md) — GitHub integration overview
- [`../../AGENTS.md`](../../AGENTS.md) — Root system overview and pipeline stages
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [uv Documentation](https://docs.astral.sh/uv/)
- [Codecov Documentation](https://docs.codecov.com/)
The infrastructure partition changes execution placement only. The required `coverage-infra` job consumes every 3.14/Linux shard, validates their complete disjoint case union and source/data identities, and enforces the unchanged 60% floor. The existing `ci-gate` complete-job policy includes this job; an unsuccessful or skipped merge prevents optional exhaustive hardening acceptance. Root 4 release evidence remains separately required.