Skip to content

Commit 4ca8aba

Browse files
fix: gate hosted webhook actions
Signed-off-by: Timothy Wayne Gregg <5861166+romgenie@users.noreply.github.com>
1 parent 3506451 commit 4ca8aba

2 files changed

Lines changed: 124 additions & 5 deletions

File tree

deploy/coven-github/coven_github_adapter.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ def trigger_enabled(policy, trigger):
290290
return trigger in set(enabled or [])
291291

292292

293+
def event_trigger_key(event_name, payload):
294+
action = str(payload.get("action") or "").strip()
295+
return "{}.{}".format(event_name, action) if action else event_name
296+
297+
293298
def build_task_from_event(event_name, delivery_id, payload, policy):
294299
repository = payload.get("repository") or {}
295300
installation = payload.get("installation") or {}
@@ -315,10 +320,12 @@ def build_task_from_event(event_name, delivery_id, payload, policy):
315320
if event_name == "issue_comment":
316321
issue = payload.get("issue") or {}
317322
comment = payload.get("comment") or {}
323+
if payload.get("action") != "created":
324+
return ignored(base, "unsupported_issue_comment_action")
318325
if not mentioned(comment.get("body"), policy):
319326
return ignored(base, "issue_comment_without_mention")
320-
if not trigger_enabled(policy, "issue_mention"):
321-
return ignored(base, "issue_mention_not_enabled")
327+
if not trigger_enabled(policy, "issue_comment.created"):
328+
return ignored(base, "issue_comment_not_enabled")
322329
if issue.get("pull_request"):
323330
base.update(
324331
{
@@ -352,9 +359,11 @@ def build_task_from_event(event_name, delivery_id, payload, policy):
352359
if event_name == "pull_request_review_comment":
353360
comment = payload.get("comment") or {}
354361
pull_request = payload.get("pull_request") or {}
362+
if payload.get("action") != "created":
363+
return ignored(base, "unsupported_pr_review_comment_action")
355364
if not mentioned(comment.get("body"), policy):
356365
return ignored(base, "pr_review_comment_without_mention")
357-
if not trigger_enabled(policy, "pr_review_comment"):
366+
if not trigger_enabled(policy, "pull_request_review_comment.created"):
358367
return ignored(base, "pr_review_comment_not_enabled")
359368
base.update(
360369
{
@@ -380,13 +389,13 @@ def build_task_from_event(event_name, delivery_id, payload, policy):
380389
action = payload.get("action")
381390
if action not in ("assigned", "labeled"):
382391
return ignored(base, "unsupported_issue_action")
383-
if action == "assigned" and not trigger_enabled(policy, "issue_assigned"):
392+
if action == "assigned" and not trigger_enabled(policy, "issues.assigned"):
384393
return ignored(base, "issue_assigned_not_enabled")
385394
if action == "assigned" and not issue_assigned_to_bot(issue, policy):
386395
return ignored(base, "issue_assigned_to_unmanaged_user")
387396
if action == "labeled" and not labels_include_trigger(issue.get("labels"), policy):
388397
return ignored(base, "issue_label_not_enabled")
389-
if action == "labeled" and not trigger_enabled(policy, "issue_label"):
398+
if action == "labeled" and not trigger_enabled(policy, "issues.labeled"):
390399
return ignored(base, "issue_label_not_enabled")
391400
base.update(
392401
{
@@ -532,6 +541,21 @@ def route_delivery(event_name, delivery_id, payload, debug):
532541
"reason": "no_policy_for_installation_repo",
533542
}
534543

544+
trigger = event_trigger_key(event_name, payload)
545+
if not trigger_enabled(policy, trigger):
546+
delivery["state"] = "ignored"
547+
delivery["routing_result"] = "trigger_not_enabled"
548+
delivery["installation_id"] = installation_id or delivery.get("installation_id")
549+
delivery["repository_id"] = repo_id or delivery.get("repository_id")
550+
write_json_atomic(delivery_file, delivery)
551+
return {
552+
"ok": True,
553+
"action": "ignored",
554+
"delivery_id": delivery_id,
555+
"reason": "trigger_not_enabled",
556+
"trigger": trigger,
557+
}
558+
535559
task = build_task_from_event(event_name, delivery_id, payload, policy)
536560
task["policy_snapshot"] = {
537561
"enabled_triggers": policy.get("enabled_triggers") or [],

deploy/coven-github/test_coven_github_adapter.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,101 @@ def test_route_signed_delivery_reports_missing_secret(self):
4141
self.assertEqual(result["status"], 500)
4242
self.assertIn("GITHUB_WEBHOOK_SECRET", result["error"])
4343

44+
def test_disabled_issue_action_is_rejected_before_task_creation(self):
45+
adapter = load_adapter()
46+
policy = {
47+
"enabled_triggers": [
48+
"issues.labeled",
49+
"issue_comment.created",
50+
"pull_request_review_comment.created",
51+
],
52+
"trigger_labels": ["coven:fix"],
53+
"bot_usernames": ["coven-cody[bot]"],
54+
"familiar": {
55+
"id": "cody",
56+
"display_name": "Cody",
57+
"model": "openai/gpt-5.5",
58+
"skills": [],
59+
},
60+
"publication": {"mode": "record_only"},
61+
}
62+
63+
with tempfile.TemporaryDirectory() as tmp:
64+
state_dir = Path(tmp)
65+
adapter.DELIVERIES_DIR = state_dir / "deliveries"
66+
adapter.TASKS_DIR = state_dir / "tasks"
67+
adapter.repo_policy = lambda payload: ("123456", "987654321", policy)
68+
adapter.run_task = lambda task_id, debug: self.fail("disabled trigger ran")
69+
70+
result = adapter.route_delivery(
71+
"issues",
72+
"delivery-issue-opened",
73+
{
74+
"action": "opened",
75+
"installation": {"id": 123456},
76+
"repository": {
77+
"id": 987654321,
78+
"full_name": "OpenCoven/example",
79+
"clone_url": "https://github.com/OpenCoven/example.git",
80+
"default_branch": "main",
81+
},
82+
"issue": {
83+
"number": 43,
84+
"title": "Installer is slow",
85+
"body": "A diagnostic issue, not a bot task.",
86+
"labels": [],
87+
"assignees": [],
88+
},
89+
},
90+
lambda message: None,
91+
)
92+
93+
self.assertEqual(result["action"], "ignored")
94+
self.assertEqual(result["reason"], "trigger_not_enabled")
95+
self.assertEqual(result["trigger"], "issues.opened")
96+
self.assertTrue(adapter.delivery_path("delivery-issue-opened").exists())
97+
self.assertFalse(adapter.task_path("delivery-issue-opened").exists())
98+
99+
def test_labeled_issue_uses_webhook_trigger_name_policy(self):
100+
adapter = load_adapter()
101+
102+
task = adapter.build_task_from_event(
103+
"issues",
104+
"delivery-issue-labeled",
105+
{
106+
"action": "labeled",
107+
"installation": {"id": 123456},
108+
"repository": {
109+
"id": 987654321,
110+
"full_name": "OpenCoven/example",
111+
"clone_url": "https://github.com/OpenCoven/example.git",
112+
"default_branch": "main",
113+
},
114+
"issue": {
115+
"number": 44,
116+
"title": "Fix it",
117+
"body": "Please fix it.",
118+
"labels": [{"name": "coven:fix"}],
119+
},
120+
},
121+
{
122+
"enabled_triggers": ["issues.labeled"],
123+
"trigger_labels": ["coven:fix"],
124+
"bot_usernames": ["coven-cody[bot]"],
125+
"familiar": {
126+
"id": "cody",
127+
"display_name": "Cody",
128+
"model": "openai/gpt-5.5",
129+
"skills": [],
130+
},
131+
"publication": {"mode": "record_only"},
132+
},
133+
)
134+
135+
self.assertEqual(task["state"], "queued")
136+
self.assertEqual(task["trigger"], "issue_assigned")
137+
self.assertEqual(task["task"]["kind"], "fix_issue")
138+
44139
def test_prepare_review_context_rejects_stale_pr_head_evidence(self):
45140
adapter = load_adapter()
46141

0 commit comments

Comments
 (0)