Webhooks
ZopNight exposes two unauthenticated webhook endpoints — one for inbound events from GitHub, and one for the build runner reporting back. Both use cryptographic verification rather than JWT session auth so they can be called without a ZopNight login.
Where these routes live
Treat webhook secrets like credentials
GitHub Webhook
The GitHub app installed via the GitHub integration delivers push and pull_request events to this endpoint. The deployer verifies the HMAC against GITHUB_APP_WEBHOOK_SECRET, then fans out to every deployment whose repo_url + branch + auto_deploy=true matches.
/webhooks/githubGitHub event delivery endpoint. Authenticated via X-Hub-Signature-256 (HMAC-SHA256 of the request body, keyed with GITHUB_APP_WEBHOOK_SECRET). (deployer)
X-GitHub-Event: push
X-GitHub-Delivery: 72d3162e-cc78-11ed-...
X-Hub-Signature-256: sha256=<hex digest>
Content-Type: application/json# Pseudocode — ZopNight does this server-side
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(headerValue)) {
return 401
}Idempotency: delivery_id as revision PK
Since Phase 3b-1 (May 2026), GitHub's X-GitHub-Delivery is hashed into a deterministic UUID v5 (namespace: the deployment ID) and used as the primary key of the resulting deployment_revisions row. The deployer issues an INSERT IGNORE when it creates the revision, so replaying the same GitHub delivery is a no-op — no duplicate builds, no duplicate Helm releases, no manual reconciliation.
This replaces the older webhook_dedup ledger table, which is still present in the deployer's schema for backwards compatibility but is no longer written to. The drop migration is scheduled for Phase 4.
Fan-out across deployments
When a push arrives, the deployer needs the list of deployments to react on. Since Phase 3c, the catalog lives on config, so the deployer calls config's gRPC:
rpc ListDeploymentsByRepoAndBranch(
ListDeploymentsByRepoAndBranchRequest { repo_url, branch }
) returns (DeploymentList);
// Indexed by idx_deployments_repo_branch (repo_url(191), branch, auto_deploy)
// to keep webhook fan-out under the same 50ms budget as the old local query.Events handled
| Event | Effect |
|---|---|
push | For every matching deployment with auto_deploy=true: INSERT IGNORE a new revision keyed by the delivery UUID, then enqueue a build job. |
pull_request | Run analysis (no build, no deploy) and post a status check back to GitHub. |
installation / installation_repositories | Sync the org's available repositories list. |
Build Callback
The GitHub Actions dispatcher that runs builds on your behalf reports completion back to ZopNight via this endpoint. The callback carries a short-lived JWT signed with the deployer's BUILD_CALLBACK_SECRET in the request body — the gateway accepts the request unauthenticated, then the deployer validates the body token before promoting the revision. If the secret isn't set on the deployer, the route isn't registered at all.
/build-callbacks/{revisionID}Build runner reports image readiness for a revision. (deployer)
curl -X POST https://zopnight.com/api/build-callbacks/rev_005 \
-H "Content-Type: application/json" \
-d '{
"token": "<JWT signed with BUILD_CALLBACK_SECRET>",
"status": "succeeded",
"imageRef": "registry.zopnight.com/acme/checkout:v1.4.3",
"logsURL": "https://github.com/acme/checkout/actions/runs/123",
"completedAt": "2026-04-29T11:04:30Z"
}'{
"token": "<JWT signed with BUILD_CALLBACK_SECRET>",
"status": "failed",
"failureReason": "build step exited with code 1: missing Dockerfile",
"logsURL": "https://github.com/acme/checkout/actions/runs/124"
}Token claims
| Claim | Description |
|---|---|
rev | The revision ID (must match the URL path). |
iat | Issued-at; the deployer rejects tokens older than 30 minutes. |
exp | Expiry; required. |
See Builds for how revisions and image references flow through the deploy pipeline, and Deployments for the runtime endpoints that consume the revisions created here.