Skip to content

App v1 Issue #4: GitHub App Webhook 接收 + 验签 + Upstash QStash 入队 + installation 落表 #169

Closed

Description

@akushonkamen

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"
  • 步骤:
    1. 读 raw body(用 await request.text(),不要 .json() — 验签需要原始字节)
    2. 读 X-Hub-Signature-256 header + X-GitHub-Event header + X-GitHub-Delivery header
    3. 验签(用 WEBHOOK_SECRET env)。失败 → 401,不打印 secret(S4)
    4. 去重:用 X-GitHub-Delivery ID 作为幂等 key 写 Upstash SETNX(TTL 24h)。已存在 → 200 + dedup: true
    5. 路由:
      • installation / installation_repositories → 直接落表 installations(同步处理,因为简单且为后续事件准备 tenant_id)
      • issues.opened / pull_request.* / label.* / issue_comment → 入 QStash 队列(异步处理,避免 Vercel 函数超时 — PRD R6)
      • 其他事件 → 200 + ignored: true
    6. 返回 200 + JSON { ok: true, event, deliveryId, queued: <count> }
  • 错误处理:未捕获异常 → 500,但不回 500 让 GitHub 重试 webhook(避免重试风暴);除非验签失败

app/src/lib/qstash.ts(NEW)

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/vercel.json(NEW,可选)

  • 配置 /api/webhook/github route 的 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-Delivery ID 在 Upstash Redis 去重(SETNX + TTL 24h)
  • installation.created 事件 upsert installations + 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

References

Activity

  1. github-actions commented on Jul 18, 2026

    @github-actions
    Contributor

    Triage: Accepted ✅

    Thanks for the detailed spec. This is a well-scoped feature implementing the webhook ingestion pipeline for the SaaS app:

    • POST /api/webhook/github — raw body HMAC verification (timing-safe), dedup via Upstash SETNX, event routing
    • Installation events → sync upsert to installations + tenants tables (soft-delete for deleted installs)
    • Async events (issues, PRs, labels, comments) → enqueue to QStash for worker in [bug] handle-triage.sh 在 macOS 下 timeout 检测失败 #5
    • Env vars — WEBHOOK_SECRET, QSTASH_URL, QSTASH_TOKEN, APP_PRIVATE_KEY_BASE64
    • Unit tests on verify + dedup, CI green via app-ci workflow

    Non-goals understood: QStash worker / runs table / BYOK / OAuth callback / replay UI / rate limiting — all deferred to later issues.

    Label accepted already applied, so this is ready for the pipeline.

  2. github-actions commented on Jul 18, 2026

    @github-actions
    Contributor

    Triage: Accepted ✅

    Thanks for the detailed spec. This is a well-scoped feature implementing the webhook ingestion pipeline for the SaaS app:

    • POST /api/webhook/github — raw body HMAC verification (timing-safe), dedup via Upstash SETNX, event routing
    • Installation events → sync upsert to installations + tenants tables (soft-delete for deleted installs)
    • Async events (issues, PRs, labels, comments) → enqueue to QStash for worker in [bug] handle-triage.sh 在 macOS 下 timeout 检测失败 #5
    • Env vars — WEBHOOK_SECRET, QSTASH_URL, QSTASH_TOKEN, APP_PRIVATE_KEY_BASE64
    • Unit tests on verify + dedup, CI green via app-ci workflow

    Non-goals understood: QStash worker / runs table / BYOK / OAuth callback / replay UI / rate limiting — all deferred to later issues.

    Label accepted already applied, so this is ready for the pipeline.

  3. added
    accepted-by-claudeClaude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)
    on Jul 18, 2026
  4. akushonkamen commented on Jul 18, 2026

    @akushonkamen
    OwnerAuthor

    🤖 Claude self-acceptance (M8 workload=standard). accepted-by-claude applied — Module 4 develop-gate will pick this up. Maintainer may override with rejected.

  5. github-actions commented on Jul 18, 2026

    @github-actions
    Contributor

    ⚠️ Module 4 produced no new commits (issue #57).

    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:

    • Re-triage the issue — the acceptance criteria may already be satisfied by existing code
    • OR refine the issue body and re-dispatch code-generate.yml
    • OR close as not-actionable

    Workflow run

  6. added
    verifyingModule 5 self-verify active
    verify:retry-1Verify retry 1/3: self-verify failed, re-running Module 4 to fix
    and removed
    verifyingModule 5 self-verify active
    on Jul 18, 2026
  7. akushonkamen commented on Jul 18, 2026

    @akushonkamen
    OwnerAuthor

    🔄 Verify retry 1/3

    Field Value
    Retry 1 of 3
    PR #170
    Prior workflow run https://github.com/akushonkamen/github-auto-dev-scaffold/actions/runs/29648579486
    Retry label verify:retry-1

    code-generate.yml will auto-trigger on the verify:retry-1 label,
    fetch this comment as the prior-verify-report, and re-run Module 4 to
    address the verify failure.

  8. added
    verifyingModule 5 self-verify active
    and removed
    verifyingModule 5 self-verify active
    verify:retry-1Verify retry 1/3: self-verify failed, re-running Module 4 to fix
    on Jul 18, 2026
  9. 6 remaining items

  10. added
    verify:retry-3Verify retry 3/3: self-verify failed, re-running Module 4 to fix
    on Jul 18, 2026
  11. akushonkamen commented on Jul 18, 2026

    @akushonkamen
    OwnerAuthor

    🔄 Verify retry 3/3

    Field Value
    Retry 3 of 3
    PR #170
    Prior workflow run https://github.com/akushonkamen/github-auto-dev-scaffold/actions/runs/29648987312
    Retry label verify:retry-3

    code-generate.yml will auto-trigger on the verify:retry-3 label,
    fetch this comment as the prior-verify-report, and re-run Module 4 to
    address the verify failure.

  12. akushonkamen commented on Jul 18, 2026

    @akushonkamen
    OwnerAuthor

    🚀 Development branch pushed: claude/issue-169-app-v1-webhook-verify-qstash-installation @ ddb735fb01ea. PR: #172

  13. github-actions commented on Jul 18, 2026

    @github-actions
    Contributor

    ⚠️ Module 4 retry produced no new commits (issue #57).

    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 for pull_request.synchronize to deliver.

    Maintainer action required:

    • Inspect the existing PR — the fix code may already be correct and the prior test failure spurious
    • OR re-dispatch code-generate.yml with stronger guidance in the issue
    • OR close as not-reproducible

    Workflow run

  14. added
    verifyingModule 5 self-verify active
    and removed
    verifyingModule 5 self-verify active
    on Jul 20, 2026
  15. akushonkamen commented on Jul 20, 2026

    @akushonkamen
    OwnerAuthor

    ❌ Verify retries exhausted (3/3)

    Module 5 failed 3 time(s). The pipeline cannot automatically recover.
    A maintainer must triage manually.

    Field Value
    PR #172
    Last workflow run https://github.com/akushonkamen/github-auto-dev-scaffold/actions/runs/29724004888
    Retry labels triage,accepted,stage:failed,accepted-by-claude,verify:retry-3
  16. added
    verifyingModule 5 self-verify active
    and removed
    verifyingModule 5 self-verify active
    on Jul 20, 2026
  17. akushonkamen commented on Jul 20, 2026

    @akushonkamen
    OwnerAuthor

    ❌ Verify retries exhausted (3/3)

    Module 5 failed 3 time(s). The pipeline cannot automatically recover.
    A maintainer must triage manually.

    Field Value
    PR #172
    Last workflow run https://github.com/akushonkamen/github-auto-dev-scaffold/actions/runs/29725154340
    Retry labels triage,accepted,stage:failed,accepted-by-claude,verify:retry-3
  18. added
    verifyingModule 5 self-verify active
    and removed
    verifyingModule 5 self-verify active
    on Jul 20, 2026
  19. akushonkamen commented on Jul 20, 2026

    @akushonkamen
    OwnerAuthor

    ❌ Verify retries exhausted (3/3)

    Module 5 failed 3 time(s). The pipeline cannot automatically recover.
    A maintainer must triage manually.

    Field Value
    PR #172
    Last workflow run https://github.com/akushonkamen/github-auto-dev-scaffold/actions/runs/29725173742
    Retry labels triage,accepted,stage:failed,accepted-by-claude,verify:retry-3
  20. added a commit that references this issue on Jul 21, 2026
    f11f2b9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedAccepted for development (Module 4 trigger)accepted-by-claudeClaude self-acceptance after clarify loop (Module 4 trigger, distinct from maintainer accepted)stage:failedA module failed; needs maintainer triagetriageNewly opened; awaiting triage botverify:retry-3Verify retry 3/3: self-verify failed, re-running Module 4 to fix

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions