Skip to main content
Webhooks are the backbone of Shipyard’s CI pipeline. Every time you push to a connected branch, GitHub sends a signed HTTP payload to Shipyard’s webhook endpoint. Shipyard verifies the signature, extracts the commit details, creates a build record, and triggers the build engine — all before returning a response to GitHub.

How webhook registration works

When you create a project via POST /api/project, Shipyard immediately calls the GitHub API to register a webhook on your repository. You do not need to configure anything in GitHub manually.
  • Shipyard registers the webhook to fire on push and pull_request events. Only push events trigger builds; pull_request events are registered but not currently processed.
  • The webhook callback URL is set to <WEBHOOK_CALLBACK>/api/webhook using the value in your .env.
  • The webhook is configured with your WEBHOOK_SECRET so GitHub can sign every payload.
  • Shipyard stores the GitHub-assigned webhook ID in the project record. When you delete a project, Shipyard uses this ID to remove the webhook from GitHub automatically.

Webhook verification

Every incoming request to POST /api/webhook is authenticated via HMAC-SHA256 signature — there is no JWT check on this endpoint. GitHub attaches a X-Hub-Signature-256 header to every webhook delivery formatted as sha256=<hex-digest>. Shipyard recomputes the HMAC using the raw request body and your WEBHOOK_SECRET, then compares the two values with a timing-safe comparison (timingSafeEqual from Node’s crypto module). This prevents signature brute-forcing through timing attacks. Requests that are missing the signature header, or whose signature does not match, are rejected with a 401 response.

What happens on a push

When GitHub delivers a valid push event:
  1. Shipyard reads the X-GitHub-Event header. A ping event (sent when the webhook is first created) is acknowledged and ignored.
  2. For push events, Shipyard extracts the commit message, author name, commit hash (payload.after), branch, and repository URL from the payload.
  3. A new build record is inserted with status queued and the extracted commit metadata.
  4. Shipyard responds to GitHub with 201 to acknowledge receipt.
  5. The build engine runs asynchronously: it clones the repo at the exact commit hash, builds a Docker image, runs your build command inside the container, and streams logs via Socket.io.

Testing webhooks locally

GitHub cannot reach localhost, so you need a tunneling tool to expose your local server to the internet during development. Run either of the following commands to create a public tunnel to port 8080:
ngrok http 8080
outray 8080
Copy the public URL provided by the tool and set it as WEBHOOK_CALLBACK in your .env:
WEBHOOK_CALLBACK=https://abc123.ngrok.io
Restart the server after updating .env so the new callback URL is used when Shipyard registers webhooks for new projects.
The tunnel URL changes every time ngrok restarts. If you restart ngrok, update WEBHOOK_CALLBACK in your .env and recreate any projects whose webhooks point to the old URL — GitHub will keep sending events to the stale address until the webhook registration is updated.

The webhook endpoint

POST /api/webhook accepts the raw JSON payload from GitHub. The relevant fields Shipyard reads from a push event are:
FieldSource in payload
Repository URLpayload.repository.html_url
Branchpayload.ref (stripped of refs/heads/ prefix)
Commit messagepayload.commits[0].message
Commit authorpayload.commits[0].author.name
Commit hashpayload.after
Shipyard uses the repository URL and branch together to look up the matching project in the database. If no project matches, the request returns 404.

Build docs developers (and LLMs) love