mirror of
https://codeberg.org/Toasterson/solstice-ci.git
synced 2026-04-10 13:20:41 +00:00
158 lines
5.1 KiB
Markdown
158 lines
5.1 KiB
Markdown
### 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.
|