solstice-ci/docs/ai/2025-10-25-forgejo-webhooks-to-jobrequest.md

159 lines
5.1 KiB
Markdown
Raw Normal View History

### 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://<your-host>: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.