### Forgejo Webhooks → JobRequest Mapping (Integration Layer) This document explains how the Forge Integration service maps real Forgejo (Gitea-compatible) webhooks to our internal `JobRequest` messages and publishes them to RabbitMQ. --- ### Overview - Service: `crates/forge-integration` - Endpoint: `POST /webhooks/forgejo` (configurable via `WEBHOOK_PATH`) - Auth: HMAC-SHA256 validation using `WEBHOOK_SECRET` - Events handled: `push`, `pull_request` (`opened`, `synchronize`, `reopened`) - Output: `JobRequest` (JSON) published to exchange `solstice.jobs` with routing key `jobrequest.v1` --- ### Headers and Security - Event type header: `X-Gitea-Event` (or `X-Forgejo-Event`) - Delivery ID header (optional, for logs): `X-Gitea-Delivery` - Signature header: `X-Gitea-Signature` (or `X-Forgejo-Signature`) - Value is lowercase hex of `HMAC_SHA256(secret, raw_request_body)` - If `WEBHOOK_SECRET` is set, the service requires a valid signature and returns `401` on mismatch/missing header. - If unset, the service accepts requests (dev mode) and logs a warning. Signature example (shell): ```bash SECRET=your_shared_secret BODY='{"after":"deadbeef", "repository":{}}' SIG=$(printf %s "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256) # Send: curl -sS -X POST http://127.0.0.1:8080/webhooks/forgejo \ -H "Content-Type: application/json" \ -H "X-Gitea-Event: push" \ -H "X-Gitea-Signature: $SIG" \ --data "$BODY" ``` --- ### Payload Mapping → JobRequest We only deserialize the minimal fields required to construct a `JobRequest`. Unused fields are ignored. - Push event (`X-Gitea-Event: push`): - `repo_url` <- `repository.clone_url` (fallback `repository.ssh_url`) - `commit_sha` <- `after` - Ignore branch deletions where `after` is all zeros - `source` = `forgejo` Minimal push payload shape: ```json { "after": "0123456789abcdef0123456789abcdef01234567", "repository": { "clone_url": "https://forge.example.com/org/repo.git", "ssh_url": "ssh://git@forge.example.com:2222/org/repo.git" } } ``` - Pull request event (`X-Gitea-Event: pull_request`): - Only actions: `opened`, `synchronize`, `reopened` (others → 204 No Content) - `repo_url` <- `pull_request.head.repo.clone_url` (fallback `ssh_url`) - `commit_sha` <- `pull_request.head.sha` - `source` = `forgejo` Minimal PR payload shape: ```json { "action": "synchronize", "pull_request": { "head": { "sha": "89abcdef0123456789abcdef0123456789abcd", "repo": { "clone_url": "https://forge.example.com/org/repo.git", "ssh_url": "ssh://git@forge.example.com:2222/org/repo.git" } } } } ``` `JobRequest` fields set now: - `schema_version = "jobrequest.v1"` - `request_id = Uuid::v4()` - `source = forgejo` - `repo_url` as above - `commit_sha` as above - `workflow_path = null` (may be inferred later) - `workflow_job_id = null` - `runs_on = null` (future enhancement to infer) - `submitted_at = now(UTC)` --- ### AMQP Publication - Exchange: `solstice.jobs` (durable, direct) - Routing key: `jobrequest.v1` - Queue: `solstice.jobs.v1` (declared by both publisher and consumer) - DLX/DLQ: `solstice.dlx` / `solstice.jobs.v1.dlq` - Publisher confirms enabled; messages are persistent (`delivery_mode = 2`). Env/CLI (defaults): - `AMQP_URL=amqp://127.0.0.1:5672/%2f` - `AMQP_EXCHANGE=solstice.jobs` - `AMQP_ROUTING_KEY=jobrequest.v1` - `AMQP_QUEUE=solstice.jobs.v1` - `AMQP_DLX=solstice.dlx` - `AMQP_DLQ=solstice.jobs.v1.dlq` --- ### Configuration - HTTP address: `HTTP_ADDR` (default `0.0.0.0:8080`) - Webhook path: `WEBHOOK_PATH` (default `/webhooks/forgejo`) - Shared secret: `WEBHOOK_SECRET` (required in prod) - AMQP settings: see above Example run: ```bash export WEBHOOK_SECRET=devsecret cargo run -p orchestrator & cargo run -p forge-integration -- --http-addr 0.0.0.0:8080 --webhook-path /webhooks/forgejo ``` --- ### Forgejo Setup 1. In the repository Settings → Webhooks, add a new webhook: - Target URL: `http://:8080/webhooks/forgejo` - Content type: `application/json` - Secret: your `WEBHOOK_SECRET` - Events: check "Just the push event" and "Pull request events" (or their equivalents) 2. Save and use "Test Delivery" to verify a 202 response. --- ### Local Verification via curl Create a minimal push body and compute signature: ```bash SECRET=devsecret BODY='{"after":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef","repository":{"clone_url":"https://example/repo.git"}}' SIG=$(printf %s "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256) curl -i -X POST http://127.0.0.1:8080/webhooks/forgejo \ -H "Content-Type: application/json" \ -H "X-Gitea-Event: push" \ -H "X-Gitea-Signature: $SIG" \ --data "$BODY" ``` You should see `HTTP/1.1 202 Accepted`, and the Orchestrator should log a received `JobRequest`. --- ### Notes & Next Steps - Add commit status updates back to Forgejo (`pending` on receipt; `success`/`failure` on completion). - Consider repo allowlist/branch filters to reduce noise. - Add idempotency keyed by `X-Gitea-Delivery` + `repo+sha` to avoid duplicate enqueues. - Optional: prefer SSH URLs via config if your Orchestrator uses SSH keys for fetch.