From 91f20a782f66fcc0cf779138fd576ec57040c9b0 Mon Sep 17 00:00:00 2001 From: aduoge <53458900+aduoge@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:45:59 +0800 Subject: [PATCH] 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 --- backend/app/gateway/github/registry.py | 4 ++-- backend/tests/test_github_dispatcher.py | 28 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/app/gateway/github/registry.py b/backend/app/gateway/github/registry.py index 1dd718924f..98d97e3478 100644 --- a/backend/app/gateway/github/registry.py +++ b/backend/app/gateway/github/registry.py @@ -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), []) diff --git a/backend/tests/test_github_dispatcher.py b/backend/tests/test_github_dispatcher.py index 3e24c147ca..42291dbb97 100644 --- a/backend/tests/test_github_dispatcher.py +++ b/backend/tests/test_github_dispatcher.py @@ -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)])