Repository navigation
Description
A repo protected more strictly than the policy is reported as drift, and its fix would lower protection
ion/analyze/policy.py:84-93 compares each branch-protection field with !=:
elif bp.get("require_pr") and rule.get("requiredApprovingReviewCount", 0) != bp.get("required_approvals", 0):
diffs.append("required_approvals")
...
if bool(rule.get("isAdminEnforced")) != bool(bp.get("enforce_admins")):
diffs.append("enforce_admins")The default policy is require_pr: true, required_approvals: 0, enforce_admins: false. Any repo that requires one or more approvals, or enforces rules for admins, is therefore drift. The policy_fix that follows carries the policy's settings, which would weaken that repo's protection. lowers_protection(current_rule, fix.settings) returns ['lowers the required approval count'], or ['stops enforcing rules for admins'].
What happens next depends on the action level:
fix(as in the SPEC examplepolicy_actions: {branch_protection: fix, ...}): the fix is allowlisted, and the preflight sees a lowering.forbid_protection_loweringthen aborts the whole--applyrun before any write. Every unrelated approved proposal is blocked on every run, for as long as one repo is stricter than the policy.issue: ion files a proposal asking maintainers to approve weakening that repo's protection. If someone approves it, the org-wide abort above follows.
The same path is reachable from a repo's own .github/ion.yml asking for weaker settings, for example policy: {branch_protection: {require_pr: false}}. PR #61 validates the types in that block but leaves this alone.
Repro, written against the test fixtures: make alpha-core's rule requiredApprovingReviewCount: 2 and set policy_actions: {branch_protection: fix}. Approve an unrelated archive of old-tool, then run_execute(apply=True). The result is circuit breaker: a branch-protection change would lower protection: lowers the required approval count, and old-tool is not archived.
Suggested fix, which needs one decision from a maintainer (that branch-protection policy is a minimum):
- Report drift only when the rule is weaker than the policy: fewer approvals, admins not enforced while the policy enforces them, force pushes or deletions allowed while the policy forbids them.
- Build the fix settings as the per-field stricter of the current rule and the policy, so a fix can only raise protection.
- In
gather_candidates, never propose abranch_protectionfix for whichlowers_protection(current_rule, settings)is non-empty. Record it as a finding instead. - Optionally, have the preflight drop just the offending fix rather than abort the run, so one repo cannot stop execution for the whole org.
Related, not the same: #3 (the PUT drops required status checks) and #12 (the first matching rule instead of the merged rules).
Automated posting by agentic team with human oversight.
Implementation draft awaiting independent code review: #114
Activity
ahrazzle commentedon Oct 21, 2026
Status after the 2026-10-21 triage pass over all open issues: narrowed by #50, core defect still open.
#50 narrowed what a repo override may do to maintainers and policy actions. It did not touch drift detection.
ion/analyze/policy.pystill compares each protection field with!=, so a repo protected more strictly than the policy is still reported as drift. The generated fix still carries the raw policy values, so applying it would lower that repo's protection. Thelowers_protectionguard and the breaker inion/writes.pythen abort the run instead.#61 is adjacent (it validates the types of policy overrides) but explicitly does not fix this. The remaining work is directional drift: stricter than policy is compliant, and a fix may only ever raise protection to the policy level, never lower a stricter setting.