Skip to content

feat(server): GitHub webhook receiver triggers source sync (#30)#60

Merged
mroops0111 merged 3 commits into
masterfrom
feat/github-webhook-trigger-sync
Jun 22, 2026
Merged

feat(server): GitHub webhook receiver triggers source sync (#30)#60
mroops0111 merged 3 commits into
masterfrom
feat/github-webhook-trigger-sync

Conversation

@mroops0111

Copy link
Copy Markdown
Owner

Closes #30.

Summary

  • Adds a public POST /webhooks/github/:workspaceId/:sourceId receiver. Verifies the per-source HMAC, confirms repository.full_name matches the source's configured owner/repo, then fires sourceLoaderRunner.syncOne in the background and returns 202 so GitHub's ~10s delivery timeout cannot trip on slow syncs.
  • Workspace-scoped admin endpoints (GET / POST rotate) under /workspaces/:ws/source-webhooks/:sourceId/github let Studio surface the payload URL and mint a fresh 32-byte hex secret. The secret is returned exactly once on rotate; subsequent GETs report only hasSecret + createdAt.
  • Introduces SourceLoaderPlugin.webhook capability (repoIdentity + shouldDispatch). The receiver becomes loader-agnostic and delegates both questions to the plugin, so adding a new webhook-capable loader needs no receiver edit (OCP).
  • github loader dispatches issues / issue_comment / ping events. git loader dispatches push events whose ref matches config.branch (or main / master as the unset default).

Security posture

  • All pre-signature failures collapse to a uniform 401 invalid signature so anonymous callers cannot enumerate workspace ids, source ids, or which sources are webhook-armed by reading distinct error messages.
  • /webhooks/github/ is the only prefix exempt from the Bearer auth middleware (narrower than a blanket /webhooks/), so a future debug / admin endpoint added under /webhooks/ does not silently inherit the anonymous bypass.
  • The receiver requires both X-Hub-Signature-256 and X-GitHub-Event; missing either is rejected rather than silently dropped.
  • payload.repository.full_name is required and compared case-insensitively (GitHub repo names are case-insensitive on the wire).

Test plan

  • pnpm --filter @braidhq/server test — 292 tests pass, including 17 receiver + admin tests covering: valid HMAC dispatches syncOne; bad HMAC → 401; missing X-Hub-Signature-256 / X-GitHub-Event → 401 / 400; missing or mismatched repository.full_name → 400; uppercase full_name matches; ref filter on the git loader; push event on a github-issues loader skipped; uniform 401 for unknown workspace / source / no secret; rotate persists + replaces; status GET never returns the secret.
  • pnpm --filter @braidhq/source-loader-git test — 14 tests, including new webhook-capability suite (https/ssh URL parsing, trailing slash + uppercase host + git+https:// + ?query tolerance, ${VAR} placeholder not throwing, push-ref filter, main and master default).
  • pnpm --filter @braidhq/source-loader-github test — 9 tests, including new webhook-capability suite (issues / issue_comment / ping dispatched, push skipped).
  • pnpm --filter @braidhq/studio typecheck — clean.
  • pnpm lint — clean.
  • Dogfood E2E: Ran ngrok against localhost:4321, registered two GitHub webhooks on mroops0111/braid (one per source), and verified end to end:
    • Issues source: editing issue feat(server): add GitHub webhook endpoint to trigger source sync #30 fires issues.edited → 202 → intents/issues/issues/30.md updated within seconds.
    • Code source: pushing to feat/github-webhook-trigger-sync fires push → 202 with {skipped: true} (feat branch ≠ tracked main/master).
    • Anonymous probes against /webhooks/github/<guess>/<guess> with bad signature / missing header / fake workspace / fake source all return identical 401 invalid signature.

Notes for review

  • Three commits on the branch: initial receiver (feat(server): add GitHub webhook endpoint to trigger source sync #30 scope), the OCP refactor that lifts webhook handling into SourceLoaderPlugin.webhook, and the /code-review max follow-up fixes.
  • Admin endpoints use OpenAPIHono to match the convention shared by workspaces / batch / skills / history so the rotate / status surface appears in /openapi.json.
  • The secret namespace mirrors the existing oauth-google / oauth-github layout (webhook-github, key <workspaceId>--<sourceId>).

🤖 Generated with Claude Code

mroops0111 and others added 3 commits June 22, 2026 00:12
Add public POST /webhooks/github/:workspaceId/:sourceId. Verifies the
HMAC against a per-source secret, confirms repository.full_name matches
the source's configured owner/repo, then fires sourceLoaderRunner.syncOne
in the background and returns 202 so GitHub's 10s delivery timeout cannot
trip on slow sync.

Workspace-scoped admin routes (GET / POST rotate) live under
/workspaces/:ws/source-webhooks/:sourceId/github and surface the URL +
secret rotation to Studio. The secret returns exactly once on rotate;
subsequent reads only see hasSecret + createdAt.

Studio renders a GitHub webhook panel under github-loader AND git-loader
filesystem sources (both can receive deliveries from a GitHub repo;
the git URL is parsed to extract owner/repo for the repo-match check).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ract

The receiver previously hard-coded github vs git loader branches and
the ref-vs-branch check, violating OCP: adding a new loader required
editing the route. Move the contract into `SourceLoaderPlugin.webhook`
with two methods the plugin owns: `repoIdentity(config)` resolves the
provider-owner-repo triple for full_name matching, and
`shouldDispatch(config, delivery)` decides whether a verified delivery
should fire syncOne.

github and git loaders now declare their own event filters
(issues/issue_comment/ping vs push-to-tracked-branch respectively).
The receiver becomes loader-agnostic and delegates both questions.

Studio's GitHub webhook panel now gates on the server's source-loaders
listing (which carries a new \`webhook: boolean\` flag) instead of
hardcoding \`loader.kind\`, so a new webhook-capable loader surfaces the
panel automatically.

Each loader package gains a unit test covering its \`webhook\` capability
in isolation; server tests use inline fake plugins to exercise the
receiver's contract without coupling to specific loader semantics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verified findings from /code-review max on PR #30.

Receiver hardening (packages/server/src/routes/sourceWebhooks.ts):
- #5 wrap pre-signature flow in a single try/catch returning uniform
  401 so a ZodError on schema drift, an unknown workspace, or a
  thrown plugin method cannot leak detail to anonymous callers
- #10 collapse every pre-signature failure into 401 'invalid signature'
  so anonymous probes cannot enumerate workspace / source ids
- #3 require X-GitHub-Event; missing header is 400, not a silent skip
- #2 require payload.repository.full_name; compare case-insensitively
- #12 admin returns 503 when apiUrl is unset rather than a relative URL
- #15 convert admin router to OpenAPIHono to match the convention used
  by workspaces / batch / skills / history

Loader (packages/source-loader-git/src/GitLoader.ts):
- #1 webhook.shouldDispatch accepts both main and master when branch is
  unset (default-branch heuristic) since the loader's sync follows HEAD
- #6 drop interpolateEnv in repoIdentity so a missing env var no longer
  throws or leaks the env var name on an anonymous endpoint
- #11 parseGithubUrl tolerates trailing slash, uppercase host,
  git+https://, query / fragment portions

Studio (packages/studio/src/components/WorkspaceDetailsSheet.tsx):
- #8 clear revealedSecret on panel collapse and on rotate onMutate so
  the "shown once" claim holds

Auth (packages/server/src/middleware/auth.ts):
- #14 narrow PUBLIC_PREFIXES from '/webhooks/' to '/webhooks/github/'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mroops0111 mroops0111 merged commit c9f1f56 into master Jun 22, 2026
6 checks passed
@mroops0111 mroops0111 deleted the feat/github-webhook-trigger-sync branch June 22, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(server): add GitHub webhook endpoint to trigger source sync

1 participant