solstice-ci/docs/ai/2025-10-25-forgejo-webhooks-to-jobrequest.md
Till Wegmueller a71f9cc7d1
Initial Commit
Signed-off-by: Till Wegmueller <toasterson@gmail.com>
2025-10-25 20:01:08 +02:00

5.1 KiB

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):

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:

{
  "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:

{
  "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:

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:

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.