Skip to content

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:

  1. Reads X-Hub-Signature-256: sha256=<hex>
  2. Decodes the hex to bytes
  3. Recomputes HMAC-SHA256(body, secret)
  4. 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:

  1. Reads X-Gitlab-Token
  2. Compares byte-by-byte with app_git_sources.webhook_secret using fold-XOR (constant-time)
  3. 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

  1. Connect provider — Settings → External Git Providers → Connect GitHub/GitLab (cf OAuth Providers)
  2. Link an appPUT /v1/apps/{id}/git/source with {"provider":"github","repo_full_name":"org/repo"}
  3. Webhook auto-registration — the control plane uses the stored OAuth token to create a webhook on the provider, with the per-app webhook_secret randomly generated.
  4. Test itgit push main → watch paas 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-secret then 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:

SELECT * FROM app_git_sources WHERE app_id = '...';

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.