diff --git a/docs/api/forge-webhooks.md b/docs/api/forge-webhooks.md index 16251e9b..dd62d0de 100644 --- a/docs/api/forge-webhooks.md +++ b/docs/api/forge-webhooks.md @@ -1,47 +1,115 @@ --- title: Forge Webhooks -summary: GitHub and GitLab webhook endpoints for issue sync +summary: GitHub, GitLab, and Forgejo webhook endpoints for real-time issue/PR sync --- -> **Phase 1** — These endpoints are under active development. +## Inbound Webhook Endpoints -## GitHub Webhook +Forge providers (GitHub, GitLab, Forgejo) POST events to these stateless endpoints. +Configure the matching URL in your forge's webhook settings. + +### GitHub + +``` +POST /api/forge/webhook/github +``` + +**Headers:** `X-Hub-Signature-256` (HMAC-SHA256), `X-GitHub-Event`, `X-GitHub-Delivery` + +Supported events: + +| `X-GitHub-Event` | Actions | Mapped internal type | +|------------------|--------------------|-------------------------------------------| +| `issues` | opened/closed/reopened | `issue_opened`, `issue_closed`, `issue_reopened` | +| `issue_comment` | created | `issue_comment` | +| `pull_request` | opened/closed | `pr_opened`, `pr_closed`, `pr_merged` | + +### GitLab + +``` +POST /api/forge/webhook/gitlab +``` + +**Headers:** `X-Gitlab-Event`, `X-Gitlab-Token` + +Supported events: `Issue Hook`, `Note Hook`, `Merge Request Hook` + +### Forgejo / Gitea ``` -POST /api/projects/{projectId}/webhooks/github +POST /api/forge/webhook/forgejo ``` -Receives GitHub webhook payloads for issue sync. Configure this URL in your GitHub repository's webhook settings. +**Headers:** `X-Forgejo-Event` or `X-Gitea-Event` + +Supported events: `issues`, `issue_comment`, `pull_request` -### Supported Events +--- -- `issues` — issue created, edited, closed, reopened -- `issue_comment` — comments synced to GitMesh Agents -- `pull_request` — PR events linked to agent tasks +## Management Endpoints (project-scoped) -### Setup +### List webhooks -1. Go to your GitHub repository Settings → Webhooks -2. Add the webhook URL displayed in Project Settings → Forge Connection -3. Select events: Issues, Issue comments, Pull requests -4. Set content type to `application/json` +``` +GET /api/projects/{projectId}/forge/webhooks +``` -## GitLab Webhook +### Register a webhook ``` -POST /api/projects/{projectId}/webhooks/gitlab +POST /api/projects/{projectId}/forge/webhooks ``` -Receives GitLab webhook payloads for issue sync. +Body: `{ "forgeProvider": "github", "forgeOwner": "owner", "forgeRepo": "repo", "events": [...] }` -### Supported Events +### Deactivate a webhook -- `Issue Hook` — issue created, updated, closed -- `Note Hook` — comments synced -- `Merge Request Hook` — MR events linked to agent tasks +``` +DELETE /api/projects/{projectId}/forge/webhooks/{webhookId} +``` -## MCP Endpoints +### Rotate webhook secret + +``` +POST /api/projects/{projectId}/forge/webhooks/{webhookId}/rotate +``` + +### Test webhook delivery + +``` +POST /api/projects/{projectId}/forge/webhooks/{webhookId}/test +``` + +### Connect/update forge + +``` +PATCH /api/projects/{projectId}/forge +``` + +Body: `{ "repoUrl": "https://github.com/owner/repo", "token": "ghp_..." }` + +--- + +## Setup (GitHub) + +1. Go to your GitHub repository **Settings → Webhooks → Add webhook** +2. **Payload URL**: `https:///api/forge/webhook/github` +3. **Content type**: `application/json` +4. **Secret**: use the webhook secret shown in Project Settings → Forge Integration +5. **Events**: Issues, Issue comments, Pull requests +6. Save — GitHub will send a ping event; check **Recent Deliveries** for `200 OK` + +> **Dev / localhost**: set `GITMESH_WEBHOOK_DEV_INSECURE=true` to skip HMAC verification. +> Use a tunnel (e.g. cloudflared, smee.io) and set `GITMESH_PUBLIC_BASE_URL` so GitMesh +> registers the correct callback URL with GitHub. + +--- -> **Phase 3** — Model Context Protocol integration is planned for a future release. +## Troubleshooting -MCP endpoints will allow AI agents to interact with GitMesh Agents using the Model Context Protocol standard. Details will be documented when available. +| Symptom | Fix | +|---------|-----| +| GitHub Recent Deliveries shows **404** | Callback URL is wrong. Verify it ends with `/api/forge/webhook/github` (not `/api/projects/forge/...`). Re-register the webhook. | +| **401 Invalid webhook signature** | Secret mismatch. Rotate the secret in Project Settings and update GitHub webhook settings. | +| Webhook works but agent doesn't wake | Check that an agent with a matching trigger (e.g. `on:issue_opened`) exists for the project. | +| No webhook registered | Set `GITMESH_PUBLIC_BASE_URL` to a public URL; localhost callbacks are skipped automatically. | diff --git a/server/src/api/forge-webhooks.ts b/server/src/api/forge-webhooks.ts index 2ca43a10..430b99ac 100644 --- a/server/src/api/forge-webhooks.ts +++ b/server/src/api/forge-webhooks.ts @@ -1,8 +1,15 @@ /** * Forge Webhook Routes * - * Handles incoming webhook events from forge providers (GitHub, GitLab, Forgejo) - * and management of webhook registrations. + * Split into two routers so inbound webhook callbacks from forge providers + * (GitHub, GitLab, Forgejo) are mounted at the correct path that matches the + * callback URL registered with the forge, while management endpoints remain + * project-scoped. + * + * - forgeWebhookManagementRoutes: mounted under /api/projects — CRUD for + * webhook registrations (list, register, rotate, deactivate, test). + * - forgeWebhookInboundRoutes: mounted at /api — stateless entry points that + * forge providers POST to (e.g. POST /api/forge/webhook/github). */ import { Router, type Request } from "express"; @@ -14,7 +21,7 @@ import { forgeSyncService, startPeriodicSync, type ForgeEvent, type ForgeEventTy import { assertBoard, assertProjectAccess, getActorInfo } from "./authz.js"; import { logActivity, secretService } from "../core/index.js"; -export function forgeWebhookRoutes(db: Db) { +export function forgeWebhookManagementRoutes(db: Db) { const router = Router(); const forgeSync = forgeSyncService(db); @@ -244,7 +251,20 @@ export function forgeWebhookRoutes(db: Db) { res.json({ ok: true }); }); - // ── Incoming Webhook Endpoints ─────────────────────────────────────── + return router; +} + +// ── Inbound Webhook Handlers ──────────────────────────────────────────── +// These are stateless entry points that forge providers call. They MUST be +// mounted at /api (not /api/projects) so the resolved path matches the +// callback URL registered with GitHub/GitLab/Forgejo: +// POST /api/forge/webhook/github +// POST /api/forge/webhook/gitlab +// POST /api/forge/webhook/forgejo + +export function forgeWebhookInboundRoutes(db: Db) { + const router = Router(); + const forgeSync = forgeSyncService(db); /** * POST /api/forge/webhook/github diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 277cbd71..f42d5dbf 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -1,6 +1,6 @@ export { healthRoutes } from "./health.js"; export { projectRoutes } from "./projects.js"; -export { forgeWebhookRoutes } from "./forge-webhooks.js"; +export { forgeWebhookManagementRoutes, forgeWebhookInboundRoutes } from "./forge-webhooks.js"; export { policyRoutes } from "./policies.js"; export { agentRoutes } from "./agents.js"; export { issueRoutes } from "./issues.js"; diff --git a/server/src/app.ts b/server/src/app.ts index e0488294..803b269a 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -11,7 +11,7 @@ import { operatorMutationGuard } from "./infra/middleware/operator-mutation-guar import { privateHostnameGuard, resolvePrivateHostnameAllowSet } from "./infra/middleware/private-hostname-guard.js"; import { healthRoutes } from "./api/health.js"; import { projectRoutes } from "./api/projects.js"; -import { forgeWebhookRoutes } from "./api/forge-webhooks.js"; +import { forgeWebhookManagementRoutes, forgeWebhookInboundRoutes } from "./api/forge-webhooks.js"; import { policyRoutes } from "./api/policies.js"; import { policyTemplateRoutes } from "./api/policy-templates.js"; import { agentRoutes } from "./api/agents.js"; @@ -134,7 +134,12 @@ export async function createApp( }), ); api.use("/projects", projectRoutes(db)); - api.use("/projects", forgeWebhookRoutes(db)); + api.use("/projects", forgeWebhookManagementRoutes(db)); + // Inbound forge webhook handlers are stateless entry points that forge + // providers (GitHub, GitLab, Forgejo) POST to. They MUST resolve to + // /api/forge/webhook/ — the same callback URL registered with + // the forge — so they are mounted at the api root, not under /projects. + api.use(forgeWebhookInboundRoutes(db)); api.use("/projects", policyRoutes(db)); // policyTemplateRoutes mixes top-level (`/policy-templates`) and project-scoped // (`/projects/:projectId/policies/install-template`) paths in one router.