CI — workflow map, tiers, owner checklist, troubleshooting
One page. Workflows live in .github/workflows/; every gate they
run is a Makefile target (see Local parity). Sources: the
CI design reviews ratified 2026-07-21 (internal working papers; the durable decisions are
this page + the workflows); platform semantics below are [VF] against GitHub docs as of
that date.
Workflow map
| Workflow | Trigger | Purpose | State |
|---|---|---|---|
ci.yml |
every push + PR | The required PR gate: change detection → conditional jobs → the ci-required aggregator |
active |
nightly.yml |
cron 09:17 UTC + dispatch | Full local gate on a clean machine + online audits (external links, networked zizmor); grows the T3 suites at M3+; files/updates one title-deduplicated nightly-failure issue on red | active |
sandbox.yml |
workflow_dispatch only |
T4 sandbox contracts — fail-closed skeleton, gated by the sandbox environment; every dispatch is deliberately red until human M4 activation |
skeleton (always refuses) |
benchmark.yml |
workflow_dispatch only |
IrrevonBench preregistered runs — fail-closed skeleton, gated by the benchmark environment; every dispatch is deliberately red until human Stage-B activation |
skeleton (always refuses) |
release.yml |
PR + manual dry run; canonical vMAJOR.MINOR.PATCH tag |
Non-publishing artifact dry run on PRs; clean tagged build, exact HEAD/tag/current-main identity, Node-24/content/renderer-hash/smoke gates, checksums, lock-aware SPDX SBOM, artifact attestation, then protected-environment PyPI/GitHub publication | active; published and attested v0.1.0 |
scorecard.yml |
main/protection change + weekly | OpenSSF Scorecard evidence and SARIF upload | active |
dependabot.yml |
monthly routine + prompt security | One multi-ecosystem routine PR across Actions, uv, web npm, site npm, and Docker; security remediation remains grouped per ecosystem | active |
Tier table — what runs when
| Tier | Job (workflow) | Condition | Runs | Today |
|---|---|---|---|---|
| T0 docs/static | docs (ci) |
always, every push/PR | make check (links, schemas, secrets, integrity, actionslint, frozen, assets, third-party, bench-integrity) |
active |
| T0 backend static | py-check (ci) |
backend paths changed AND pyproject.toml exists |
make py-check |
active (engine landed; skips cleanly on docs-only PRs) |
| T1 backend unit+property | py-test (ci) |
same | make py-test (≥1,000 Hypothesis cases/invariant — spec, never lowered) |
active (same skip rule) |
| F0 web static | web-check (ci) |
web/ paths changed AND web/ exists |
make web-check |
active (workbench landed; skips cleanly on non-web PRs) |
| F1/F2 web tests | web-test (ci) |
same | make web-test |
active (same skip rule) |
| F3 web browser + accessibility | web-e2e (ci) |
same | make web-e2e (built review app; full workflow matrix + route-wide axe in both themes, zero retries) |
active (same skip rule; required through ci-required) |
| workflow security | workflow-security (ci) |
.github/workflows/** changed |
actionlint + zizmor (online, pedantic) | active |
| T2 integration | py-test-integration (ci) |
backend changes | make py-test-integration vs the digest-pinned compose Postgres |
active (wired at the rebuild consolidation, per this row’s earlier “due” note) |
| site static | site-check (ci) |
site/ changed |
make site-check (astro check + every drift gate) |
active (wired at the rebuild consolidation) |
| site tests | site-test (ci) |
site/ changed |
make site-test (build + Playwright a11y/keyboard/no-JS/links/budgets/search/anti-fabrication/SEO) |
active |
| live E2E | web-e2e-live (ci) |
backend OR web/ changed (both slices must exist) |
make web-e2e-live (real demo → real irrevon serve → Playwright against the staged packaged workbench) |
active |
| — | ci-required (ci) |
if: always() |
aggregates all of the above; the ONLY required check | active |
| T3 nightly | validate + t3-backend (nightly) |
cron | make check + online audits; conformance-budget properties + full integration suite (fault-matrix subset vs the stub destination) |
active |
| wheel smoke | wheel-smoke (nightly) |
cron | make dist-smoke (= make dist + the Node-less container smoke; ADR-0018 chain, wheel + sdist legs) |
active — nightly, not PR: needs docker + a second full web build + wheel build; the PR-side integration truth is web-e2e-live |
| T4 sandbox | sandbox-contract (sandbox) |
human dispatch + env approval | Today: exactly make sandbox-stage-m4, whose reserved recipe unconditionally refuses. M4 activation must replace it with the reviewed credentialed contract recipe |
skeleton (always refuses) |
| benchmark | bench (benchmark) |
human dispatch + env approval | Today: exactly make benchmark-stage-b, whose reserved recipe unconditionally refuses. M7 activation must replace it with the complete preregistered, cache-free, sanitized-evidence recipe |
skeleton (always refuses) |
| package release | dry-run / validate-build / build-attest / publish jobs (release) |
PR/manual is non-publishing; canonical version tag only for publication | locked Python validators + make launch-audit + Node-24 release dry run; privileged attestation/publish jobs download validated artifacts and never execute repository code |
active; protected release environment remains human-gated |
The local make check-all ladder includes web-check, web-test, and web-e2e; F4
pixel baselines remain the explicit container-only make web-vrt gate. The bench
foundation (ADR-0030, proposed) additionally added: bench-integrity
into make check (stdlib-only fixture/canary/holdout/freeze gate) and
bench-smoke into make check-all (CLI end-to-end over two dev workloads,
conventional arms, no database). The full harness suites run inside the
existing py-test / py-test-integration tiers (tests/bench/).
A docs-only PR runs changes + docs (+ workflow-security if workflows changed) and
passes legitimately — conditional jobs skip and the aggregator verifies each skip against
the change detection.
Node runtime contract
[DD] Active jobs never inherit the mutable Node version preinstalled on
ubuntu-latest. Every job that invokes Corepack first runs the full-SHA-pinned
actions/setup-node v7.0.0 action and resolves Node from the package it operates:
web/.nvmrc for workbench/build jobs and site/.nvmrc for marketing-site jobs. Package
manager versions remain locked by each package’s packageManager field and lockfile.
A stdlib-only workflow contract test fails if any active Corepack job loses, reorders, or
changes that runtime setup.
Fail-closed sandbox skeleton
[DD] The sandbox workflow cannot be used as live-provider evidence today. Its sole
executable body after checkout is the reserved make sandbox-stage-m4 entrypoint, and that
target unconditionally exits nonzero without reading an input or credential, testing for a
file, invoking a harness, creating an artifact, or contacting a provider. Consequently,
configuring repository state cannot turn the dormant skeleton green.
Activation is a separate human-reviewed M4 task after Stage A, the human ADR-0010 and ADR-0012 decisions, applicable terms review, and a real credentialed contract recipe. In one reviewed change it must replace the refusal with that recipe, add guards for the human-selected environment credentials, preserve manual dispatch and environment approval, and replace the static refusal contract with tests of the active target. A green pre-activation dispatch is a workflow-integrity failure, not a successful sandbox contract.
Fail-closed benchmark skeleton
[DD] The benchmark workflow cannot be used as evidence today. Its sole executable body
after checkout is the reserved make benchmark-stage-b entrypoint, and that target
unconditionally exits nonzero without reading a secret, testing for a file, invoking the
harness, or contacting a provider. Consequently, creating the benchmark environment,
configuring any secret, or committing a registration-shaped file cannot turn the skeleton
green.
Activation is a separate human-approved Stage-B task after the preregistration’s P7 gate. In one reviewed change it must replace the refusal with the complete registered run procedure, pin its environment/tooling, add credential guards before any credentialed step, preserve the manual trigger and environment approval, and replace the static refusal contract with tests of the real target. A green pre-activation dispatch is a workflow integrity failure, not a successful benchmark.
Protected package release
[DD] Pull requests and manual dispatches can only run the non-publishing dry
run. Publication requires a canonical-repository tag that exactly matches a
non-development package version and whose peeled commit exactly equals current
origin/main and checked-out HEAD; merely being an ancestor of main is
insufficient. Repository code runs only in an unprivileged validation job; a
separate job downloads those artifacts to request GitHub attestations. The
candidate build refuses non-Node-24 Workbench builds. Twine 6.2.0,
check-jsonschema, PyPA pip-audit 2.10.1, and SPDX Tools are
exact-constrained in the dedicated release-validation group and transitively
resolved by uv.lock; release jobs invoke the group through uv run --locked.
The audit consumes a hash-bearing frozen export of only the production graph
without dependency resolution. Twine’s check --strict PyPI
long-description gate must leave the wheel/sdist hashes unchanged, after which
the archive manifests are checked again. PyPI and GitHub publication are
separate least-privilege jobs behind the owner-created and protected release
environment. No long-lived publishing token is accepted. On 2026-07-25 this
path published and attested v0.1.0; future publication still requires a
canonical owner-pushed annotated tag and protected-environment approval. See
release-process.md.
Dependabot update policy
[DD] Routine version updates use GitHub’s multi-ecosystem-groups contract:
all five supported dependency trees join monthly-routine, so the scheduled
steady state is at most one open routine pull request. Every entry retains a
seven-day cooldown and a one-PR version-update limit. The group and each
ecosystem assign the owner and the explicit dependencies label; no automatic
merge path exists.
Security fixes are intentionally different. GitHub does not combine security
updates across package ecosystems, so each ecosystem has its own
applies-to: security-updates catch-all group. Security updates bypass routine
schedules and cooldowns and keep their separate platform limit. Repository-level
Grouped security updates must remain enabled so qualifying fixes across
directories within an ecosystem consolidate without suppressing remediation.
Quarterly major-upgrade review
Routine major updates are disabled for /web and /site, whose dependencies
are exact-pinned and validated as complete stacks. Once per quarter, the owner:
- reviews upstream release notes and security advisories for deferred majors;
- opens one normal, non-Dependabot change for one stack at a time;
- regenerates only that stack’s lockfile and dependency notices;
- runs its static, unit, browser, accessibility, visual, and package embedding
gates plus
make check; and - merges only through the ordinary
ci-requiredpath, with rollback notes for material runtime or build changes.
Urgent security remediation never waits for this quarterly cycle.
Local parity
Required CI gate bodies run their documented make target after
scripts/bootstrap-tools.sh. Its native
lychee/gitleaks/actionlint/zizmor archives are checksum-verified. The generic
docs/nightly path retains the explicitly version-pinned check-jsonschema
fallback for runners without uv; the release workflow does not use that
fallback and instead resolves both Python validators from uv.lock through
uv run --locked. What CI checks is what the corresponding local Make target
checks. [DD] Three non-Make exceptions remain: workflow-security runs zizmor
with network advisories and nightly runs lychee/zizmor online (online variants
of offline make gates — the local gate stays deterministic --offline on
purpose), and dependency-review (GitHub-owned action, SHA-pinned,
contents: read) has no local equivalent because it consumes GitHub’s advisory
database against the PR diff. The job exists only on pull_request events, is
included in ci-required.needs, and the aggregator requires it to succeed on
pull requests while accepting its skip on push events.
Owner settings and verified launch posture
Script: scripts/setup-repo-settings.sh automates
secret scanning, Dependabot, default workflow permissions, fork approval, and the initial
ruleset (bash scripts/setup-repo-settings.sh, idempotent, semantic read-back included).
Its --phase2 mode is self-guarded and refuses until ci-required has reported green on a
real PR. Actions allowlisting/SHA-pin enforcement, CodeQL default setup, private
vulnerability reporting, immutable releases, and environments require
authenticated platform configuration plus semantic read-back.
Verified read-back on 2026-07-24:
-
Secret scanning + push protection — Settings → Advanced Security → enable Secret scanning, Push protection, and Scan for non-provider patterns. Automated by
scripts/setup-repo-settings.sh(phase 1). Second layer only — local gitleaks (pre-commit +make secrets) stays primary. Secret scanning and push protection are enabled. GitHub accepted but did not activate non-provider-pattern and validity scanning for this account, so both remain explicitly unavailable rather than represented as enabled. -
Dependabot alerts + security updates — same pane; automated by
scripts/setup-repo-settings.sh(phase 1). Also enable Grouped security updates; configuration groups remain per ecosystem because GitHub does not combine security fixes across ecosystems. -
Actions posture — Settings → Actions → General: use selected-actions mode with GitHub-owned actions plus only the exact full-SHA setup-uv, Scorecard, and PyPI-publish pins committed in the workflows; check Require actions to be pinned to a full-length commit SHA; fork-PR approval = Require approval for all external contributors (the default first-time-only tier is gameable
[VF]); workflow permissions read-only, “create and approve pull requests” off. Read-back: selected-actions mode and platform SHA-pin enforcement are enabled. GitHub-owned actions plus the exact full-SHA pins for setup-uv, Scorecard, and PyPI publication are allowed. Default workflow permissions are read-only and Actions cannot approve pull requests. -
Ruleset phase 1 — Settings → Rules → Rulesets → New branch ruleset on the default branch:
deletion,non_fast_forward,pull_request(0 required approvals — a solo owner cannot approve their own PR),bypass_actors: []. Read-back:main-protection-phase1is active, applies tomain, requires the pull-request path andci-required, blocks deletion and non-fast-forward updates, and has an emptybypass_actorsarray. The setup script refuses semantic drift instead of treating a same-named ruleset as correct. Afterci-requiredhas reported on at least one PR (order matters — see traps below): -
Ruleset phase 2 — add
required_status_checkswith the single contextci-required. Never list individual jobs. -
CodeQL default setup — Settings → Advanced Security → CodeQL → Default. Verified active for Python and JavaScript/TypeScript on 2026-07-24. Keep the settings-managed scanner enabled; GitHub rejects a competing advanced-configuration workflow while default setup is active
[VF]. -
Retention stays 90 days (public max). Private vulnerability reporting: verified enabled (API read, 2026-07-21) — see SECURITY.md. Additional read-back: Discussions and all six default categories are enabled; immutable releases are enabled; and the protected
releaseenvironment requires the owner reviewer with self-review permitted. Its PyPI path uses OIDC and has no environment secret. The launch welcome post was created through one exact, payload-validated operation. The unrelatedsandboxandbenchmarkenvironments remain absent and must be created before their first human-approved use.
Site deploys (not a GitHub Actions workflow):
- Repository policy permits Vercel auto-deployment from
mainonly (ADR-0038, superseding ADR-0027’s manual-upload mechanic). The rootvercel.jsonoverrides the project’s stale Python autodetection, installs the lockedsite/pnpm graph, invokesscripts/vercel-build.sh, servessite/dist, and carries the response-header contract. Its branch map disables every automatic deployment exceptmain; its ignore command stops other refs before dependency installation, and the wrapper independently refuses a non-production environment, non-mainref, or non-full commit SHA. The install and build commands entersite/before invoking Corepack, so the checked-inpackageManagerpin selects pnpm rather than a platform default. Rootvercel.jsonchanges select the complete site CI slice. This path deploys only the marketing/docs site—it cannot merge code or publish Python packages, GitHub Releases, benchmarks, or provider activity. Details in site/README.md. Vercel does not wait forci-required; the protected pull-request path supplies the review boundary before a commit reachesmain. Production read-back 2026-07-24: main-only Git deployment is active, the production deployment is ready athttps://irrevon.vercel.app/, and/version.jsonreports the exact currentmaincommit andproductionenvironment. Each launch merge is verified against that document again.
Before the first M4/M7 dispatch:
- Environments — Settings → Environments → create
sandboxandbenchmark, each with Required reviewers = owner, Prevent self-review UNCHECKED (checking it deadlocks a solo owner), deployment branches =mainonly; put sandbox-only secrets there — no repo-level secrets, ever[DD]. Create the environments BEFORE any dispatch: a run referencing a missing environment silently auto-creates it unprotected. Caveat[VF]: converting the repo back to private silently drops environment protection and secrets — re-privatization must re-trigger this checklist.
Troubleshooting
- Skipped counts as passing. A required status check whose job was skipped satisfies
the ruleset
[VF]. That is why individual jobs are never required andci-requiredfails on skips that contradict change detection. If a code PR shows green with skipped code jobs, the change filter is broken — fixchangesin ci.yml, never the aggregator. - Never-reported stays pending. A required check that never reports blocks the PR
forever
[VF]. So: no workflow-levelpaths:filters on ci.yml, and never add the ruleset requirement before the workflow has reported once on a real PR. - Adversarial-filter residual. A PR can edit the change filter and code in one diff;
the aggregator then sees “legit” skips. The aggregator defends against accidental skips;
human review of any workflow diff is the control against adversarial ones
[EI]. - Missing secret = empty string. An absent Actions secret does not fail a job; it
evaluates to
"". The fail-closed sandbox/benchmark skeletons deliberately read no secret today. Their activation changes must add an explicit non-empty guard before each credentialed step; keep that pattern for every future secret. - Nightly silently auto-disables after 60 days without repo activity (public repos)
[VF]. Quarterly sweep: confirm the nightly workflow is still enabled, re-verify the action pin table and (at M3+) the Postgres digest, which Dependabot does not bump. - Docs gate fails on a ratification PR. The frozen gate
(
scripts/check-frozen.sh) treatsdocs/review-queue.mdas append-only, with ONE exception — a human ratification integration: deletions pass only when the same PR range carries the master-doc re-pin (docs/master-doc.md+scripts/master-doc.sha256changed together, the human-only amendment act) AND the queue’s added lines record the ratifying amendment (AM-<n>…RATIFIED). Deletions without both pieces of evidence still fail — never exempt the file or skip the gate on PRs. The scenario matrix lives intests/scripts/test_check_frozen.py. - Push protection blocks a push: rotate/remove the secret and recommit — never bypass (hook- and policy-blocked).
- Nightly external-link failures are often flakes: follow the triage protocol in the auto-filed issue; close only after a green manual re-run.
Design notes — historical cuts and current reconciliation [DD]
Reconciliation rule: the public-repo review findings supersede the earlier private-repo design.
- Cut
gitleaks/gitleaks-actionjob (from the earlier security.yml design): redundant with themake secretsparity scan already insidemake check; the CI design review itself named the parity scan as the keeper if the redundancy chafed. - Cut
zizmor-actionand separatesecurity.yml/docs-check.ymlworkflows: replaced by the pinned zizmor/actionlint binaries from the one bootstrap pin table (offline inmake check, online inworkflow-security/nightly) and by the single always-running ci.yml — required-check semantics on a public repo make the aggregator pattern, not separate path-filtered workflows, the correct shape. Two fewer third-party actions. - Cut the workflow-level
on.pathsfilters (ci.yml): they break required checks (pending-forever trap); replaced by thechangesjob + aggregator. - Postgres service-container cut was temporary: once the M3 engine and integration suite landed, the digest-pinned Compose service and active T2/T3 jobs landed with them. The current workflow and tier tables above are authoritative.
- Cut the GitHub Pro pairing recommendation (CI design review): rulesets/environments are free on public repos; Pro buys nothing while public.
- Community and ownership controls have landed: the PR template, repository-wide CODEOWNERS baseline with path-specific rules, README badges, and Scorecard workflow are active. Nightly failure labeling is deliberately limited to the fixed owner-created label and title-deduplicated failure issue; there is no general comment-driven auto-labeling bot.
- Deferred: Cursor CLI diagnostic workflow (CI design review): its guardrails are designed, but
it requires a
CURSOR_API_KEYsecret — which violates the no-repo-level-secrets rule until an owner-created environment holds it — and the vendor installer has no published stable checksum to pin. Revisit when both close. - Nightly link parity:
make links-onlineshares the offline gate’s source exclusions and Vite public-directory remaps, then adds external requests. Exact bot-blocked primary sources may be excluded individually; broad 4xx acceptance is forbidden. - Wheel-smoke network: Postgres remains host-loopback-only. The Node-less smoke
container joins the compose network and reaches the
ledger-db-testservice by its internal DNS name, which works on Linux without widening the host port binding. - Nightly-failure body placement: lives at
.github/nightly-failure-body.md, outside the public issue chooser. The workflow fills placeholders from trusted context only and deduplicates by the fixed issue title.