Webhooks - Forgejo Webhooks for CI Triggers

Status: Active Last Updated: 2026-08-26 Category: CI/CD - Integration Prerequisites: forgejo-installation, woodpecker-installation Time: 1 hour Tags: forgejo, webhooks, woodpecker, security, debugging

Summary

Webhooks are how Forgejo tells Woodpecker (and anything else in your stack) that something happened: a push landed, a PR opened, a tag was cut. This article covers webhook anatomy, configuring them with secrets, the payload shapes that matter for CI, and a systematic approach to debugging failed deliveries.

🎯 What You'll Learn

By the end of this article, you'll be able to:


Context / Why This Matters

Every push-triggered pipeline you've run so far exists because of one HTTP POST. When "CI didn't run," the root cause is almost always at the webhook layer β€” wrong URL, secret mismatch, TLS failure, or an event type you didn't subscribe to. Understanding deliveries turns flaky CI triggers into diagnosable systems.


Implementation / Core Content

How Woodpecker Registers Its Own Webhook

When you enable a repo through Woodpecker's UI using the Forgejo OAuth integration, Woodpecker creates the webhook for you on POST /repos/{owner}/{repo}/hooks. It points to:

https://ci.example.com/api/hooks?repo=acme-corp/api-service

and subscribes to push, pull_request, tag events. If it's missing or broken, first re-sync from Woodpecker: Repo β†’ Settings β†’ "Repair repository" β€” this recreates hooks without touching config.

Manual registration is only needed for non-Woodpecker receivers (alerting scripts, deploy bots):

Repo β†’ Settings β†’ Webhooks β†’ Add Webhook β†’ Forgejo/Gitea:

Field Value
Target URL e.g. https://deploy-bot.internal/hooks/forgejo
Method POST
Trigger On Custom events… (pick explicitly)
Secret Random β‰₯32 chars: openssl rand -hex 32

Event Types That Matter for CI

Event Typical use
push Build/test on every commit; deploy on default branch
pull_request PR validation (opened, synchronize, reopened)
tag / release Versioned builds, image publishes
issue_comment Chat-ops style /retest commands
repository Sync org-level automation when repos change

Payload Anatomy (push)

Trimmed but representative:

{
  "ref": "refs/heads/main",
  "before": "a1b2c3...",
  "after": "d4e5f6...",
  "compare_url": "https://git.example.com/acme-corp/api-service/compare/a1b2c3...d4e5f6",
  "commits": [
    {
      "id": "d4e5f6...",
      "message": "fix: handle empty queue\n\nPipeline-Run: full",
      "author": { "name": "Alice", "email": "alice@example.com" }
    }
  ],
  "head_commit": { "id": "d4e5f6..." },
  "repository": { "full_name": "acme-corp/api-service", "default_branch": "main" },
  "sender": { "login": "alice" },
  "secret_hash": "<hmac-sha256 hex>"
}

Woodpecker maps these to pipeline metadata: CI_COMMIT_BRANCH, CI_COMMIT_SHA, CI_PIPELINE_EVENT β€” which your when: blocks filter on:

when:
  event: [push]
  branch: main          # from payload .ref == refs/heads/main

Tag pushes arrive as ref: refs/tags/v1.4.0 β†’ event: tag, branch unset.

Securing Deliveries

Forgejo signs each request with your configured secret as X-Forgejo-Signature / X-Gitea-Signature (HMAC-SHA256 of the raw body). Verify server-side:

import hmac, hashlib
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, headers["x-forgejo-signature"])

Checklist:

Debugging Deliveries

Forgejo records every attempt. Repo β†’ Settings β†’ Webhooks β†’ click the hook β†’ Recent Delivery History shows timestamp, status code, and full request/response bodies.

Reading the history like an engineer:

2026-08-26 09:14:02   POST https://ci.example.com/api/hooks?...   β†’ 200 OK      βœ“
2026-08-26 09:15:10   POST ...                                     β†’ timeout     βœ—
2026-08-26 09:16:44   POST ...                                     β†’ 403         βœ—

Replay any past delivery with the Redeliver button after fixing the cause β€” no dummy commits needed.

Receiver-side quick capture for custom endpoints:

# One-off listener to inspect real payloads
nc -l -p 9999 | tee last-webhook.txt
# Or with more structure:
docker run --rm -p 8080:8080 weshigbee/webhook-listener

Then point a test webhook at http://build-host:9999/ and push a trivial commit to a scratch repo.


Practical Examples

Example 1: Diagnose "pipeline didn't run"

  1. Open the repo webhook history in Forgejo. No entries β†’ event not subscribed or hook deleted; run Woodpecker's "Repair repository".
  2. Entry with 200 but no run β†’ check Woodpecker repo settings: is the branch excluded? Is .woodpecker.yaml present on that ref?
  3. Entry with non-2xx β†’ fix per status table above, then Redeliver.

Example 2: Deploy bot webhook with secret verification

Minimal Flask receiver:

from flask import Flask, request, abort
import hmac, hashlib, os

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()

@app.post("/hooks/forgejo")
def hook():
    sig = request.headers.get("X-Forgejo-Signature", "")
    expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(403)
    data = request.json
    if data["ref"] == "refs/heads/main":
        print(f"deploying {data['after']}")  # kick off deploy job
    return "", 204

Common Pitfalls & Troubleshooting

Problem Cause Fix
Webhook returns 403 Secret mismatch between Forgejo and receiver Re-copy the secret; watch for trailing whitespace/newline
Deliveries time out intermittently Receiver behind slow DNS or proxy buffering Increase Forgejo timeout; check proxy access logs
CI ignores pushes to feature branches Hook fine, but Woodpecker when: filters branch Adjust pipeline filters; see woodpecker-pipeline-testing
x509: certificate signed by unknown authority Forgejo container lacks CA bundle Mount host CAs or use publicly trusted certs (letsencrypt-automation)
Duplicate pipeline runs Two hooks registered (auto + manual) Delete the manual duplicate in Forgejo webhook list
Tag build runs twice (push + tag) Both events subscribed and pipeline matches both Scope when: blocks precisely
Payload body unreadable in receiver Body consumed before signature check Read raw bytes once, verify HMAC, then parse JSON

Next Steps / Ops Actions

Sources & Related Articles

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse