Webhooks & events
GitHub tells the world what happened to a repository through
webhooks — POSTs to a URL on every push, pull request, release,
and so on. Alchemy gives you two levels of abstraction: the
Webhook resource manages the webhook itself and points it at any
URL, and consumeRepositoryEvents subscribes a Cloudflare Worker to
a repository with the webhook, routing, and signature verification
handled for you.
The Webhook resource
Section titled “The Webhook resource”GitHub.Webhook manages a repository webhook’s lifecycle — created
on the first deploy, updated in place on subsequent deploys, deleted
when the stack is destroyed:
import * as GitHub from "alchemy/GitHub";
yield* GitHub.Webhook("ci-webhook", { owner: "my-org", repository: "my-repo", url: "https://example.com/github", events: ["push", "pull_request"], secret,});events defaults to ["push"]; pass ["*"] to receive every event
GitHub emits. The secret (a Redacted string, e.g. from
Config.redacted — see Secrets & env)
makes GitHub sign each delivery with HMAC-SHA256 in the
X-Hub-Signature-256 header so your receiver can verify the payload
came from GitHub.
Credentials come from GitHub.providers() — env, stored PAT, or the
gh CLI (see setup). The token needs repo scope
(admin access to the repository) to manage webhooks.
Point it at another resource
Section titled “Point it at another resource”url accepts an Output<string>, so the webhook can target a
resource provisioned in the same stack:
const worker = yield* Cloudflare.Worker("Api", { ... });
yield* GitHub.Webhook("repo-webhook", { owner: "my-org", repository: "my-repo", url: worker.url!, events: ["*"],});At this level the receiving end is yours to build: parsing the payload, routing the delivery path, and verifying signatures. Most of the time you don’t want that — read on.
Consume events from a Worker
Section titled “Consume events from a Worker”GitHub.consumeRepositoryEvents subscribes a Cloudflare Worker to a
repository and replaces all the plumbing: it provisions the webhook
pointing at the Worker, binds the signing secret onto it, claims the
delivery path in fetch, verifies each delivery’s signature, and
hands your handler a typed payload:
import * as Cloudflare from "alchemy/Cloudflare";import * as GitHub from "alchemy/GitHub";import * as Effect from "effect/Effect";
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { yield* GitHub.consumeRepositoryEvents( { owner: "my-org", repository: "my-repo", events: ["push", "pull_request"], secret, }, (event) => { switch (event.name) { case "push": return Effect.log(`push to ${event.payload.ref}`); case "pull_request": return Effect.log(`PR: ${event.payload.pull_request.title}`); } }, );
// ...your own fetch handler is untouched }).pipe(Effect.provide(Cloudflare.Workers.GitHubRepositoryEventSourceLive)),);event is Octokit’s EmitterWebhookEvent, narrowed to the events
you selected — a switch on event.name narrows event.payload
exhaustively. The stack provides both clouds’ providers:
Layer.mergeAll(Cloudflare.providers(), GitHub.providers()).
For the full walkthrough — runtime layer, secret verification, deploy, and a release-blogging agent built on this event source — see React to GitHub events from a Worker.
Cloudflare-only today
Section titled “Cloudflare-only today”Cloudflare.Workers.GitHubRepositoryEventSourceLive is currently
the only implementation of the RepositoryEventSource contract —
there is no AWS Lambda event source yet. On other hosts you can
still provision a GitHub.Webhook directly with any delivery URL,
but routing and signature verification are yours to implement.
Where next
Section titled “Where next”- React to GitHub events from a Worker — the goal-oriented guide.
- Repositories — create and converge the repos these webhooks attach to.
- GitHub setup — credentials and token scopes.
Reference: