The books were one physical file each: ARCHITECTURE 2,386 lines and DEVELOPMENT 3,886, with 13 and 14 `##` sections. `reference_books.py` had shipped the chaptered reader — membership, authored introductions, exact physical source views — with the migration still at zero, so every reader took the legacy monolith branch and the validator had no production caller. Each `##` section is now one chapter file under `docs/architecture/` or `docs/development/`. The only new bytes per chapter are its prologue: the old section title at H1 (numbering text kept, so every `ARCHITECTURE "8. Git Branching, CI, and Build"` cross-reference still reads) and one authored introductory paragraph saying what the chapter owns and why it exists. Everything after that prologue is the old section body byte for byte, with `###`/`####` levels untouched — so the residue rules keep reading the exact subsection headings they exempt, and no section title was renamed. The move is therefore INVERTIBLE, and `tests/test_reference_book_migration.py` inverts it: drop each chapter's H1 line and its one introduction, re-prefix `## `, concatenate in membership order, and require the recorded SHA-256 of the old body — plus, whenever the base commit is reachable, byte equality with `git show <base>:<path>`. `docs/reference-books-migration.md` is the operator transfer table: every row a verbatim move, with its line range at the base, its destination and an empty rename column. It lives directly under `docs/`, so it is reviewable without becoming a book member. `_preamble` now takes the FIRST paragraph under the H1 instead of demanding the only one before the first H2. Most sections open with prose at the level they already had, so the old rule could only be satisfied by promoting `###` to `##` or inventing a sub-heading — either of which would rewrite what this migration relocates verbatim. A source whose H1 is followed straight by a subsection still has no introduction and is still refused, which is the property that keeps an overview from quoting body prose as authored orientation. `.gitattributes` pins `docs/**/*.md` to LF: chapter line ranges, byte spans and SHA-256s are physical facts that a Windows checkout must not rewrite, and `full-test` runs on windows-latest for every PR. The two ARCHITECTURE-derived generated inventories are regenerated, because a chaptered section now carries its physical provenance note. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
15 KiB
Build & CI
This chapter owns the build and test topology: the one dependency-lock authority and its packaging projections, the seven opt-in pytest marker lanes, the parallel and serial split CI actually runs, the hermetic commit gate that mirrors that split in a disposable checkout, and the GitHub Actions secret-gating shape. It exists because the gate's verdict has to be reproducible and un-weakenable by the candidate it is judging.
Python dependency locks
pyproject.toml is the direct-dependency SSOT and uv.lock the reviewed
cross-platform resolution; local and CI use uv sync --locked, and no
independent hand-written requirements authority exists. Release packaging
exports build requirements ephemerally and commits
requirements-runtime.lock for embedded pip; requirements.txt is a
generated pointer for older managed updaters, never an authority. A
dependency change updates the metadata, runs uv lock, regenerates the
runtime export with the exact README command, and leaves the CI clean-diff
check green. The pinned tool.uv.required-version and digest-pinned
setup-uv action make resolver changes deliberate rather than an ambient
CI upgrade. Documentation may pair the checkout-free uv tool install form
with a full commit SHA to pin the source revision, but must not claim it
locks dependencies or describe it as a release-artifact install or
contributor development environment.
Pytest marker lanes
Default local pytest excludes seven costly or environment-dependent lanes —
integration, browser, ui_browser, ui_browser_docker,
portable_detail, skill_smoke, and size_ratchet — and CI opts into
them explicitly (job topology and provider matrix: ARCHITECTURE "CI
topology"):
integrationruns real provider checks, including the trusted direct-OpenAI canary rows derived fromOPENAI_DIRECT_DEFAULTS. Missing core credentials are red in the official repository job; explicit quota/429/5xx/timeout may be typed inconclusive, while contract/auth/model/reasoning/tool 4xx stay red. Secretless request-wire and Anthropic-custody contracts remain in ordinary pull-request tests: do not move provider secrets into PR jobs or duplicate the trusted lane. The KEYLESStests/system_e2e/scenario lane rides the same marker plusserialand theOUROBOROS_E2E_DEEP=mockenv gate — real isolated servers, no provider keys (ARCHITECTURE "System E2E suite"). Because those three gates shut it out of every other pass, the only thing that runs it is the dedicatedsystem-e2e-mockCI job on its daily schedule, manual dispatch or release tag: never an ordinary branch push or pull_request, and carrying no secret.browser/ui_browser/ui_browser_dockerlaunch real Playwright engines (agent browser tools / the host UI / theouroboros-web:testcontainer; the docker lane skips cleanly when Docker is unavailable locally). The marker is the source of truth for what the lane collects; the four Widgets lifecycle suites listed under "Declarative widgets" run in it.portable_detailcovers build/portable artifact invariants. Existingui-smokeruns onlytests/test_skill_publish_browser.pyon PRs, using Chromium and real task admission, Main card and history. Manual/tag runs keep the full host UI and Chromium/WebKit browser-tool suites. These checks do not join the default local pytest run or require a paid provider.skill_smokeinstalls the nine pinned official skills from the LIVE catalog (list intests/test_skill_smoke_official.py) and runs as the dedicated 3-OS CI job in serial pytest invocations with real network and real pip; red means investigate — there is deliberately no fallback-skip. Its paid review tier runs as a SEPARATE pytest step (fresh process) that alone carries the provider key, ORDERED FIRST and ubuntu-only: the other tiers import downloaded plugin code in-process, and running the secret step first means the runner has never executed payload code while the secret was present. A missing key is a hard red, not a skip.size_ratchetcarries the live-repo size gates and is the ONLY blocking surface for repository size (rules under "Module Size & Complexity"; only checks against the live repo carry the marker). The base fallback verifies the parent manifest against the parent's own tree — accepting a copied manifest would allow debt laundering, so a resolvable base that lost its manifest fails closed.
skill_smoke and size_ratchet tests must NOT also carry the serial
marker or join _SERIAL_TEST_FILES: the and not <lane> markexprs in
quick/full-test are the lane barrier, and single-lane assignment keeps each
test's placement unambiguous. When adding a new opt-in lane, register the
marker in pyproject.toml, add a collect-only zero-test guard in CI, and
keep the default local addopts free of network and Docker requirements.
Parallel CI and the serial marker
CI runs the default suite in parallel — python -m pytest tests/ with
-m "not serial and <the seven lane exclusions>", -n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread —
followed by a serial pass for -m "serial and <the same exclusions>"
(.github/workflows/ci.yml, jobs quick-test / full-test). Two rules
keep new tests from breaking that:
- Mark real-process / real-port tests, and tests that mutate process-global
state WITHOUT reliable fixture isolation,
@pytest.mark.serial(or add the file to_SERIAL_TEST_FILESintests/conftest.py). Under-nsuch a test flakes on kill/reap or port-reclaim timing, or crashes its worker — and with--max-worker-restart=0a dead worker fails its WHOLE co-located batch, showing up as spurious failures in unrelated files. - Keep every other test parallel-safe so it stays in the fast pass: use
tmp_path(never a fixed path),monkeypatch.setenv/delenv/setattrfor environment and attribute changes, and no execution-order assumptions. The autousetests/conftest.py::_os_environ_isolationsnapshot restoresos.environat every test boundary, so a bare assignment no longer leaks; monkeypatch stays the rule because it reverses exactly the named change inside the test, before the snapshot runs. A module-global mutation that is reliably snapshot-and-restored by a fixture may stay in the parallel pass — the pattern istests/conftest.py::_isolate_workspace_executor_globals.
The commit gate mirrors the CI split
ouroboros/preflight_runner.py::run_hermetic_pytest mirrors CI in one
disposable checkout and scrubbed temporary data root: the node test lane
(cd web && node --test tests/*.test.js, content-keyed — a candidate
without web tests never requires node, while an active web suite cannot
silently disappear when node is missing), then the same two logical pytest
passes (parallel not serial, then flag-free serial).
The browser no-undef check has two layers: the dependency-free acorn walker
in that suite (web/tests/no_undef.test.js) is the hermetic gate's, and
both CI jobs additionally run ESLint's no-undef (web/eslint.config.js,
exact-pinned, installed with npm ci from web/package-lock.json) as an
independent second opinion — CI-only, never part of the gate.
LANE_EXCLUSION_EXPR and PARALLEL_PASS_FLAGS are executable SSOTs pinned
against both CI jobs; the candidate is captured as one hardened
worktree-vs-HEAD binary diff, and a capture or apply failure is the typed
PREFLIGHT_CANDIDATE_ASSEMBLY hard block, never a test failure.
The pyproject.toml addopts line is the single home of the per-test timing
report (--durations=25 --durations-min=1.0): it is prepended to every argv, so
the same slowest-test evidence appears in a plain local run, in both CI jobs and
in both gate passes without any surface pinning its own copy.
Contributor rules:
- The candidate cannot weaken the pass:
PYTEST_*/NODE_OPTIONSare scrubbed, and so is owner runtime state —OUROBOROS_*, secret-suffixed keys and every settings keyconfig.apply_settings_to_envprojects (derived fromsettings_env_keys()), so the verdict cannot depend on the operator's install profile; required plugins are probed outside candidate control and forced on with host-owned worker evidence, post-commit checks also inspectHEAD~1so suite deletion cannot hide after the commit exists, and exit status owns the verdict — rendered diagnostics do not.OUROBOROS_PREFLIGHT_SERIAL=1is the explicit temporary rollback lever, never a silent fallback. - A red post-commit gate is warning-only for an ordinary commit (the local
commit is preserved for forensics); evolution publication refuses to
auto-push while the warning stands, and inside a managed update the gate
blocks boot promotion and routes through rollback — an incomplete
rollback leaves
gate_blockedso boot retries recovery instead of promoting the rejected merge. - The managed mandate is "the full suite provably ran green on the exact
committed tree", not "run it twice": the reuse authority is the
process-held runner proof (
ctx._preflight_test_proof) covering the tested tree, installed index, effective passes and execution environment. Equivalent ordinary preflights and post-commit runs reuse it too, after distinct baseline checks. Every workload binds HEAD: equal file trees do not establish equivalence for history-sensitive tests, including ordinary unmarked tests. A newly created commit therefore requires a new run; repeated preflights with unchanged HEAD still reuse the existing proof. A skip, no applicable suite or a mockedNonereturn cannot mint a proof. The durabletests_evidencerecord is forensic telemetry the gate never consults, so a restart forces a rerun. Review-binding and tag-binding mismatches use the same managed failure route. Creation/reuse observations use the existing event log with the exact subject and workload fingerprint; logs never become reuse authority. Preserve the executable's invocation path as well as its resolved binary identity, because separate Python environments may symlink the same binary. - Process containment is unconditional, including after a green pass:
Windows uses a kill-on-close Job Object; POSIX uses an environment
membership token plus a process-group enumeration backstop and promises
honest detection with a fail-closed verdict for attributed members, not
guaranteed teardown of an arbitrary detached process. A same-uid unreadable
stranger is a warning, never membership proof; an unobserved descendant
that detached and hid its token remains a disclosed detection gap. Known
roots, groups and the retained set of observed members still fail closed when unreadable
(
tests/test_preflight_process_containment.py). A crashed worker, a timeout-killed worker, a missing plugin, containment failure, and ordinary test failure keep distinct diagnostics. - Mark process/port/global-state tests
serial; make a merely slow test faster or split it — marking it serial removes the 300s per-test timeout and lets it consume the remaining total gate budget.
GitHub Actions: secrets in step-level if: conditions
GitHub Actions rejects secrets.* inside step-level if: expressions, and
a step's own env: block is not visible to that same step's if:. Derive
a non-secret boolean in the job-level env: block, gate steps with that
boolean, and map the actual credentials only inside the first-party steps
that need them — later SBOM and attestation steps then inherit none of
them.
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
env:
HAS_APPLE_SIGNING: ${{ matrix.os == 'macos-latest' && secrets.BUILD_CERTIFICATE_BASE64 != '' && secrets.P12_PASSWORD != '' && secrets.KEYCHAIN_PASSWORD != '' && secrets.APPLE_TEAM_ID != '' && 'true' || 'false' }}
steps:
- name: Import Apple signing certificate
if: env.HAS_APPLE_SIGNING == 'true'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "${BUILD_CERTIFICATE_BASE64}" | base64 -d > cert.p12
security create-keychain -p "${KEYCHAIN_PASSWORD}" build.keychain
security import cert.p12 -k build.keychain -P "${P12_PASSWORD}"
- name: Cleanup keychain
if: always() && matrix.os == 'macos-latest' && env.HAS_APPLE_SIGNING == 'true'
run: security delete-keychain build.keychain
# ❌ WRONG — workflow fails to parse
- name: Bad
if: secrets.BUILD_CERTIFICATE_BASE64 != '' # parse error
env: # not visible to this step's if:
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
tests/test_build_scripts.py::TestMacOSSigning::test_ci_uses_env_context_for_condition
enforces this for .github/workflows/ci.yml only; other workflow files are
not scanned by it.
Apple signing & notarization (macOS Build job)
Prerelease artifacts may intentionally be unsigned and must report that
state; stable publication applies the configured signing and notarization
policy rather than implying credentials or success that were absent. Only
the non-secret HAS_APPLE_SIGNING gate is job-wide; certificate/keychain
values exist only in the import step and Apple ID notarization values only
in the first-party build step. Notary/stapler failures are soft outcomes
recorded through NOTARIZE_OUTCOME, so a transient Apple service problem
does not silently drop an otherwise valid signed artifact; cleanup uses the
always() plus matrix/env guards, and signing material never persists
across runs.
Release proof capsule
The artifact pipeline — per-platform archive smokes, native Linux packages,
the AppImage custody chain, SBOM and attestation binding, and the
seven-asset release job — lives in ARCHITECTURE "8. Git Branching, CI, and
Build" and .github/workflows/ci.yml. The honesty invariants a change must
preserve:
- Publication is draft-first with a per-tag concurrency group; the remote annotated tag is revalidated against the event SHA immediately before draft creation AND again before publication, and a published release is never overwritten by a rerun.
- Vendor-distribution smokes (Astra, RED OS) are reported evidence, never release authority — third-party registry reachability is outside the publication pipeline's control.
- The AppImage smoke deliberately makes no native GTK/Qt claim; packaged native webview coverage remains a separate Linux distribution contract.
OUROBOROS_SKIP_PLAYWRIGHT_INSTALL_DEPS=1is only a local-builder escape hatch — it skips Playwright's host-library installation, not browser-binary bundling — and a build using it must disclose that browser host compatibility was not locally proven.- Never represent a later checksum inventory as build-time provenance, an SBOM, or packaged smoke evidence that the original build did not create.