Skip to content

Commit f0ca41c

Browse files
hubyrodclaude
andcommitted
Migrate to single Next.js server, remove Bun proxy
Replace the dual-process architecture (Bun server + Next.js proxy) with a single Next.js app using Route Handlers for webhooks and health check. - app/github/[route]/route.ts handles webhook POST requests - app/health/route.ts handles health checks - instrumentation.ts runs startup validation and calendar poller - Move web/ app files to root app/ directory - Remove src/index.ts (Bun.serve + proxy) - Remove .ts extensions from all imports for Next.js compatibility - Update package.json scripts to use next dev/build/start Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c32ed66 commit f0ca41c

31 files changed

Lines changed: 345 additions & 404 deletions

.github/workflows/deploy.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ jobs:
1616

1717
- run: bun install
1818

19+
- name: Build
20+
run: bun run build
21+
1922
- name: Typecheck
2023
run: bun run typecheck
2124

2225
- name: Test
23-
run: bun test
26+
run: bun test src/
2427

2528
deploy:
2629
needs: check

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# dependencies (bun install)
22
node_modules
33

4+
# Next.js build output
5+
.next/
6+
47
# output
58
out
69
dist

app/github/[route]/route.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { loadConfig, type EventType, type SkiphooksConfig } from "@/src/config";
2+
import { verifySignature } from "@/src/webhook";
3+
import { postToSlashwork, type SlashworkConnection } from "@/src/slashwork";
4+
import type { EventHandler } from "@/src/handlers/types";
5+
import { pullRequestHandler } from "@/src/handlers/pull-request";
6+
import { issuesHandler } from "@/src/handlers/issues";
7+
import { pushHandler } from "@/src/handlers/push";
8+
import { issueCommentHandler } from "@/src/handlers/issue-comment";
9+
import { releaseHandler } from "@/src/handlers/release";
10+
11+
export const dynamic = "force-dynamic";
12+
13+
let _config: SkiphooksConfig | null = null;
14+
function getConfig() {
15+
if (!_config) _config = loadConfig();
16+
return _config;
17+
}
18+
19+
const handlers: Record<EventType, EventHandler> = {
20+
pull_request: pullRequestHandler,
21+
issues: issuesHandler,
22+
issue_comment: issueCommentHandler,
23+
push: pushHandler,
24+
release: releaseHandler,
25+
};
26+
27+
function log(level: string, message: string) {
28+
console.log(`[${new Date().toISOString()}] [${level}] ${message}`);
29+
}
30+
31+
function resolveRoute(config: SkiphooksConfig, routeName: string): { targetId: string; authToken: string } {
32+
const route = config.routes[routeName]!;
33+
if ("group" in route) {
34+
const group = config.groups![route.group]!;
35+
return { targetId: group.id, authToken: group.authToken };
36+
}
37+
return { targetId: route.streamId, authToken: route.authToken };
38+
}
39+
40+
export async function POST(
41+
request: Request,
42+
{ params }: { params: Promise<{ route: string }> },
43+
) {
44+
const config = getConfig();
45+
const { route: routeName } = await params;
46+
47+
const route = config.routes[routeName];
48+
if (!route) {
49+
log("warn", `No route configured for: ${routeName}`);
50+
return new Response("Not found", { status: 404 });
51+
}
52+
53+
const { targetId, authToken } = resolveRoute(config, routeName);
54+
const connection: SlashworkConnection = {
55+
graphqlUrl: config.slashwork.graphqlUrl,
56+
authToken,
57+
};
58+
59+
let body: string;
60+
try {
61+
body = await request.text();
62+
} catch {
63+
return new Response("Bad request", { status: 400 });
64+
}
65+
66+
const signature = request.headers.get("x-hub-signature-256");
67+
if (!verifySignature(body, signature, config.github.webhookSecret)) {
68+
log("warn", `Invalid webhook signature for route "${routeName}" (signature: ${signature ? `"${signature.slice(0, 20)}..."` : "missing"}, body length: ${body.length})`);
69+
return new Response("Invalid signature", { status: 401 });
70+
}
71+
72+
const eventType = request.headers.get("x-github-event") as EventType | null;
73+
if (!eventType) {
74+
log("info", "Missing x-github-event header");
75+
return new Response("OK");
76+
}
77+
78+
const handler = handlers[eventType];
79+
if (!handler) {
80+
log("info", `No handler for event: ${eventType}`);
81+
return new Response("OK");
82+
}
83+
84+
let payload: { action?: string };
85+
try {
86+
const json = body.startsWith("payload=")
87+
? decodeURIComponent(body.slice("payload=".length))
88+
: body;
89+
payload = JSON.parse(json);
90+
} catch (err) {
91+
log("error", `Failed to parse ${eventType} payload: ${err} (body length: ${body.length})`);
92+
return new Response("OK");
93+
}
94+
95+
if (!handler.isRelevantAction(payload.action)) {
96+
log("info", `Ignoring ${eventType} action: ${payload.action}`);
97+
return new Response("OK");
98+
}
99+
100+
try {
101+
const { markdown } = handler.format(payload);
102+
await postToSlashwork(connection, targetId, markdown);
103+
log("info", `Posted ${eventType} event: ${payload.action ?? "n/a"}`);
104+
} catch (err) {
105+
log("error", `Failed to post to Slashwork: ${err}`);
106+
}
107+
108+
return new Response("OK");
109+
}

app/health/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function GET() {
2+
return new Response("OK");
3+
}
4+
5+
export function HEAD() {
6+
return new Response("OK");
7+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.

bun.lock

Lines changed: 115 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SkiphooksConfig } from "./src/config.ts";
1+
import type { SkiphooksConfig } from "./src/config";
22

33
const config: SkiphooksConfig = {
44
github: {

0 commit comments

Comments
 (0)