Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

58 lines · 2.2 KBRaw History
1import { hmacSha256Hex } from "./util.js";
2
3/** One webhook delivery; failed ones go back on the events queue with a delay. */
4export interface WebhookRetry {
5 type: "webhook_retry";
6 repoId: string;
7 hookId: string;
8 event: string;
9 diffineryEvent: string;
10 body: string;
11 delivery: string;
12 /** Attempts made so far. */
13 attempt: number;
14}
15
16export interface WebhookDeps {
17 /** The hook as it is now (it may have been deleted, disabled or re-keyed), or null. */
18 hook(repoId: string, hookId: string): Promise<{ id: string; url: string; secret: string } | null>;
19 record(repoId: string, hookId: string, status: number): Promise<void>;
20 requeue(r: WebhookRetry, delaySeconds: number): Promise<void>;
21 fetch?: typeof fetch;
22}
23
24// Delays after the first, second, ... failed attempt: about five hours in all.
25export const WEBHOOK_RETRY_DELAYS = [60, 300, 1800, 3600, 3 * 3600];
26
27/** Network errors, timeouts, rate limits and server errors are worth another try. */
28export const retryable = (status: number) => status === 0 || status === 408 || status === 429 || status >= 500;
29
30/** Send one delivery (to `known` when the caller already has the hook) and schedule a retry if it fails. */
31export async function deliverWebhook(deps: WebhookDeps, r: WebhookRetry, known?: { id: string; url: string; secret: string }): Promise<number | null> {
32 const h = known ?? (await deps.hook(r.repoId, r.hookId));
33 if (!h) return null;
34 let status = 0;
35 try {
36 const res = await (deps.fetch ?? fetch)(h.url, {
37 method: "POST",
38 headers: {
39 "content-type": "application/json",
40 "user-agent": "Diffinery-Hookshot/1",
41 "x-github-event": r.event,
42 "x-github-delivery": r.delivery,
43 "x-diffinery-event": r.diffineryEvent,
44 "x-hub-signature-256": `sha256=${await hmacSha256Hex(h.secret, r.body)}`,
45 },
46 body: r.body,
47 redirect: "manual",
48 signal: AbortSignal.timeout(10_000),
49 });
50 status = res.status;
51 } catch {
52 status = 0;
53 }
54 await deps.record(r.repoId, h.id, status);
55 const delay = WEBHOOK_RETRY_DELAYS[r.attempt];
56 if (retryable(status) && delay !== undefined) await deps.requeue({ ...r, attempt: r.attempt + 1 }, delay);
57 return status;
58}