Skip to content

Branch protection & ruleset (main) — setup

Main is protected by branch protection/ruleset settings. This doc records the current required status checks and gives minimal SOTA 2026 settings for keeping that protection in sync (UI or gh CLI).

Why: Without protection, anyone with push access can push directly to main, bypassing CI, reviews, and status checks.


Minimal settings (SOTA 2026)

  • Require a pull request before merging (no direct pushes to main).
  • Required approvals: at least 1–2.
  • Required status checks: CI, host-capability-check, review-record-check, and the commit status lane-check/proof (pinned to the GitHub Actions app; observed live on 2026-07-27, issue #1869 migration). The lane-check Actions job still runs and reports, but is informational: the proof status is posted on the exact PR head and refreshes on a same-head delegated proof, which the check-run cannot. See "Required checks: when each is needed" for rationale; optional: Smoke Install, assay-action-contract-tests, MCP Security, Kernel Matrix.
  • Require branch to be up to date before merging.
  • Restrict force-push and branch deletion (do not allow force-push to main; restrict who can delete the branch).

Extra (recommended):

  • Require review from Code Owners for:
  • .github/workflows/**
  • release.yml (if separate)
  • assay-action/**
  • infra/**

Ensure .github/CODEOWNERS exists and lists the right owners (see repo root).


Option A: GitHub UI

  1. Settings → Code and automation → Rules → Rulesets (or Branches for classic branch protection).
  2. Create rule (or “Add rule” / “Add branch protection rule”).
  3. Target: Branch rule, branch name pattern main.
  4. Enable:
  5. Require a pull request before merging.
  6. Require approvals (set number, e.g. 1).
  7. Require status checks: add CI, host-capability-check, review-record-check, and lane-check/proof (or the exact context names your workflows expose; check Settings → Branches → Branch protection or the Ruleset UI for the list of available checks).
  8. Require branches to be up to date before merging.
  9. Do not allow force pushes / restrict force pushes.
  10. Restrict who can push to matching branches (optional; or leave as default).
  11. If using Code Owners: enable “Require a review from Code Owners” and ensure CODEOWNERS covers the paths above.

Note: Exact status check names come from your workflow name and job name (or job id). After the first run on a PR, they appear in the “Status checks that are required” dropdown.


Canonical context names (use these exactly; they match name: in the workflow files):

Context Workflow file
CI .github/workflows/ci.yml
lane-check/proof commit status posted by scripts/ci/assay_runner_lane_check.py (runs in .github/workflows/assay-runner-lane-check.yml)
lane-check .github/workflows/assay-runner-lane-check.yml (informational since the #1869 migration)
host-capability-check .github/workflows/host-capability-check.yml
review-record-check .github/workflows/review-record-check.yml
Smoke Install (E2E) .github/workflows/smoke-install.yml
assay-action-contract-tests .github/workflows/action-tests.yml
MCP Security (Assay) .github/workflows/assay-security.yml
Kernel Matrix CI .github/workflows/kernel-matrix.yml
Assay Gate .github/workflows/assay.yml

Use CI (not CIExpected or any other variant). No workflow in this repo reports a check named CIExpected.


Required checks: when each is needed

Check What it does Dependabot / deps-only PRs Other PRs (features, workflows, action)
CI Build, test, clippy, cargo-deny, cargo-audit, eBPF smoke Essential — new deps must not break build or tests. Essential — same.
lane-check/proof Commit status confirming runner-sensitive PRs carry the delegated proof for the exact head. Required but normally quick/no-op unless the classifier says proof is needed. Required; becomes load-bearing for runner/evidence-sensitive changes. A same-head delegated proof refreshes it without a manual rerun.
host-capability-check Confirms whether the PR requires host-capability proof before privileged runner evidence is trusted. Required but normally quick/no-op for ordinary dependency updates. Required; becomes load-bearing for host/kernel/runner capability-sensitive changes.
review-record-check Requires a public non-building review record bound to the current PR head. Required; dependency automation still needs an independent exact-head record. Required; a later push invalidates the prior record until a new review is posted.
Smoke Install (E2E) Build from source, run assay, JUnit Redundant with CI (CI already builds and tests). Useful — verifies install path.
assay-action-contract-tests Tests GitHub Action in assay-action/ Not needed — Cargo.toml/Cargo.lock don't touch the action. Essential if PR touches assay-action/ or workflows.
MCP Security (Assay) Install assay, run validate with demo config Redundant with CI for deps-only (CI validates the binary). Useful — sanity check for security workflow.
Kernel Matrix CI eBPF tests on self-hosted runner Not needed — kernel-matrix paths exclude Cargo.toml/Cargo.lock. Essential if PR touches eBPF/Monitor/evidence.
Adequacy drift gate Re-derives every published mutation-adequacy number and fails when a document and the measurement disagree Not needed — relevance is derived from each manifest's own declared sources, which a deps bump does not touch. Load-bearing when a PR touches a declared corpus source, its vectors, its manifest, or conformance/INDEX.md / ERRATA.md. Deliberately not required — see below.

Why the adequacy drift gate is not required, and what would change that

It fits the criterion the other three meet: universal, quick when irrelevant, load-bearing when relevant. It is kept out for a property none of them has — it clones an external repository. corpus-adequacy is a sibling checkout, deliberately not vendored, and the lane fetches it at the commit each manifest pins. Making the gate required would couple main to a third party's availability, and no current required check does that.

That is a property of the instrument, not of the measurement, so it is fixable. Promote this gate to required once the tool no longer needs a network fetch at gate time — a pinned local copy verified against tool_pin. That is a cache, not a second implementation, so it does not conflict with the reason the tool is unvendored, which is drift between two implementations rather than storage.

Until then the gate runs, reports on every PR, and is a review obligation like Smoke Install and MCP Security: if a PR moves a published number, its green tick is the thing to look for before merging.

Current recommendation: Keep CI, lane-check/proof, host-capability-check, and review-record-check required. CI is the universal build/security gate; lane-check/proof and host-capability-check are quick for ordinary PRs but preserve the runner/evidence proof boundary when a PR touches sensitive paths. Smoke Install, assay-action-contract-tests, and MCP Security still run and appear on the PR; they are not required to merge. If you merge changes to assay-action/ or workflows, ensure contract tests and MCP Security have passed before merging (e.g. via review policy or by re-adding them to required checks when needed).


Option B: gh CLI (branch protection)

Classic branch protection via API (no rulesets). The API expects a JSON body with real booleans; form fields (-f) can send strings and cause 422 "Validation Failed". Use --input - with a JSON payload below.

User-owned repos: Do not send restrictions with users/teams or dismissal_restrictions with users/teams (only org repos support those). Use restrictions: null and omit or empty dismissal_restrictions.

# Replace OWNER/REPO (e.g. Rul1an/assay) and status check contexts to match your workflow job names.
gh api repos/OWNER/REPO/branches/main/protection -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  --input - <<'JSON'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["CI", "host-capability-check", "lane-check/proof", "review-record-check"]
  },
  "enforce_admins": false,
  "required_pull_request_reviews": {
    "dismiss_stale_reviews": false,
    "require_code_owner_reviews": true,
    "required_approving_review_count": 1
  },
  "restrictions": null,
  "allow_force_pushes": false,
  "allow_deletions": false
}
JSON

Use required_approving_review_count: 2 in the JSON if you want two approvals. The listed contexts match the currently required live protection. Add e.g. Kernel Matrix CI, Smoke Install (E2E), action tests, or MCP Security only if those checks must become required rather than visible/advisory.

To inspect current protection:

gh api repos/OWNER/REPO/branches/main/protection

To inspect only the required status checks subresource (what the reconciliation monitor reads):

gh api repos/OWNER/REPO/branches/main/protection/required_status_checks

To remove protection (use with care):

gh api repos/OWNER/REPO/branches/main/protection -X DELETE

Live reconciliation monitor (required contexts)

.github/rulesets/main-required-ci-contexts.json is the sole machine-readable expected set. Classic branch protection on the default branch is what actually enforces merges. The scheduled workflow .github/workflows/required-context-reconciliation.yml compares those two on context names and strict only (it ignores app_id / integration_id, review settings, force-push, and deletion rules).

Prerequisite (owner action): create a fine-grained personal access token for this repository only with Administration: Read-only, and store it as the repository secret BRANCH_PROTECTION_READ_TOKEN. The workflow does not fall back to github.token. Until that secret exists, scheduled and manual runs fail closed at preflight with an explicit missing-secret error; they do not silently skip reconciliation.

Credential expiry: 2026-10-12. Fine-grained tokens expire, and an expired one fails the same way a missing scope does: gh: Bad credentials (HTTP 401) on the first API call, on a job that is not a required check, so nothing blocks and the drift monitor is simply blind. That happened between 2026-09-10 and 2026-09-12 (#2938). Before the date above, regenerate the token with the same scope (this repository only, Administration: Read-only), update the repository secret under the same name, and record the new expiry here. Verify with a manual run: the Fetch live required status checks and Reconcile ruleset against live protection steps must both pass, not only the preflight.

The monitor is not a required status check. It runs on a schedule (every six hours) and workflow_dispatch on the default branch only. A red run means live protection drifted from the checked-in artifact, or the read failed (auth, API, or malformed evidence).

Local check against a saved API response (stdin-only; the checker does not accept a path argument):

gh api repos/OWNER/REPO/branches/main/protection/required_status_checks \
  > /tmp/live-required-status-checks.json
python3 scripts/ci/check-required-contexts.py --live-response \
  < /tmp/live-required-status-checks.json

Exit codes for --live-response: 0 match, 1 semantic drift, 2 unreadable or missing live evidence.


Environments (release / publish gates)

For human-in-the-loop on release and publish:

  1. Settings → Environments → create (if missing):
  2. release — for the “Create Release” job and/or release workflow.
  3. crates — for publish to crates.io.
  4. pypi — already exists; use for publish to PyPI.
  5. For each environment, add Required reviewers (e.g. 1–2 maintainers).
  6. In release.yml, set environment: release (or crates / pypi) on the corresponding jobs so that runs wait for approval before executing.

dependabot-maintenance is a different kind of environment: it holds the Dependabot lane's GitHub App credentials, not a release approval. Keep it restricted to deployment branch main, with no required reviewers or wait timer (the scheduled maintenance run would stall) and no custom deployment protection rules (the job uses deployment: false, which refuses them). It holds variables DEPENDABOT_APP_CLIENT_ID and DEPENDABOT_APP_SLUG and secret DEPENDABOT_APP_PRIVATE_KEY; the App (assay-dependabot-lane) is installed on this repository only with Contents and Pull requests read and write. See docs/reference/runner/dependabot-lane-flow.md for why and for key rotation.

See docs/REVIEWER-PACK.md (sectie 3, “Environments & approvals”) and the current release.yml for which jobs already use environment:.


Checklist

  • Branch protection or ruleset on main with: require PR, approvals, status checks, up to date, no force-push.
  • Required status checks: CI, host-capability-check, lane-check/proof, and review-record-check (see "Required checks: when each is needed" above; add Smoke Install / contract tests / MCP Security / Kernel Matrix only when stricter gates are intentionally needed).
  • CODEOWNERS in place; “Require review from Code Owners” enabled.
  • Environments release / crates / pypi configured with required reviewers; release workflow uses environment: on publish jobs.

Troubleshooting: "CIExpected — Waiting for status to be reported"

If a PR shows a required check CIExpected that never completes, branch protection is requiring a context that no workflow reports.

Fix: In Settings → Branches → Branch protection rule for main, under "Require status checks", remove CIExpected and add CI (from .github/workflows/ci.yml) plus the current required live contexts host-capability-check, lane-check/proof, and review-record-check. Save.

Via API: Inspect with gh api repos/OWNER/REPO/branches/main/protection. Ensure required_status_checks.contexts contains CI, host-capability-check, lane-check/proof, and review-record-check, and does not contain CIExpected or the retired bare lane-check. Re-apply using the JSON in Option B above with "contexts": ["CI", "host-capability-check", "lane-check/proof", "review-record-check"].