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.

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