Documentation

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

Both webhook endpoints are served by the deployer service. The gateway forwards them through unauthenticated; the deployer's handler does the HMAC / JWT check itself.

Treat webhook secrets like credentials

Anyone who can sign a request with the right secret can deliver events ZopNight will trust. Store secrets in your secret manager, rotate them when an integration is removed, and audit which integrations carry which secret.

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.

POST
/webhooks/github

GitHub event delivery endpoint. Authenticated via X-Hub-Signature-256 (HMAC-SHA256 of the request body, keyed with GITHUB_APP_WEBHOOK_SECRET). (deployer)

Headers GitHub sendsbash
X-GitHub-Event: push
X-GitHub-Delivery: 72d3162e-cc78-11ed-...
X-Hub-Signature-256: sha256=<hex digest>
Content-Type: application/json
HMAC verification (Go)bash
# 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:

gRPC (deployer → config)bash
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

EventEffect
pushFor every matching deployment with auto_deploy=true: INSERT IGNORE a new revision keyed by the delivery UUID, then enqueue a build job.
pull_requestRun analysis (no build, no deploy) and post a status check back to GitHub.
installation / installation_repositoriesSync 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.

POST
/build-callbacks/{revisionID}

Build runner reports image readiness for a revision. (deployer)

Requestbash
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"
  }'
Failure examplejson
{
  "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

ClaimDescription
revThe revision ID (must match the URL path).
iatIssued-at; the deployer rejects tokens older than 30 minutes.
expExpiry; 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.