---
id: w2/m9/t003
title: Webhook — accept GitHub-App-signed pushes (BEX_GITHUB_WEBHOOK_SECRET as second key)
worker: worker2
status: done
estimate: 30m
depends_on: [w2/m9/t002]
---

## Objective

Zero-config push-to-deploy: the bex GitHub App's webhook (configured once, app-wide, at app creation) delivers push events for every installed repo to `POST /v1/webhooks/git`; the handler verifies `X-Hub-Signature-256` against `BEX_GITHUB_WEBHOOK_SECRET` **in addition to** the existing `BEX_WEBHOOK_SECRET`, then reuses the existing matching + redeploy path unchanged.

## Context

- Handler: `lego/backend/internal/apps/webhook.go` — `validSignature(h.Secret, …)` at line ~96; 503-when-unset at ~88. Matching (repo canonicalization across URL forms, branch, rootDir, `autoDeploy`) needs no change.
- Two-key semantics: valid under either secret ⇒ accept; 503 only when *neither* secret is set; constant-time comparison for both.
- GitHub App deliveries include `X-GitHub-Event` — process `push`, respond 200-no-op to `ping`/`installation` events (don't 401 GitHub's own lifecycle events when the signature is valid).
- Env plumbing: `BEX_GITHUB_WEBHOOK_SECRET` → CLAUDE.md env table + `.env.example`/`.env.template` (m8/t001 ADR already names it).

## Steps

1. Extend signature verification to try both secrets; keep single-secret behavior byte-identical.
2. Event-type handling: `push` → existing flow; benign event types → 200 `{"ignored": "<event>"}`.
3. Tests: valid under either key, invalid under both → 401, ping accepted, neither set → 503.
4. Sync env docs/templates.

## Files

- `lego/backend/internal/apps/webhook.go` + `webhook_test.go`/`deploy_test.go`
- `CLAUDE.md`, `.env.example`, `.env.template`

## Acceptance criteria

- [ ] Existing HMAC path unchanged (old tests still pass untouched)
- [ ] GitHub-App-signed push redeploys the matching App; ping is a 200 no-op
- [ ] Env docs/templates in sync

## Out of scope

- Per-app/per-tenant webhook secrets (deferred, docs/ADR017-deploy-from-chat.md alternatives)
