Skip to content

Commit bf70d40

Browse files
MartinRoberts-FountainColeMurraykartikyeKartikye
authored
Add GitHub Automations Feature (#572)
<img width="1248" height="944" alt="image" src="https://github.com/user-attachments/assets/85eeefc4-5dab-4fad-8f2e-228df02808b1" /> Unlocks the GitHub automation <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitHub trigger integration (PRs, issues, comments, check suites) with selectable event types, normalization, and forwarding to the control plane. * Human-readable GitHub event context blocks for automations. * **Enhancements** * UI: event-type selector, enabled GitHub trigger card, and GitHub-specific instruction placeholder. * New automation condition types: branch, label, path patterns, actor, check conclusion. * Session archiving UI and archiveSession helper; sidebar archive flow. * **Tests** * Added tests for GitHub normalization, identity parsing, tokens, and queue behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <colemurray.cs@gmail.com> Co-authored-by: Kartikye Mittal <kartikye.mittal@gmail.com> Co-authored-by: Kartikye <kartikye@dots.dev>
1 parent 26f77fe commit bf70d40

20 files changed

Lines changed: 1448 additions & 19 deletions

File tree

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* GitHub automation event webhook route — internal endpoint that receives
3+
* pre-normalized GitHubAutomationEvents from the github-bot and proxies
4+
* them to the SchedulerDO for automation matching and session dispatch.
5+
*/
6+
7+
import type { GitHubAutomationEvent } from "@open-inspect/shared";
8+
import { verifyInternalToken } from "../auth/internal";
9+
import type { Route, RequestContext } from "../routes/shared";
10+
import { parsePattern, json, error } from "../routes/shared";
11+
import type { Env } from "../types";
12+
13+
async function handleGitHubAutomationEvent(
14+
request: Request,
15+
env: Env,
16+
_match: RegExpMatchArray,
17+
_ctx: RequestContext
18+
): Promise<Response> {
19+
// 0. Authenticate — fail closed if secret is unconfigured or token is invalid
20+
if (!env.INTERNAL_CALLBACK_SECRET) {
21+
return error("Internal authentication not configured", 500);
22+
}
23+
const isValid = await verifyInternalToken(
24+
request.headers.get("Authorization"),
25+
env.INTERNAL_CALLBACK_SECRET
26+
);
27+
if (!isValid) {
28+
return error("Unauthorized", 401);
29+
}
30+
31+
// 1. Parse body
32+
let body: unknown;
33+
try {
34+
body = await request.json();
35+
} catch {
36+
return error("Invalid JSON", 400);
37+
}
38+
39+
// 2. Validate required fields
40+
const event = body as Partial<GitHubAutomationEvent>;
41+
if (event.source !== "github") {
42+
return error("Invalid event: source must be 'github'", 400);
43+
}
44+
if (!event.repoOwner || !event.repoName) {
45+
return error("Invalid event: repoOwner and repoName are required", 400);
46+
}
47+
if (!event.eventType || !event.triggerKey || !event.concurrencyKey) {
48+
return error("Invalid event: eventType, triggerKey, and concurrencyKey are required", 400);
49+
}
50+
51+
// 3. Forward to SchedulerDO
52+
if (!env.SCHEDULER) {
53+
return error("Scheduler not configured", 503);
54+
}
55+
56+
const doId = env.SCHEDULER.idFromName("global-scheduler");
57+
const stub = env.SCHEDULER.get(doId);
58+
59+
let response: Response;
60+
try {
61+
response = await stub.fetch("http://internal/internal/event", {
62+
method: "POST",
63+
headers: { "Content-Type": "application/json" },
64+
body: JSON.stringify(event),
65+
});
66+
} catch (_e) {
67+
return json({ ok: false, error: "Failed to reach scheduler" }, 502);
68+
}
69+
70+
let result: { triggered: number; skipped: number };
71+
try {
72+
result = await response.json<{ triggered: number; skipped: number }>();
73+
} catch {
74+
return json({ ok: false, error: "Invalid response from scheduler" }, 502);
75+
}
76+
77+
return json({ ok: true, ...result }, response.status);
78+
}
79+
80+
export const githubAutomationEventRoute: Route = {
81+
method: "POST",
82+
pattern: parsePattern("/internal/github-event"),
83+
handler: handleGitHubAutomationEvent,
84+
};

packages/control-plane/src/webhooks/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@
55
import type { Route } from "../routes/shared";
66
import { sentryWebhookRoute } from "./sentry";
77
import { automationWebhookRoute } from "./automation-webhook";
8+
import { githubAutomationEventRoute } from "./github";
89

9-
export const webhookRoutes: Route[] = [sentryWebhookRoute, automationWebhookRoute];
10+
export const webhookRoutes: Route[] = [
11+
sentryWebhookRoute,
12+
automationWebhookRoute,
13+
githubAutomationEventRoute,
14+
];

packages/github-bot/src/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import type { Logger } from "./logger";
1717
import { createLogger, parseLogLevel } from "./logger";
1818
import { verifyWebhookSignature } from "./verify";
19+
import { normalizeGitHubEvent, buildInternalAuthHeaders } from "@open-inspect/shared";
1920
import {
2021
handlePullRequestOpened,
2122
handleReviewRequested,
@@ -185,6 +186,38 @@ async function handleWebhook(
185186
wideEvent.handler_action = result.handler_action;
186187
}
187188
log.info("webhook.handled", wideEvent);
189+
190+
// Forward normalized event to control-plane for automation triggering.
191+
// This is additive — failures here must not affect existing bot behavior.
192+
if (event) {
193+
const normalizedEvent = normalizeGitHubEvent(event, p);
194+
if (normalizedEvent !== null) {
195+
try {
196+
const body = JSON.stringify(normalizedEvent);
197+
const authHeaders = await buildInternalAuthHeaders(env.INTERNAL_CALLBACK_SECRET, traceId);
198+
const response = await env.CONTROL_PLANE.fetch("https://internal/internal/github-event", {
199+
method: "POST",
200+
headers: { "Content-Type": "application/json", ...authHeaders },
201+
body,
202+
});
203+
if (!response.ok) {
204+
log.warn("webhook.github_event_forward_failed", {
205+
trace_id: traceId,
206+
delivery_id: deliveryId,
207+
event_type: event,
208+
status: response.status,
209+
});
210+
}
211+
} catch (err) {
212+
log.warn("webhook.github_event_forward_error", {
213+
trace_id: traceId,
214+
delivery_id: deliveryId,
215+
event_type: event,
216+
error: err instanceof Error ? err : new Error(String(err)),
217+
});
218+
}
219+
}
220+
}
188221
}
189222

190223
function dispatchHandler(

packages/shared/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"vitest": "^4.0.18"
2525
},
2626
"dependencies": {
27+
"@octokit/webhooks-types": "^7.6.1",
2728
"cron-parser": "^5.5.0"
2829
}
2930
}

0 commit comments

Comments
 (0)