feat(web): the Luke GitHub App's webhook keeps what an agent published current for the plan list
Follows #2068 (merged), which added coding_agent.published and the plan list's per-plan agents summary. This PR makes GitHub keep that column current, so a PR that is merged, closed, drafted, or whose checks fail shows in the sidebar row and hover card without anyone opening the agent's tab. Dean ruled out background polling and sweeps, so the update comes from GitHub App webhooks. The "written on every successful read" path from #2068 stays, so everything works before the webhook is configured.
What changes
POST /api/github/webhook(server/github-webhook-app.ts, routeserver/routes/github/webhook.ts, rewrites regenerated).-
Fails closed: with no
GITHUB_APP_WEBHOOK_SECRETset, every delivery gets503 unavailable. -
Signature check before anything else:
X-Hub-Signature-256is compared against the HMAC-SHA256 of the raw body bytes usingtimingSafeEqual. A mismatched or missing signature gets401 invalid-token, and GitHub is not asked. Deliveries are bounded at 5 MB; any method other than POST gets405. -
Which events are read:
pull_requestwith actionopened,reopened,closed,edited,synchronize,ready_for_review, orconverted_to_draft- any
check_suite check_runwith actioncompleted(branches from itspull_requests)status(commit statuses, which the checks word counts beside check runs)
pingand every other event are answered200and ignored. -
What a delivery does: from the payload, only the event, the action,
repository.full_name, and the head branch names are read. Every other key is dropped unread. The branch finds the coding agents whose row stands on it, of any account, at most ten, newest first. Each agent's PR is then re-read from GitHub on its owner's own token through the existingagentPublished, exactly as the agent tab reads it, and that read writescoding_agent.published. This keeps the aggregate checks word correct instead of guessing it from one suite's event, and it makes redeliveries idempotent. -
What leaves the service: nothing from a delivery is stored or logged. A failed re-read logs only the failure's tag.
-
readBodyBytesEffect/parseJsonBytesare split out ofreadJsonBodyEffect(same bounds, same refusals), because the signature has to be computed over the raw bytes.HostedEnvironment.githubWebhookSecretis read fromGITHUB_APP_WEBHOOK_SECRET(a blank value counts as absent, like every other secret).listCodingAgentsOnBranches(store) finds agents by repository and branch. It is the one read that names no account; each result is then re-read on its owner's own token.
Setup for Dean (once this is merged and deployed)
- Make a secret:
openssl rand -hex 32. - Vercel: in the
luke-webproject, go to Settings → Environment Variables. AddGITHUB_APP_WEBHOOK_SECRETwith that value for Production; previews don't receive the App's deliveries. Then redeploy production so the function reads it. Until this is set, deliveries are answered 503 and nothing changes. - GitHub App settings: go to ReviewStage → Settings → Developer settings → GitHub Apps → the Luke app → General.
- Webhook: check Active.
- Webhook URL:
https://tryluke.dev/api/github/webhook - Webhook secret: the same value as in step 2.
- SSL verification: enabled.
- Save.
- Permissions & events tab:
- Repository permissions → Checks: Read-only and Commit statuses: Read-only. GitHub only lets an App subscribe to check events with this permission. #2058's CI reading may already have needed it; if it is already Read-only, nothing to do. Pull requests stays as it is.
- Subscribe to events: check Pull request, Check suite, Check run, and Status.
- Save. If a permission changed, each installation owner has to accept the new permission. GitHub emails them, and they accept it in the installation's settings. Until they do, check events don't arrive for that installation; pull request events still do.
- Verify: in the App's Advanced → Recent Deliveries, the
pingdelivery should show200with{"read":0}. After the next PR event on an agent's branch, the response is{"read":1}.
Tests
apps/web/tests/github-webhook-app.test.ts, over PGlite and a scripted GitHub:
- Refusals: with no secret, every delivery is refused 503. A wrong secret, an empty signature, and a missing signature are each refused 401, with GitHub never asked and nothing written. GET is refused 405.
- Pull request deliveries: a signed merge delivery re-reads the PR and keeps
merged, and the same delivery again keeps the same value.pingreads nothing. - Check deliveries: a check run that is still running, a non-moving PR action (
labeled), a branch no agent stands on, and an unrelated event all read nothing. A completed check run (shaped like GitHub's real payload, with the suite named by id only) re-reads the checks and keepsfailing, and a commit status re-reads them again.
./scripts/check.sh exits 0. This is a service-only change with no Mac or UI surface.
🤖 Generated with Claude Code