Repository navigation
App v1 Issue #4: GitHub App Webhook 接收 + 验签 + Upstash QStash 入队 + installation 落表 #169
Copy link
Copy link
Closed
Labels
acceptedAccepted for development (Module 4 trigger)Accepted for development (Module 4 trigger)accepted-by-claudeClaude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)Claude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)stage:failedA module failed; needs maintainer triageA module failed; needs maintainer triagetriageNewly opened; awaiting triage botNewly opened; awaiting triage botverify:retry-3Verify retry 3/3: self-verify failed, re-running Module 4 to fixVerify retry 3/3: self-verify failed, re-running Module 4 to fix
Description
Background
App v1 Issue #4 — 实现 GitHub App webhook 接收 + 验签 + 入队(Upstash QStash)+ installation 事件落表(PRD docs/prd-app-v1.md §4.1 + §6 S10/S11 + US-004)。地基是 #164 (NextAuth 登录) + #162 (Postgres schema)。
Webhook 是整个 SaaS 的入口:GitHub 事件 → 验签 → 落表 → 触发引擎。Issue #5(workflow_dispatch)会消费这里入队的事件。
Context budget paths
允许 AI 读写:
app/src/app/api/webhook/github/route.ts(NEW — webhook 接收 + 验签)app/src/lib/webhook-verify.ts(NEW — X-Hub-Signature-256 验签)app/src/lib/qstash.ts(NEW — Upstash QStash client + enqueue helpers)app/src/lib/github-events.ts(NEW — 事件类型 + 路由表)app/src/lib/installations.ts(NEW — installation events 落表 helpers)app/src/db/queries/(NEW — installation upsert queries)app/.env.example(加 QStash + App 私钥 env)app/src/auth/with-app-installer.ts(如 app-v1 Issue #3: GitHub App OAuth + NextAuth 登录链路 #164 已建则扩展,否则一并建)docs/app-architecture.md(更新 §4.1 webhook 段)
禁止扫:py/、rust/、.github/workflows/(除了 .github/workflows/app-ci.yml)、.github/actions/。
Files + lines
app/src/lib/webhook-verify.ts(NEW)
import crypto from "node:crypto";
export function verifyGitHubWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// signatureHeader 形如 "sha256=<hex>"
if (!signatureHeader.startsWith("sha256=")) return false;
const expected = signatureHeader.slice("sha256=".length);
const hmac = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// timing-safe compare 防 timing attack
const a = Buffer.from(expected, "hex");
const b = Buffer.from(hmac, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}app/src/app/api/webhook/github/route.ts(NEW)
- POST handler,强制
export const runtime = "nodejs"+dynamic = "force-dynamic" - 步骤:
- 读 raw body(用
await request.text(),不要.json()— 验签需要原始字节) - 读
X-Hub-Signature-256header +X-GitHub-Eventheader +X-GitHub-Deliveryheader - 验签(用
WEBHOOK_SECRETenv)。失败 → 401,不打印 secret(S4) - 去重:用
X-GitHub-DeliveryID 作为幂等 key 写 Upstash SETNX(TTL 24h)。已存在 → 200 +dedup: true - 路由:
installation/installation_repositories→ 直接落表installations(同步处理,因为简单且为后续事件准备 tenant_id)issues.opened/pull_request.*/label.*/issue_comment→ 入 QStash 队列(异步处理,避免 Vercel 函数超时 — PRD R6)- 其他事件 → 200 +
ignored: true
- 返回 200 + JSON
{ ok: true, event, deliveryId, queued: <count> }
- 读 raw body(用
- 错误处理:未捕获异常 → 500,但不回 500 让 GitHub 重试 webhook(避免重试风暴);除非验签失败
app/src/lib/qstash.ts(NEW)
- 用
@upstash/qstash(已在 [app-v1] Next.js 14 脚手架 + CI(monorepo app/ 目录) #156 deps 里)的Client enqueue(eventType: string, payload: unknown): Promise<{ messageId: string }>— 投递到 QStash- QStash topic URL =
/api/webhook/github/worker(后续 Issue 处理) - 当前 Issue 只入队,不实现 worker(worker 是 [bug] handle-triage.sh 在 macOS 下 timeout 检测失败 #5)
app/src/lib/github-events.ts(NEW)
export const ASYNC_EVENTS = [
"issues.opened",
"issues.labeled",
"issues.unlabeled",
"issue_comment.created",
"pull_request.opened",
"pull_request.labeled",
"pull_request.ready_for_review",
"pull_request_review.submitted",
"label.created",
"label.deleted",
] as const;
export const SYNC_EVENTS = [
"installation.created",
"installation.deleted",
"installation_repositories.added",
"installation_repositories.removed",
] as const;app/src/lib/installations.ts(NEW)
upsertInstallationFromEvent(payload: InstallationEvent): Promise<void>— installation.created/removed 落installations表- 同时 upsert
tenants行(用 installation.account.id + login) - installation.deleted → 软删除(设
uninstalled_at = now(),不真删行 — 保留 audit trail)
app/.env.example(MODIFY)
- 加:
WEBHOOK_SECRET=(注释:与 GitHub App webhook secret 一致)QSTASH_URL=(注释:从 Upstash console 拿)QSTASH_TOKEN=APP_PRIVATE_KEY_BASE64=(注释:base64 of PEM file, decode at runtime — 避免 PEM 换行被环境变量吃掉)
app/src/auth/with-app-installer.ts(NEW 或 MODIFY #164)
- 如果 app-v1 Issue #3: GitHub App OAuth + NextAuth 登录链路 #164 没建则建;建了则扩展
getAppInstallationToken(installationId)用APP_PRIVATE_KEY_BASE64解码后签 JWT
app/vercel.json(NEW,可选)
- 配置
/api/webhook/githubroute 的maxDuration = 30(Vercel Pro)/ 10(Hobby)
docs/app-architecture.md(MODIFY)
- §4.1 鉴权后加 webhook 段:
GitHub → POST /api/webhook/github → verify → dedup → enqueue → worker (#5)
Acceptance criteria
-
POST /api/webhook/github接收 raw body 并用 timing-safe HMAC 验签 - 验签失败 → 401,secret 不进日志(S4)
- 用
X-GitHub-DeliveryID 在 Upstash Redis 去重(SETNX + TTL 24h) -
installation.created事件 upsertinstallations+tenants行 -
installation.deleted设uninstalled_at,不删行 -
issues.opened等异步事件入 QStash 队列 - 未识别事件返回 200 +
ignored: true(不报错) -
app/.env.example含 4 个新 env 变量占位 -
pnpm install/lint/typecheck/build全绿(无 QSTASH_TOKEN 也能跑 build) - 单元测试覆盖验签函数 + 幂等检查(Vitest,新增 test 依赖 — 这是 [app-v1] Postgres schema + Drizzle ORM(5 张表 + migration + db client) #162 deferred 的)
- CI 跑通:app-ci workflow 全绿
Context / non-goals
- 不实现 QStash worker(消费队列 → 触发 workflow_dispatch,是 [bug] handle-triage.sh 在 macOS 下 timeout 检测失败 #5 范围)
- 不实现 issue 落
runs表逻辑(issues.opened入队后由 [bug] handle-triage.sh 在 macOS 下 timeout 检测失败 #5 worker 处理时落表) - 不实现 BYOK key 注入(make the app faster #8)
- 不处理 GitHub App OAuth callback(app-v1 Issue #3: GitHub App OAuth + NextAuth 登录链路 #164 范围)
- 不做 webhook 回放 UI(v2)
- 不做 rate limit(v2 — 当前依赖 Upstash 自身 limit)
- Vercel 函数超时对策:所有非 installation 事件都异步入队,handler 平均响应时间 < 1s
- 不在 CI 测真实 GitHub webhook(需要真实 WEBHOOK_SECRET + 完整事件 payload;CI 只验证编译 + 单元测试)
References
docs/prd-app-v1.md§4.1(分层 — webhook 是 API Routes 入口)docs/prd-app-v1.md§6 S10(webhook 验签强制)+ S11(installation_token 短期性)+ S12(BYOK 不回传)docs/prd-app-v1.mdUS-004(验收标准)docs/prd-app-v1.mdR6(Vercel 函数超时对策 = Upstash Queue)- GitHub webhook docs: https://docs.github.com/en/webhooks/secure-webhooks
- QStash docs: https://upstash.com/docs/qstash
- 现有
app/package.json([app-v1] Next.js 14 脚手架 + CI(monorepo app/ 目录) #156 装了 @upstash/qstash + @upstash/redis)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
acceptedAccepted for development (Module 4 trigger)Accepted for development (Module 4 trigger)accepted-by-claudeClaude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)Claude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)stage:failedA module failed; needs maintainer triageA module failed; needs maintainer triagetriageNewly opened; awaiting triage botNewly opened; awaiting triage botverify:retry-3Verify retry 3/3: self-verify failed, re-running Module 4 to fixVerify retry 3/3: self-verify failed, re-running Module 4 to fix
Activity
github-actions commentedon Jul 18, 2026
Triage: Accepted ✅
Thanks for the detailed spec. This is a well-scoped feature implementing the webhook ingestion pipeline for the SaaS app:
installations+tenantstables (soft-delete for deleted installs)WEBHOOK_SECRET,QSTASH_URL,QSTASH_TOKEN,APP_PRIVATE_KEY_BASE64Non-goals understood: QStash worker / runs table / BYOK / OAuth callback / replay UI / rate limiting — all deferred to later issues.
Label
acceptedalready applied, so this is ready for the pipeline.github-actions commentedon Jul 18, 2026
Triage: Accepted ✅
Thanks for the detailed spec. This is a well-scoped feature implementing the webhook ingestion pipeline for the SaaS app:
installations+tenantstables (soft-delete for deleted installs)WEBHOOK_SECRET,QSTASH_URL,QSTASH_TOKEN,APP_PRIVATE_KEY_BASE64Non-goals understood: QStash worker / runs table / BYOK / OAuth callback / replay UI / rate limiting — all deferred to later issues.
Label
acceptedalready applied, so this is ready for the pipeline.akushonkamen commentedon Jul 18, 2026
🤖 Claude self-acceptance (M8 workload=standard).
accepted-by-claudeapplied — Module 4 develop-gate will pick this up. Maintainer may override withrejected.github-actions commentedon Jul 18, 2026
Claude analysed the issue and concluded no code change is required —
pre-sha == post-sha, so no commit was made. Downstream modules (pr-lifecycle → verify) will NOT fire.Maintainer action required:
code-generate.ymlWorkflow run
akushonkamen commentedon Jul 18, 2026
🔄 Verify retry 1/3
code-generate.yml will auto-trigger on the
verify:retry-1label,fetch this comment as the prior-verify-report, and re-run Module 4 to
address the verify failure.
6 remaining items
akushonkamen commentedon Jul 18, 2026
🔄 Verify retry 3/3
code-generate.yml will auto-trigger on the
verify:retry-3label,fetch this comment as the prior-verify-report, and re-run Module 4 to
address the verify failure.
akushonkamen commentedon Jul 18, 2026
🚀 Development branch pushed:
claude/issue-169-app-v1-webhook-verify-qstash-installation@ddb735fb01ea. PR: #172github-actions commentedon Jul 18, 2026
Claude analysed the prior test report and concluded the existing branch tip already addresses it —
pre-sha == post-sha, so no commit was made. Downstream modules (pr-lifecycle → verify) will NOT fire because there is no new push event forpull_request.synchronizeto deliver.Maintainer action required:
code-generate.ymlwith stronger guidance in the issueWorkflow run
akushonkamen commentedon Jul 20, 2026
❌ Verify retries exhausted (3/3)
Module 5 failed 3 time(s). The pipeline cannot automatically recover.
A maintainer must triage manually.
akushonkamen commentedon Jul 20, 2026
❌ Verify retries exhausted (3/3)
Module 5 failed 3 time(s). The pipeline cannot automatically recover.
A maintainer must triage manually.
akushonkamen commentedon Jul 20, 2026
❌ Verify retries exhausted (3/3)
Module 5 failed 3 time(s). The pipeline cannot automatically recover.
A maintainer must triage manually.
feat(app-v1): Issue akushonkamen#4 webhook 接收 + 验签 + QStash 队列