Skip to content

Make CI enforcement trustworthy before the rules registry - #3887

Draft
anth-volk wants to merge 8 commits into
masterfrom
phase-0-enforcement
Draft

anth-volk wants to merge 8 commits into
masterfrom
phase-0-enforcement

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3886

Summary

Phase 0 of the rules workstream: make CI enforcement trustworthy before the rules registry (#3884, kept as a draft until this merges). The sim counterpart is PolicyEngine/policyengine-sim-api#754.

  • CODEOWNERS (step 0.2). A deliberately narrow file covering only the rule machinery: rules/, scripts/rules/, scripts/guards/, scripts/run_quality_guards.py and CODEOWNERS itself, owned by @PolicyEngine/core-developers. pyproject.toml and workflow files are not owned, so dependency bots and everyday CI edits aren't blocked. The paths for the registry take effect once Add a rules registry that makes every enforced and unenforced rule visible #3884 creates them.
  • Ruff pin (step 0.3). The lint jobs in pr.yml and push.yml ran pip install ruff>=0.9.0 unquoted, so the shell read > as a redirect and CI installed the latest ruff. They now run .github/scripts/install_locked_ruff.sh, which installs the version pinned in uv.lock (currently 0.15.12) and prints it. Following the workstream's CI rule, the multi-line logic lives in a tested script, and the workflow step is a single line. [tool.ruff.lint] select = ["E4", "E7", "E9", "F"] makes ruff's current defaults explicit. No rules are added or removed. Stricter ruff comes in separate PRs right after the registry lands.
  • CRUD boundary discovery (step 0.5). test_data_crud_boundaries.py discovers modules by filename instead of listing them, so households and user_households (12 modules) are checked for the first time. Discovery fails at collection if a pattern matches nothing, because pytest silently skips an empty parameter list. A new test requires every connector module to be a recognised create, read, update or delete module.
  • PR conventions (step 0.6) and the no-bypass rule. github-prs.md now requires a tracking issue and Fixes #ISSUE_NUMBER on the first line of the PR description, and a new PR template starts with that line. Agents must never bypass branch rules (for example gh pr merge --admin), even with admin credentials; AGENTS.md points to both.
  • Full CI only at the bottom of a stack (step 0.7). GitHub runs pull_request workflows for every PR in a native stack as if each targeted master, so a nine-PR stack ran this whole workflow nine times.
    • Every pr.yml job without needs now runs only when the PR isn't stacked, or is the lowest unmerged one (github.event.pull_request.stack.base.ref == github.event.pull_request.base.ref). Jobs that need them are skipped with them.
    • A new CI complete job needs every other job and runs with if: always(). A skipped job would count as passing a required check, so the gate fails on stacked PRs above the bottom, and otherwise passes only when every job passed. Stacks therefore merge bottom-up, and nothing merges untested.
    • The logic lives in .github/scripts/check_ci_complete.sh. tests/unit/test_ci_complete_gate.py tests it and checks that every job is needed by the gate and carries the condition. github-prs.md documents it, including what to do if no run starts when a PR reaches the bottom.
    • test_alembic_workflows.py used to forbid any if: on the Alembic v2 and cross-database jobs, so they could never be skipped. It now allows exactly the stacked-PR condition and nothing else.
    • Environment variables: the gate step reads JOB_RESULTS (toJSON(needs)), PULL_REQUEST_BASE_REF and STACK_BASE_REF. All three come from GitHub Actions contexts and are set in that step's env:. No secrets are involved.

Step 0.1 is not in this PR. It makes CI complete the single required check and turns on code-owner review in the main ruleset. That's a ruleset change, applied separately after merge.

Testing

  • pytest tests/unit/v2/test_data_crud_boundaries.py: 40 tests pass, covering 39 discovered modules (previously 27) plus the classification test.
  • Negative checks, each reverted afterwards:
    • Adding import sqlalchemy to services/v2/households/validators.py makes the households case fail. Before this change, nothing checked that module.
    • Adding a connector named writes.py makes the classification test fail.
  • pytest tests/unit/test_install_locked_ruff_script.py: 4 tests pass, using fake pip and ruff. They cover the locked version, the repository's own lock as the default, a missing pin (fails without installing) and a failing pip. The script also passes shellcheck, and a real run against the repo's lock in a throwaway venv installs ruff 0.15.12.
  • Ruff:
    • The install script reads 0.15.12 from the lock.
    • With that version, ruff format --check . and ruff check . both pass.
    • ruff check . with ruff 0.16.8 and the explicit select also passes. With 0.16.8's own defaults, it would report 867 errors.
  • python scripts/run_quality_guards.py and tests/unit/test_alembic_skill.py pass.
  • The make test selection passes: 2,402 tests.
  • pytest tests/unit/test_ci_complete_gate.py: 8 tests pass. They cover the workflow shape, a non-stacked PR, the bottom of a stack, a PR higher in a stack, failed, cancelled and skipped jobs, and missing input. The gate script passes shellcheck.
  • actionlint reports nothing on the changed workflows.
  • GitHub's CODEOWNERS validation returns no errors for this branch.

🤖 Generated with Claude Code

anth-volk and others added 5 commits October 12, 2026 22:41
Changes to the rules registry, guard scripts and CODEOWNERS itself need
review from a core developer other than the author. Deliberately narrow:
pyproject.toml and workflow files are not owned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lint jobs ran `pip install ruff>=0.9.0` unquoted, so the shell read
`>` as a redirect and CI installed the latest ruff. Install the version
pinned in uv.lock instead, and select ruff's current default rules
explicitly so an upgrade cannot change which rules apply. No rules are
added or removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four boundary checks listed their modules by hand, so the households
and user_households packages (12 modules) were never checked. Discover the
modules by filename instead, fail at collection if a pattern matches
nothing, and require every connector module to be a recognised create,
read, update or delete module so a new name cannot escape the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every PR needs a tracking issue and `Fixes #ISSUE_NUMBER` as the first
line of its description; a PR template starts with that line. Agents must
never bypass branch rules, even with admin credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Multi-line workflow run blocks become tested shell scripts. The lint jobs
now run .github/scripts/install_locked_ruff.sh, which reads ruff's version
from uv.lock and installs it, and tests cover the lock lookup, the default
lock, a missing pin and a failing pip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.56%. Comparing base (21ddf34) to head (8cedb09).

Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3887       +/-   ##
===========================================
+ Coverage   45.22%   87.56%   +42.33%     
===========================================
  Files         183      198       +15     
  Lines       11073    12211     +1138     
  Branches     1905     2159      +254     
===========================================
+ Hits         5008    10692     +5684     
+ Misses       5644      922     -4722     
- Partials      421      597      +176     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

GitHub runs pull_request workflows for every PR in a stack as if each
targeted the trunk, so a nine-PR stack ran the whole workflow nine times.
Every job without needs now carries the stack condition, and the jobs that
need them are skipped with them.

A new CI complete job needs every other job and is meant to be the single
required check. A skipped job counts as passing, so the gate fails on any
stacked PR above the lowest unmerged one, and otherwise passes only when
every job passed. Stacks therefore merge bottom-up, and nothing merges
untested.

check_ci_complete.sh holds the gate logic, and
tests/unit/test_ci_complete_gate.py tests it and checks that every job is
needed by the gate and carries the condition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_pr_always_runs_reusable_alembic_and_v2_integration_checks forbade
any if: on the Alembic v2 and cross-database jobs, so they could never be
skipped. They now carry the stacked-PR condition like every other job:
they still run on every PR that isn't stacked and on the lowest unmerged
PR of a stack, and CI complete, which needs them, fails on the rest. The
test now allows exactly that condition and nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk requested a deployment to staging October 13, 2026 15:35 — with GitHub Actions Pending

This branch is pending to be deployed

1 pending deployment
staging — 299dba8b Pending Oct 13, 2026 by anth-volk via Ensure PolicyEngine bundle is supported by simulation API #4776
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make CI enforcement trustworthy before the rules registry

1 participant