Push-Triggered Ingestion Webhooks
Codumentor can accept push notifications from your Git host and re-ingest the affected repository automatically, so the vector database stays in sync without an admin running codumentor ingest by hand.
The webhook endpoint is designed to work out-of-the-box with Gitea and GitHub push payloads. It also accepts a minimal generic JSON shape — {"repo": "...", "branch": "..."} — so SVN post-commit hooks (or any HTTP client) can drive ingestion without having to fake a git push blob.
How it works
- Your Git host sends a
pushevent toPOST /ops/webhook/push. - Codumentor verifies the request against a list of configured shared secrets.
- The
repositoryblock of the payload is matched against your configuredrepos:. Pushes for repositories not listed in your config are rejected with404— the webhook will never clone a repository you have not explicitly opted into. - Codumentor chooses what to do based on the pushed branch: - Push matches the repo's configured branch (or the repo has no branch restriction) → queue a full ingestion: clone/pull the repo and update the vector DB. Response:
202 {"status": "queued"}. - Push is on a different branch → queue a fetch-only refresh: rungit fetch --all --tags --pruneagainst the existing clone soorigin/<branch>stays current. No re-embedding is performed, but the branch-switching plugin can immediately check that branch out against the latest state. Response:202 {"status": "fetch_queued"}. - Concurrent pushes for the same repo are coalesced: at most one run is active and at most one is queued. New pushes arriving during a run simply mark "another pass needed" rather than stacking up. When modes mix, full dominates fetch: a full request arriving while a fetch is pending upgrades the pending entry; a fetch request arriving while a full is pending is a no-op (the full run already fetches every ref).
- Different repos are serialized within the server process so they never write to the vector DB at the same time. A
codumentor ingestrunning in a separate process (CLI) still claims the cross-process lock; webhook runs that land during that window are dropped and will re-trigger on the next push.
The endpoint answers quickly and never blocks on ingestion work.
Configuration
Add a webhooks block to your codumentor.yaml:
webhooks:
enabled: true
secrets:
- "${CODUMENTOR_WEBHOOK_SECRET}"
- "${CODUMENTOR_WEBHOOK_SECRET_PREVIOUS}" # optional, for rotation
repos:
- url: "https://gitea.example.com/org/my-repo"
branch: "main"
- "https://github.com/other-org/their-lib@release"
| Field | Required | Description |
|---|---|---|
webhooks.enabled | yes | Must be true for the endpoint to respond. When false, the endpoint returns 404. |
webhooks.secrets | yes | List of accepted shared secrets. The request is accepted if any secret matches. Use environment substitution (${VAR_NAME}) to keep secrets out of the file. |
Leaving secrets empty while enabled: true returns 503 — an empty list is treated as a misconfiguration, not as "accept anything".
Rotation. To rotate a secret without downtime, add the new secret to the list first, update the Git host to send the new secret, then remove the old secret on the next config change.
Gitea setup
In your Gitea repository:
- Settings → Webhooks → Add Webhook → Gitea.
- Target URL:
https://codumentor.example.com/ops/webhook/push - HTTP Method:
POST - POST Content Type:
application/json - Secret: the same value you put in
webhooks.secrets. - Trigger On: select "Custom Events…" and enable only Push Events. (Leaving "all events" on is also fine — Codumentor silently returns
204for events it doesn't care about.) - Save.
Gitea will sign each request with X-Gitea-Signature: <hex HMAC-SHA256 of the body>.
GitHub setup
In your GitHub repository:
- Settings → Webhooks → Add webhook.
- Payload URL:
https://codumentor.example.com/ops/webhook/push - Content type:
application/json - Secret: the same value you put in
webhooks.secrets. - Which events would you like to trigger this webhook? → Just the
pushevent. - Save.
GitHub sends X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the body>. Codumentor accepts both header styles.
Calling from SVN (post-commit hook)
Subversion has no native webhook system, but the endpoint accepts a minimal generic JSON payload that an SVN post-commit hook can produce in a few lines of shell.
Prerequisites:
- The repo must be configured with
scm: svn(see the SVN plugin docs). The SVN plugin handles the actualsvn checkout/svn updateduring ingestion. - Add the webhook secret to
webhooks.secretsas usual.
Example hooks/post-commit on the SVN server:
#!/bin/bash
SECRET='your-shared-secret'
BODY='{"repo":"myproject","branch":"trunk","event":"push"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -fsS -X POST https://codumentor.example.com/ops/webhook/push \
-H 'Content-Type: application/json' \
-H "X-Webhook-Signature: $SIG" \
--data "$BODY" >/dev/null
The repo field may be the configured repo's name, its url, or a local path / basename — whatever is easier to produce from the hook. branch is optional; when present it is compared against the configured repo's branch the same way as for git. Omit branch to always do a full ingestion.
What Codumentor does for SVN:
| Scenario | Action |
|---|---|
branch matches configured branch (or no branch set) | Full ingestion. The SVN plugin runs svn update (or svn checkout on first run), then re-embeds changed files. |
branch differs from configured branch | Fetch-only. Runs svn update on the working copy without re-embedding, so the agent's file/shell tools see latest content. |
Credentials configured on the SVN repo entry (options.username, options.password) are used automatically for the fetch-only svn update.
Calling from other systems
Any client that can compute HMAC-SHA256 over the raw request body will work. Use either the Gitea-shaped push payload or the generic shape:
# Gitea / GitHub shape (git)
SECRET='your-shared-secret'
BODY='{"ref":"refs/heads/main","repository":{"clone_url":"https://gitea.example.com/org/my-repo.git","ssh_url":"git@gitea.example.com:org/my-repo.git","full_name":"org/my-repo","name":"my-repo"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST https://codumentor.example.com/ops/webhook/push \
-H 'Content-Type: application/json' \
-H 'X-Gitea-Event: push' \
-H "X-Gitea-Signature: $SIG" \
--data "$BODY"
# Generic shape — any repo identifier plus an optional branch
BODY='{"repo":"my-repo","branch":"main"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST https://codumentor.example.com/ops/webhook/push \
-H 'Content-Type: application/json' \
-H "X-Webhook-Signature: $SIG" \
--data "$BODY"
HMAC headers are interchangeable: X-Gitea-Signature, X-Hub-Signature-256 (accepts the sha256= prefix), and X-Webhook-Signature are all validated the same way.
If you cannot compute an HMAC (e.g., a simple monitoring script), Codumentor also accepts a bearer-token fallback against the same secret list:
curl -X POST https://codumentor.example.com/ops/webhook/push \
-H 'Content-Type: application/json' \
-H 'X-Gitea-Event: push' \
-H "Authorization: Bearer $SECRET" \
--data "$BODY"
The X-Webhook-Token: $SECRET header is accepted as an equivalent alternative.
Security note. The HMAC path authenticates both the caller and the body contents. The bearer-token path only authenticates the caller. Prefer HMAC when the Git host supports it.
Accepted payload shapes
Gitea / GitHub push (git-only). The endpoint reads repository identification from, in order of preference:
repository.clone_urlrepository.ssh_urlrepository.html_url(fallback whenclone_urlis absent)repository.full_namerepository.name
Branch detection reads ref (refs/heads/<branch>). Tag pushes and other refs are ignored.
Generic (works for git, SVN, or anything else):
{"repo": "<name | url | path>", "branch": "<branch>", "event": "push"}
Only repo is required. branch is optional (omit to always do a full ingest). event is optional — any non-push value causes the endpoint to return 204 so the same URL can be wired into a hook that fires for multiple event types.
The repo field is matched against your configured repos by URL, name, local path, or path basename — whichever makes your hook simplest.
A payload missing repository identification (no repository block and no repo field) returns 400.
Response codes
| Code | Meaning |
|---|---|
202 | Accepted. Work has been queued. Body is {"status": "queued", "repo": "<name>"} for a full ingestion, or {"status": "fetch_queued", "repo": "<name>"} for a fetch-only refresh on a non-indexed branch. |
204 | Event ignored on purpose (non-push event such as ping). |
400 | Malformed JSON or missing repository identification. |
401 | Missing, invalid, or unmatched signature / token. |
404 | Webhooks disabled, or the pushed repository is not in config.repos. |
503 | Webhooks enabled but secrets list is empty — misconfiguration. |
Observability
Webhook activity logs under the codumentor.api.webhook and codumentor.ingestion.webhook_scheduler loggers. Useful messages:
Webhook push accepted for repo '<name>' (branch=<b>)— full ingestion queued.Webhook push for '<name>' queued fetch-only: push branch '…' != configured ref '…'— fetch-only queued on an off-branch push.Webhook push rejected: repo not in config (…)— 404 with enough detail to fix config.Starting webhook-triggered ingestion for repo '<name>'/Webhook ingestion complete for '<name>'— full-ingestion lifecycle.Starting webhook-triggered fetch for repo '<name>'/Fetched all refs for <path>— fetch-only lifecycle.Webhook ingestion skipped for '<name>': …— skipped because the cross-process file lock was held by a CLI run. The next push will re-trigger.
Bump logging.logger_levels.codumentor.api.webhook to DEBUG if you need per-request tracing.
Operational caveats
- The webhook scheduler is in-memory. If the API server restarts while a push is queued, that push is lost. Subsequent pushes recover automatically; a manual
codumentor ingestalso works. - Unknown repositories are rejected, not auto-registered. Add the repo to
config.reposand reload before the first push will be accepted. - The endpoint has no host allowlist: anything in
config.reposis acceptable, and anything outside it is not. Don't add repos you wouldn't want cloned. - Webhook ingestion shares the same cross-process
IngestionLockas CLI and/ops/ingest. If one is running, the other defers.