A Cloudflare Worker that sits between GitHub and another webhook consumer. It
accepts GitHub release webhook deliveries, keeps only the actions you list,
and replays those deliveries to an upstream URL unchanged — same body bytes,
same GitHub headers — so the receiver cannot tell the difference from a direct
delivery.
Everything else (other events, other release actions) is acknowledged with a
200 and dropped.
| Condition | Result |
|---|---|
Not a POST |
405 |
Missing/invalid X-Hub-Signature-256 |
401, nothing forwarded |
X-GitHub-Event: ping |
200, nothing forwarded |
Event is not release |
200, nothing forwarded |
release action not in INCLUDED_ACTIONS |
200, nothing forwarded |
release action in INCLUDED_ACTIONS |
forwarded upstream, 200 |
| Upstream errored or timed out (10s) | 502, so GitHub can redeliver |
Every delivery is signature-verified with GITHUB_WEBHOOK_SECRET before it is
parsed. If any setting is missing the worker fails closed with a 500.
All settings live in .env (gitignored). See .env.sample:
| Setting | Stored as | Purpose |
|---|---|---|
INCLUDED_ACTIONS |
plain var | Comma-separated release actions to forward, e.g. published,released. Matching is case-insensitive. |
UPSTREAM_WEBHOOK_URL |
secret | Where included deliveries are replayed. |
GITHUB_WEBHOOK_SECRET |
secret | Shared secret used to verify GitHub's HMAC signature. |
GitHub's release actions are: published, unpublished, created, edited,
deleted, prereleased, released.
1. Install prerequisites — Node 18+ and a Cloudflare account.
npm install
npx wrangler login # one-time browser authFor CI instead of wrangler login, set CLOUDFLARE_API_TOKEN (needs the Edit
Cloudflare Workers template) and CLOUDFLARE_ACCOUNT_ID in the environment.
2. Fill in your settings
cp .env.sample .env
openssl rand -hex 32 # use this for GITHUB_WEBHOOK_SECRET
$EDITOR .env3. Deploy
npm run deploy # add --dry-run to preview without touching Cloudflarescripts/deploy.sh reads .env, publishes the worker with
wrangler deploy --var INCLUDED_ACTIONS:…, then pushes UPSTREAM_WEBHOOK_URL
and GITHUB_WEBHOOK_SECRET with wrangler secret put. The secrets are stored
encrypted in Cloudflare and are write-only in the dashboard afterwards. .env
is never uploaded.
Wrangler prints the deployed URL, e.g.
https://github-webhook-filter.<your-subdomain>.workers.dev.
The first deploy of a brand-new worker publishes before the secrets are attached, so for a few seconds it answers
500(fail-closed). Re-runningnpm run deploylater updates everything in place.
4. Point GitHub at it — in the repo (or org): Settings → Webhooks → Add webhook
- Payload URL: the worker URL from step 3
- Content type:
application/json - Secret: the same
GITHUB_WEBHOOK_SECRETvalue from.env - Events: Let me select individual events → check Releases only
GitHub immediately sends a ping; a green checkmark in Recent Deliveries
means the signature check passed.
5. Verify — tail the live logs and cut a release:
npx wrangler tailEach delivery logs one line, e.g.
delivery=<uuid> forwarded: action=published upstreamStatus=200 or
delivery=<uuid> skipped: release action=edited is not included.
Edit .env and re-run npm run deploy.
wrangler dev reads secrets from .dev.vars, which uses the same format as
.env:
cp .env .dev.vars
npm run devSend yourself a signed test delivery:
BODY='{"action":"published","release":{"tag_name":"v1.0.0"}}'
SECRET=$(grep '^GITHUB_WEBHOOK_SECRET=' .env | cut -d= -f2-)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -sS -i http://localhost:8787/ \
-H 'content-type: application/json' \
-H 'x-github-event: release' \
-H 'x-github-delivery: local-test' \
-H "x-hub-signature-256: sha256=$SIG" \
-d "$BODY"npm test # vitest: filtering, signature checks, forwarding, failure modes
npm run typechecksrc/index.ts worker: verify -> filter -> forward
scripts/deploy.sh reads .env, deploys, pushes vars + secrets to Cloudflare
test/worker.test.ts unit tests
wrangler.toml worker name, entrypoint, compatibility date
.env.sample documented template for .env