Webhook Deploy — GitHub & GitLab¶
Every push to your connected GitHub or GitLab repo automatically triggers a build and rolling deploy on PaaS Runtime.
Workflow¶
sequenceDiagram
participant Dev as Developer
participant GH as GitHub/GitLab
participant CP as Control Plane
participant DB as Postgres (app_git_sources)
participant Tekton
participant Harbor
participant K8s
Dev->>GH: git push main
GH->>CP: POST /v1/webhooks/github/{app_id} (HMAC signed)
CP->>CP: verify HMAC SHA-256 (X-Hub-Signature-256)
CP->>DB: SELECT app_git_sources WHERE app_id + provider
CP->>CP: branch filter + auto_deploy gate
CP->>DB: SHA dedup (skip if last_deploy_sha == sha)
CP->>Tekton: DeployRequest{source:"github_webhook:<sha>"}
Tekton->>Harbor: docker build + push image
Tekton->>CP: build complete callback
CP->>K8s: update Deployment image tag
CP->>DB: UPDATE last_deploy_sha
K8s-->>Dev: app live at https://app.runtime.di2amp.com
Endpoints¶
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/v1/webhooks/github/{app_id} |
X-Hub-Signature-256 HMAC-SHA256 |
GitHub push event |
POST |
/v1/webhooks/gitlab/{app_id} |
X-Gitlab-Token constant-time match |
GitLab push event |
The app_id is a UUID (path segment). Both providers return:
| Status | Body | Meaning |
|---|---|---|
200 |
{"ping":"ok"} |
GitHub ping event (hook just registered) |
200 |
{"status":"non-push event ignored"} |
GitLab non-Push event (Tag/MR/Issue) |
202 |
{"deploy_id":"…","sha":"…","repo":"…"} |
Deploy queued |
202 |
{"status":"branch mismatch, no deploy triggered"} |
Push wasn't to app_git_sources.branch |
202 |
{"status":"auto_deploy disabled"} |
auto_deploy_enabled = false for this app |
202 |
{"status":"sha unchanged, skipped"} |
Same commit already deployed (SHA dedup) |
400 |
{"error":"malformed JSON"} |
Body wasn't valid JSON |
401 |
{"error":"missing X-Hub-Signature-256"} / {"error":"invalid signature"} |
HMAC failure |
401 |
{"error":"invalid X-Gitlab-Token"} |
GitLab token mismatch |
404 |
{"error":"no github source for this app"} |
No app_git_sources row for this app_id + provider |
Security¶
GitHub HMAC-SHA256¶
GitHub signs the raw request body with the per-app webhook secret stored in app_git_sources.webhook_secret. The control plane:
- Reads
X-Hub-Signature-256: sha256=<hex> - Decodes the hex to bytes
- Recomputes
HMAC-SHA256(body, secret) - Compares using
hmac::Mac::verify_slice()(constant-time)
A missing/empty signature, invalid hex, or mismatch all return 401.
GitLab token¶
GitLab doesn't sign the body — it just echoes a shared secret in X-Gitlab-Token. The control plane:
- Reads
X-Gitlab-Token - Compares byte-by-byte with
app_git_sources.webhook_secretusing fold-XOR (constant-time) - Rejects empty configured secrets unconditionally (so an attacker can't bypass with no auth)
SHA idempotency¶
The same commit pushed twice (e.g. webhook retry on transient error) won't double-deploy:
SELECT last_deploy_sha FROM app_git_sources WHERE app_id = $1
-- if last_deploy_sha = incoming sha → return 202 "sha unchanged, skipped"
Implemented in crates/control-plane/src/deploy_trigger.rs::trigger_deploy_from_webhook.
Setup¶
- Connect provider — Settings → External Git Providers → Connect GitHub/GitLab (cf OAuth Providers)
- Link an app —
PUT /v1/apps/{id}/git/sourcewith{"provider":"github","repo_full_name":"org/repo"} - Webhook auto-registration — the control plane uses the stored OAuth token to create a webhook on the provider, with the per-app
webhook_secretrandomly generated. - Test it —
git push main→ watchpaas logs --tail.
Troubleshooting¶
401 missing X-Hub-Signature-256¶
The webhook is configured without a secret on the provider side. Edit the GitHub hook → set the Secret field → re-test. The platform always sends a non-empty signature; missing is treated as missing config.
401 invalid signature¶
Secret mismatch between the provider config and app_git_sources.webhook_secret. Either:
- Re-register the webhook (auto-generates a new secret), or
- Manually rotate via
PUT /v1/apps/{id}/git/webhook-secretthen update the GitHub hook.
202 branch mismatch¶
The push was to a branch other than the configured one (default main). Update app_git_sources.branch via PUT /v1/apps/{id}/git/source.
404 no github source for this app¶
No row in app_git_sources for this (app_id, provider). Make sure step 2 of the setup ran. Verify with:
Ping events succeed but pushes don't¶
GitHub sends a ping event when the webhook is created — it has a zen field instead of head_commit. The handler returns 200 {"ping":"ok"} immediately. Real push events have ref + head_commit.id. Check the Recent Deliveries tab on the GitHub webhook page.
Related¶
- OAuth Providers — connect GitHub/GitLab via OAuth
- Git Push Deploy — Forgejo (built-in) variant
- Apps — what an "app" is