1---
2title: React to GitHub events from a Worker
3description: Subscribe a Cloudflare Worker to GitHub repository webhooks with typed payloads — alchemy provisions the webhook and verifies delivery signatures.
4sidebar:
5 order: 20
6---
7
8A push lands on `main`, a pull request opens, a release tag appears —
9and you want code to run. `GitHub.consumeRepositoryEvents` subscribes
10a Cloudflare Worker to a repository's webhooks: alchemy provisions
11the webhook on the repo, points it at the Worker, verifies each
12delivery's signature, and hands your handler a fully typed payload.
13
14This event source is **Cloudflare-only today** — see
15[the note below](#cloudflare-only-today).
16
17## 1. Subscribe to push events
18
19Call `consumeRepositoryEvents` in the Worker's Construction phase with the
20repository and the events you want, plus a handler that runs once per
21delivery:
22
23```typescript
24// src/worker.ts
25import * as Cloudflare from "alchemy/Cloudflare";
26import * as GitHub from "alchemy/GitHub";
27import * as Effect from "effect/Effect";
28import * as HttpServerResponse from "effect/http/HttpServerResponse";
29
30export default Cloudflare.Worker(
31 "Worker",
32 { main: import.meta.url },
33 Effect.gen(function* () {
34 yield* GitHub.consumeRepositoryEvents(
35 {
36 owner: "my-org",
37 repository: "my-repo",
38 events: ["push"],
39 },
40 (event) => Effect.log(`received ${event.name} (${event.id})`),
41 );
42
43 return {
44 fetch: Effect.gen(function* () {
45 return HttpServerResponse.text("ok");
46 }),
47 };
48 }),
49);
50```
51
52`events` defaults to `["push"]`; pass `["*"]` to receive everything
53GitHub emits. The handler receives each delivery as a typed
54`WebhookEvent` and returns an `Effect` — more on the payload types
55below.
56
57## 2. Provide the Cloudflare runtime layer
58
59`consumeRepositoryEvents` is host-agnostic — it requires a
60`RepositoryEventSource` implementation. Provide the Cloudflare one on
61the Worker:
62
63```diff lang="typescript"
64 export default Cloudflare.Worker(
65 "Worker",
66 { main: import.meta.url },
67 Effect.gen(function* () {
68 // ...
69- }),
70+ }).pipe(Effect.provide(Cloudflare.Workers.GitHubRepositoryEventSourceLive)),
71 );
72```
73
74`GitHubRepositoryEventSourceLive` does both halves: at deploy time it
75provisions the repository webhook pointing at this Worker, and at
76runtime it registers a `fetch` listener that claims the webhook's
77delivery path and forwards each verified delivery to your handler.
78Requests on any other path fall through to the Worker's own `fetch`.
79
80## 3. Add GitHub providers to the Stack
81
82The webhook is a GitHub resource, so the Stack needs GitHub providers
83alongside Cloudflare's:
84
85```typescript
86// alchemy.run.ts
87import * as Alchemy from "alchemy";
88import * as Cloudflare from "alchemy/Cloudflare";
89import * as GitHub from "alchemy/GitHub";
90import * as Effect from "effect/Effect";
91import * as Layer from "effect/Layer";
92import Worker from "./src/worker.ts";
93
94export default Alchemy.Stack(
95 "GitHubEvents",
96 {
97 providers: Layer.mergeAll(Cloudflare.providers(), GitHub.providers()),
98 state: Cloudflare.state(),
99 },
100 Effect.gen(function* () {
101 const worker = yield* Worker;
102 return { url: worker.url.as<string>() };
103 }),
104);
105```
106
107`GitHub.providers()` resolves credentials from the environment, a
108stored PAT, or the `gh` CLI — see [GitHub setup](/github/setup). The
109token needs `repo` scope (admin access to the repository) to manage
110webhooks.
111
112## 4. Verify deliveries with a secret
113
114Without a secret, anyone who learns the delivery URL can forge
115events. Pass one and alchemy rejects any delivery whose
116`X-Hub-Signature-256` header doesn't match:
117
118```diff lang="typescript"
119+import * as Config from "effect/Config";
120
121 Effect.gen(function* () {
122+ const secret = yield* Config.Redacted("GITHUB_WEBHOOK_SECRET");
123+
124 yield* GitHub.consumeRepositoryEvents(
125 {
126 owner: "my-org",
127 repository: "my-repo",
128 events: ["push"],
129+ secret,
130 },
131 (event) => Effect.log(`received ${event.name} (${event.id})`),
132 );
133```
134
135The webhook is provisioned with the secret, so GitHub signs every
136delivery with `HMAC-SHA256`; the Worker recomputes the signature over
137the raw body and compares in constant time, answering `401` on a
138mismatch. `Config.Redacted` reads the value from your `.env` at
139deploy time and binds it as a Worker secret — see
140[Secrets & env](/cloudflare/security/secrets-env).
141
142## 5. Deploy
143
144```sh
145alchemy deploy
146```
147
148On deploy, alchemy creates a `GitHub.Webhook` on the repository whose
149delivery URL is the Worker's URL plus a deterministic per-repo path
150(`/__alchemy/github/{owner}/{repository}` — override with the `path`
151prop). The webhook is updated in place on subsequent deploys and
152deleted when the stack is destroyed. Push a commit and watch the
153handler log the delivery.
154
155## What alchemy automates
156
157One `consumeRepositoryEvents` call replaces the usual webhook
158plumbing:
159
160- **Provisioning** — a [`GitHub.Webhook`](/providers/github/webhook)
161 resource is created on the repo, wired to the Worker's URL. You
162 never paste a URL into repo settings.
163- **Secret distribution** — the signing secret is bound onto the
164 Worker as a secret under a deterministic env name; the runtime
165 reads it back to verify signatures.
166- **Routing** — the runtime only claims `POST`s on the delivery
167 path (`405` for other methods, `202` once a delivery is accepted);
168 every other request reaches your own `fetch` handler untouched.
169- **Signature verification** — `HMAC-SHA256` over the raw body,
170 compared in constant time against `X-Hub-Signature-256`.
171
172## Typed payloads
173
174`event` is Octokit's `EmitterWebhookEvent` — a discriminated union of
175`{ id, name, payload }` keyed on `name`, narrowed to the events you
176selected. A `switch` on `event.name` narrows `payload` exhaustively:
177
178```typescript
179yield* GitHub.consumeRepositoryEvents(
180 {
181 owner: "my-org",
182 repository: "my-repo",
183 events: ["push", "pull_request"],
184 },
185 (event) => {
186 switch (event.name) {
187 case "push":
188 return Effect.log(`push to ${event.payload.ref}`);
189 case "pull_request":
190 return Effect.log(`PR: ${event.payload.pull_request.title}`);
191 }
192 },
193);
194```
195
196Selecting `["*"]` (or omitting `events`) widens the union back to
197every GitHub event name.
198
199## Case study: a release-blogging agent
200
201The [`cloudflare-agent` example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-agent)
202uses this event source as the front door of an autonomous pipeline:
203a Worker consumes push events on the alchemy repo, filters for
204release commits on `main`, and hands each one to a Durable Object
205that drives an AI agent to write the release blog post.
206
207The filter is ordinary handler code over the typed `push` payload:
208
209```typescript
210// src/ReleaseService.ts
211yield* GitHub.consumeRepositoryEvents(
212 {
213 owner: "alchemy-run",
214 repository: "alchemy",
215 events: ["push"],
216 },
217 (event) => {
218 const title = event.payload.head_commit?.message.split("\n")[0] ?? "";
219 const isRelease =
220 event.payload.ref === "refs/heads/main" &&
221 title.startsWith("chore(release):");
222
223 return isRelease
224 ? versions.getByName(event.payload.head_commit!.id).generateBlog({
225 input: event.payload,
226 })
227 : Effect.log(`skipping ${event.payload.head_commit?.id}`);
228 },
229);
230```
231
232Worth reading in the example:
233
234- `src/ReleaseService.ts` — the Worker above; each release commit is
235 routed to a Durable Object keyed by commit hash.
236- `src/ReleaseVersion.ts` — the per-release Durable Object that
237 forwards the payload to the agent.
238- `src/ReleaseBlogger.ts` — an `Alchemy.Agent` whose prompt
239 references its tools directly.
240- `src/DevBox.ts` — a Cloudflare Container the agent's file and shell
241 tools execute in.
242
243## Cloudflare-only today
244
245`Cloudflare.Workers.GitHubRepositoryEventSourceLive` is currently the
246only implementation of the `RepositoryEventSource` contract — there
247is no AWS Lambda event source yet. On other hosts you can still
248provision a [`GitHub.Webhook`](/providers/github/webhook) directly
249with any delivery URL, but signature verification and routing are
250yours to implement.
251
252## Where next
253
254- [GitHub provider](/github) — repositories, Actions secrets and
255 variables, and the rest of the CI/CD glue.
256- [Workers](/cloudflare/compute/workers) — the host this event source runs
257 on.
258- [Webhook API reference](/providers/github/webhook) — the resource
259 provisioned under the hood.