Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/operations/github-pr-event-feed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# github pr event feed

this service receives github webhook deliveries for `pingdotgg/t3code` and exposes them as a replayable server-sent event feed. contributors can use the feed as the wake-up input for pr babysitters instead of polling github.

## deploy

set these secrets in the deployment environment:

- `GITHUB_WEBHOOK_SECRET`: a random secret of at least 32 characters shared only with github
- `GITHUB_EVENTS_FEED_TOKEN`: a separate random token of at least 32 characters distributed to feed subscribers
- `CLOUDFLARE_API_TOKEN`: the alchemy deployment credential
- `CLOUDFLARE_ACCOUNT_ID`: the target cloudflare account

`GITHUB_REPOSITORY` defaults to `pingdotgg/t3code`.

```sh
vp run --filter t3code-github-events deploy --stage prod
```

save the worker url returned by alchemy.

## configure github

create one repository webhook with:

- payload url: `<worker-url>/v1/github/webhook`
- content type: `application/json`
- secret: the exact `GITHUB_WEBHOOK_SECRET`
- ssl verification: enabled
- events:
- pull requests
- issue comments
- pull request reviews
- pull request review comments
- check runs
- check suites
- workflow runs
- commit statuses

only comments attached to pull requests enter the feed. unsupported deliveries are acknowledged and ignored.

## subscribe

subscribe to every retained and live repository event:

```sh
curl -N \
-H "Authorization: Bearer $GITHUB_EVENTS_FEED_TOKEN" \
https://<worker-host>/v1/repos/pingdotgg/t3code/events
```

subscribe to one pull request and resume after a stored sequence:

```sh
curl -N \
-H "Authorization: Bearer $GITHUB_EVENTS_FEED_TOKEN" \
-H "Last-Event-ID: 1204" \
"https://<worker-host>/v1/repos/pingdotgg/t3code/events?pull=7321"
```

`after=1204` is equivalent to `Last-Event-ID: 1204`. event ids are monotonically increasing within the repository. reconnect after a disconnect using the latest received id. subscribers that stop reading are disconnected before their buffers can grow without bound, so consumers must reconnect from their last id.

if the requested cursor predates the latest 512 retained events, the service returns `410` with `earliestSequence` and `latestSequence`. perform one github refresh, then reconnect from the latest sequence. if a cursor is ahead of the feed, the service returns `409` with the same bounds instead of allowing sequence rewind.

github does not automatically redeliver failed webhook deliveries. use the webhook's recent deliveries page to redeliver one manually. the latest 10,000 `X-GitHub-Delivery` ids are deduplicated independently of the 512-event replay window.

## event shape

sse messages use event name `github`. each `data` value is json with this envelope:

```json
{
"version": 1,
"sequence": 1205,
"deliveryId": "github-delivery-uuid",
"event": "issue_comment",
"action": "created",
"repository": {
"id": 123,
"fullName": "pingdotgg/t3code",
"url": "https://github.com/pingdotgg/t3code"
},
"pullRequestNumbers": [7321],
"headSha": null,
"actor": {
"id": 456,
"login": "contributor",
"avatarUrl": "https://avatars.githubusercontent.com/..."
},
"receivedAt": "2026-08-18T12:00:01Z",
"occurredAt": "2026-08-18T12:00:00Z",
"details": {
"comment": {
"id": 789,
"body": "comment content",
"author": {
"id": 456,
"login": "contributor",
"avatarUrl": null
},
"url": "https://github.com/pingdotgg/t3code/pull/7321#issuecomment-789"
}
}
}
```

`details` contains the event-specific pull request, action context, comment, review, check output, check suite, workflow run, or commit status fields. comment and review bodies are included so subscribers do not need another github read just to understand the update. the durable object remembers the latest 10,000 head-sha-to-pr associations and uses them to attach status and ci events when github omits the embedded pull request list. events still include the head sha when no association has been observed yet.

## security and limits

- github hmac signatures are verified against the raw request bytes before json parsing
- payloads larger than 512 kib are rejected, bodies use one bounded buffer, and reads time out after 10 seconds
- only the configured repository is accepted
- feed tokens belong in the authorization header, never in urls
- each repository accepts at most 64 concurrent subscribers and disconnects slow readers
- normalized events are retained in durable object storage, not raw webhook payloads
- configure a cloudflare rate-limit or waf rule on `/v1/github/webhook` for additional abuse protection
- rotate either secret by deploying the new value, updating github or subscribers, and retiring the old value
- comment and review content is untrusted input and must never be treated as instructions by a babysitter
3 changes: 3 additions & 0 deletions infra/github-events/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GITHUB_REPOSITORY=pingdotgg/t3code
GITHUB_WEBHOOK_SECRET=
GITHUB_EVENTS_FEED_TOKEN=
18 changes: 18 additions & 0 deletions infra/github-events/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# github pr event feed

this package runs the hosted github webhook and replayable server-sent event feed used by pr babysitters.

it is a standalone cloudflare worker with one durable object per repository. the durable object orders deliveries, retains the latest 512 normalized events, deduplicates the latest 10,000 delivery ids, remembers recent sha-to-pr associations, replays from `last-event-id`, and fans new events out to at most 64 connected sse subscribers.

## commands

```sh
vp run --filter t3code-github-events typecheck
vp test run infra/github-events/src/*.test.ts
vp run --filter t3code-github-events dev --stage dev_$USER
vp run --filter t3code-github-events deploy --stage prod
```

copy `.env.example` to `.env` for local development. deployment requires cloudflare credentials plus the two application secrets.

see `docs/operations/github-pr-event-feed.md` for webhook setup, subscription examples, and the event schema.
42 changes: 42 additions & 0 deletions infra/github-events/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";

import type { GitHubEventHub as GitHubEventHubClass } from "./src/worker.ts";

export const GitHubEventHub = Cloudflare.DurableObject<GitHubEventHubClass>("GitHubEventHub", {
className: "GitHubEventHub",
});

export const Worker = Cloudflare.Worker("GitHubEvents", {
main: "./src/worker.ts",
compatibility: {
date: "2026-06-04",
},
env: {
GITHUB_EVENT_HUB: GitHubEventHub,
GITHUB_EVENTS_FEED_TOKEN: Config.redacted("GITHUB_EVENTS_FEED_TOKEN"),
GITHUB_REPOSITORY: Config.string("GITHUB_REPOSITORY").pipe(
Config.withDefault("pingdotgg/t3code"),
),
GITHUB_WEBHOOK_SECRET: Config.redacted("GITHUB_WEBHOOK_SECRET"),
},
});

export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;

export default Alchemy.Stack(
"T3CodeGitHubEvents",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const worker = yield* Worker;
return {
url: worker.url.as<string>(),
workerName: worker.workerName,
};
}),
);
21 changes: 21 additions & 0 deletions infra/github-events/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "t3code-github-events",
"private": true,
"type": "module",
"scripts": {
"deploy": "alchemy deploy",
"destroy": "alchemy destroy",
"dev": "alchemy dev",
"test": "vp test run",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"alchemy": "2.0.0-beta.65",
"effect": "catalog:"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260601.1",
"@types/node": "catalog:",
"vite-plus": "catalog:"
}
}
Loading
Loading