fix(github): match repository bindings case-insensitively (#7982)

GitHub owner and repository names are case-insensitive, but the agent
registry indexed and looked up bindings by the raw repo string. A
binding for acme/widget never matched webhooks for Acme/Widget, so the
event was dropped without an error. Lowercase the repo on both sides.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
aduoge 2026-11-08 13:45:59 +08:00 committed by GitHub
parent 5b1bee3d5a
commit 91f20a782f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 2 deletions

View File

@ -95,7 +95,7 @@ def _build_index(agents: list[tuple[str, AgentConfig]]) -> _Registry:
# it is here, so this branch is unreachable. Keep the
# guard for type-checker happiness.
continue
index.setdefault((binding.repo, event), []).append(GitHubAgentMatch(user_id=user_id, agent=cfg, trigger=resolved))
index.setdefault((binding.repo.lower(), event), []).append(GitHubAgentMatch(user_id=user_id, agent=cfg, trigger=resolved))
return index
@ -145,4 +145,4 @@ def lookup_agents(
and the pre-resolved trigger config for this specific event, so the
caller does not need to walk the agent's ``bindings`` again.
"""
return registry.get((repo, event), [])
return registry.get((repo.lower(), event), [])

View File

@ -47,6 +47,34 @@ async def _drain(bus: MessageBus) -> list[InboundMessage]:
return out
@pytest.mark.asyncio
@pytest.mark.parametrize("configured_repo, payload_repo", [("Acme/Widget", "acme/widget"), ("acme/widget", "Acme/Widget")])
async def test_fanout_matches_repository_case_insensitively(base_dir: Path, configured_repo: str, payload_repo: str) -> None:
"""GitHub repository names identify the same repository regardless of case."""
_write_agent(
base_dir,
"default",
"reviewer",
{"name": "reviewer", "github": {"bindings": [{"repo": configured_repo, "triggers": {"issues": {"actions": ["opened"]}}}]}},
)
bus = MessageBus()
result = await fanout_event(
bus,
"issues",
"delivery-mixed-case",
{
"action": "opened",
"issue": {"number": 7, "user": {"login": "alice"}, "title": "Bug", "body": "Details"},
"repository": {"full_name": payload_repo},
"sender": {"login": "alice"},
},
)
assert result["fired_agents"] == ["reviewer"]
messages = await _drain(bus)
assert len(messages) == 1
assert messages[0].chat_id == payload_repo
@pytest.mark.asyncio
@pytest.mark.parametrize("event, object_key", [("issues", "issue"), ("pull_request", "pull_request")])
@pytest.mark.parametrize("author, sender, should_fire", [("owner", "other", False), ("other", "owner", True)])