Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@

## Proposed

- Compare GitHub repository identities case-insensitively at owned-head and
compare-ref boundaries so canonical casing drift cannot misroute an
organization-owned branch through external-fork restrictions.

- Reject Draft pull requests again at both direct-merge and auto-merge
mutation functions. This defense-in-depth boundary prevents a stale caller
decision from reaching guarded GitHub mutations after PR lifecycle changes.
Expand Down
33 changes: 33 additions & 0 deletions docs/doctoring/case-insensitive-owned-head-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Case-insensitive owned-head identity

Decision date: **2026-09-07**

## Problem

GitHub treats repository names case-insensitively, but the scheduler compared
`headRepository.nameWithOwner` with the configured target using exact string
equality. Casing drift could classify an organization-owned branch as an
external fork and build the wrong compare ref.

## Decision

Case-fold both repository identifiers in `same_repository_head` and
`compare_ref_for_pr_head`. No permission or ownership inference changes; only
names GitHub already considers identical are unified.

## Failure scenes

- `Owner/Repo` versus `owner/repo`: classify as the same repository.
- A genuinely different repository: retain external-head handling.
- Missing head repository metadata: preserve the existing compare fallback and
fail-closed mutation eligibility.

## Evidence and follow-up

RED commit: `4fb514db54e6210fc0606f0dfa8d9033f3e1f6f5`.
Fresh exact-head hosted checks and independent review remain required.

## Reference

GitHub. (2026). *REST API endpoints for repositories*.
https://docs.github.com/en/rest/repos/repos
12 changes: 12 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -3429,3 +3429,15 @@ same name in another file can carry the opposite safety property.**
- **Evidence:** RED commit
`2d140a84203a0df0cb86cd6b6ab31fc37bbdbda2`; fresh exact-head hosted checks
remain required before integration.


### Case-insensitive owned-head identity

- **Status:** Proposed
- **Owner:** `ContextualWisdomLab/.github`
- **Problem:** Same-repository head and compare-ref checks used case-sensitive
repository strings even though GitHub repository identity is case-insensitive.
- **Action:** Case-fold both sides at the two owned-head routing boundaries.
- **Evidence:** RED commit
`4fb514db54e6210fc0606f0dfa8d9033f3e1f6f5`; fresh exact-head hosted checks
remain required before integration.
4 changes: 2 additions & 2 deletions scripts/ci/pr_review_merge_scheduler_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,7 +1552,7 @@ def compare_ref_for_pr_head(repo: str, pr: dict[str, Any]) -> str:
"""Return the compare-API head ref for a PR branch."""
head_ref = pr.get("headRefName") or "HEAD"
head_repo = (pr.get("headRepository") or {}).get("nameWithOwner")
if not head_repo or head_repo == repo:
if not head_repo or head_repo.casefold() == repo.casefold():
return head_ref
head_owner, _ = split_repo(head_repo)
return f"{head_owner}:{head_ref}"
Expand Down Expand Up @@ -2965,7 +2965,7 @@ def post_update_branch_followup(
def same_repository_head(repo: str, pr: dict[str, Any]) -> bool:
"""Return whether the PR head branch belongs to the repository being scanned."""
head_repo = (pr.get("headRepository") or {}).get("nameWithOwner")
return head_repo == repo
return bool(head_repo) and head_repo.casefold() == repo.casefold()


def can_update_pr_head(repo: str, pr: dict[str, Any]) -> bool:
Expand Down
11 changes: 11 additions & 0 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10908,3 +10908,14 @@ def test_draft_pr_cannot_reach_merge_mutations(monkeypatch):
mutation("owner/repo", draft_pr, dry_run=False)

assert calls == []


def test_same_repository_identity_is_case_insensitive():
"""GitHub casing drift cannot route an owned branch through the fork path."""
pull_request = make_pr(
headRefName="feature",
headRepository={"nameWithOwner": "Owner/Repo"},
)

assert sched.same_repository_head("owner/repo", pull_request)
assert sched.compare_ref_for_pr_head("owner/repo", pull_request) == "feature"
Loading