#!/usr/bin/env bash # # 真实例子 4:GitHub Webhook(HMAC-SHA256 前缀形态)实测 # # 链路: ngrok/cloudflared 隧道 → GitHub 仓库事件 push 进来 → # hmac-sha256 验签(x-hub-signature-256, sha256= 前缀, hex)→ # mapping 落库 → receipts/trace → (可选)SSE # # 前置: # 1. 隧道工具(ngrok 或 cloudflared),本脚本会自动探测: # - ngrok: ngrok http 9898 (另一个终端) # - cloudflared: cloudflared tunnel --url http://localhost:9898 # 2. GitHub 仓库的管理员权限(设置 webhook) # 3. 服务已启动: INTEGRATION_VAULT_KEY=dev-secret just dev # # 用法: # GITHUB_SECRET=whsec_xxx GITHUB_PUBLIC_URL=https://xxxx.ngrok.app \ # ADMIN_TOKEN=... ./github-webhook.sh setup # 建渠道+给出 GitHub 配置指引 # GITHUB_SECRET=whsec_xxx ADMIN_TOKEN=... ./github-webhook.sh wait # 等事件 # GITHUB_SECRET=whsec_xxx ADMIN_TOKEN=... ./github-webhook.sh ping # 用 API 发 ping 事件 # set -uo pipefail BASE_URL="${BASE_URL:-http://localhost:9898/api/v1}" CMD="${1:-setup}" ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; } bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; } sec() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; } die() { printf '\033[31mFATAL\033[0m %s\n' "$1"; exit 1; } jget() { python3 -c "import sys,json;d=json.loads(sys.argv[1]);print($2)" "$1" 2>/dev/null; } [ -n "${ADMIN_TOKEN:-}" ] || die "请提供 ADMIN_TOKEN" [ -n "${GITHUB_SECRET:-}" ] || die "请提供 GITHUB_SECRET(GitHub webhook 的 Secret)" curl -sf "$BASE_URL/health" >/dev/null || die "服务未启动" req() { _RES=$(curl -s -w '\n%{http_code}' -X "${1}" "$BASE_URL${2}" \ -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \ ${3:+-d "$3"}); _CODE=$(echo "$_RES" | tail -1); _BODY=$(echo "$_RES" | sed '$d'); } case "$CMD" in setup) sec "1/2 渠道: github(hmac-sha256, GitHub 形态)" req GET "/admin/integration/channels" CH_ID=$(jget "$_BODY" "next((c['id'] for c in d['data'] if c['channel_key']=='github'), '')") CH_BODY=$(GITHUB_SECRET="$GITHUB_SECRET" python3 -c ' import json,os print(json.dumps({ "provider":"github","display_name":"GitHub Webhook", "mode":"push","transport":"http1","framing":"raw","codec":"json", "endpoint":None,"verify_kind":"hmac-sha256", "verify_config":{"header":"x-hub-signature-256","scheme":"sha256=","encoding":"hex"}, "credentials":{"secret":os.environ["GITHUB_SECRET"]}, "mapping":{ "external_id":"$._headers.x-github-delivery", "payload":{"body":"$.action | default(\"ping\")"} }, "target_type":"ingress_note", "enabled":True }, ensure_ascii=False))') if [ -n "$CH_ID" ]; then req POST "/admin/integration/channels/$CH_ID/delete" "{}" ok "old channel removed" fi BODY=$(python3 -c 'import json,sys;b=json.loads(sys.argv[1]);b["channel_key"]="github";print(json.dumps(b))' "$CH_BODY") req POST /admin/integration/channels "$BODY" CH_ID=$(jget "$_BODY" 'd["data"]["id"]') [ "$_CODE" = "200" ] && [ -n "$CH_ID" ] && ok "channel github created" || die "创建失败: $_RES" # CT target req POST /admin/content-types '{"name":"Ingress Note","singular":"ingress_note","plural":"ingress_notes","table":"ingress_notes","fields":[{"name":"external_id","field_type":"text"},{"name":"body","field_type":"text"}]}' case "$_CODE" in 200|201|409) ok "CT ingress_note ready ($_CODE)";; *) die "CT: $_RES";; esac sec "2/2 在 GitHub 上配置 webhook" PUBLIC="${GITHUB_PUBLIC_URL:-}" if [ -z "$PUBLIC" ]; then printf ' 未提供 GITHUB_PUBLIC_URL — 探测本机隧道:\n' PUBLIC=$(curl -s --max-time 3 http://127.0.0.1:4040/api/tunnels 2>/dev/null | jget "$(curl -s --max-time 3 http://127.0.0.1:4040/api/tunnels 2>/dev/null)" 'd["tunnels"][0]["public_url"]' 2>/dev/null) fi cat <