Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 92 additions & 24 deletions docs/api/forge-webhooks.md
Original file line number Diff line number Diff line change
@@ -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://<your-domain>/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. |
28 changes: 24 additions & 4 deletions server/src/api/forge-webhooks.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
9 changes: 7 additions & 2 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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/<provider> — 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.
Expand Down
Loading