When something happens, machines get a webhook — people get a notification.
GAEvery meaningful change in a repo becomes an event. Subscribe an endpoint and it arrives as a signed webhook you can verify and act on; the people who care get a personal notification in the dashboard. One thing happens; the right systems and the right humans both find out.
What fires
These are the events you can subscribe to. Each carries a small JSON body describing what changed and which repo it belongs to.
| Event | Fires when |
|---|---|
push | Commits are pushed to a branch. |
pull_request | A pull request is opened or merged. |
issue | An issue is opened. |
release | A release is published. |
deployment | A deployment changes state. |
check | A commit status / check is reported. |
merge_queue | The merge queue starts testing a merge result (action: "testing", with trial_ref and trial_sha for your CI to fetch and report checks on), or an entry under test leaves the queue (action: "failed", with the reason). |
ping | You send a test from the dashboard or CLI. |
push can kick off your CI; a published release can trigger a rollout; a deployment can update your status page. Run your jobs on TillForge runners, or let these events tell the CI you already have exactly when to run.Point an endpoint at the events
Add a webhook for an org (all repos) or a single repo, choose your events, and set a shared secret — the secret is what lets you prove a delivery is genuinely from TillForge. Send yourself a ping to confirm the endpoint is wired before anything real depends on it.
# Subscribe an endpoint to the events you care about. It prints the
# signing secret your receiver verifies deliveries with, once.
tilldev forge webhook add \
--url https://ci.example.com/hooks/tillforge \
--events push,pull_request,release,deployment \
--repo acme-api
tilldev forge webhook secret <id> # replace it (or --secret <yours>)
tilldev forge webhook ls
tilldev forge webhook ping <id> # send a test 'ping' event now
tilldev forge webhook deliveries <id> # the recent delivery log
tilldev forge webhook rm <id>Verify every delivery
X-TillForge-Event names the event and X-TillForge-Delivery the delivery, which the body repeats as delivery_id alongside attempt. When the webhook has a secret, X-TillForge-Webhook-Signature carries t=<unix>,v1=<hex>: an HMAC-SHA256 of t.raw_body keyed by your secret, the same scheme as TillAuth webhooks. Recompute it over the exact bytes you received, compare in constant time, and refuse a t more than five minutes off. That’s how you know a request is really from us, untampered, and not a recording played back later. A retry keeps its delivery_id, so skip one you’ve already handled.
X-TillForge-Signature: sha256=<hex> (the body alone, as GitHub signs) is still sent for receivers written before. It proves origin but not freshness, so a captured delivery verifies forever; move to the timestamped header.
import crypto from 'node:crypto'
// Verify a delivery before you trust it. v1 is an HMAC-SHA256 of
// "<t>.<raw body>", keyed by your webhook secret.
export function isValid(rawBody: string, header: string, secret: string, now = Date.now() / 1000) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false // stale: a replay
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
const got = Buffer.from(String(parts.v1 ?? ''), 'hex')
// constant-time compare, never a plain ===
return got.length === expected.length && crypto.timingSafeEqual(got, expected)
}
// isValid(rawBody, req.headers['x-tillforge-webhook-signature'], YOUR_SECRET)
// then skip a delivery_id you have already handled: retries reuse it.===.Delivery you can inspect
A delivery that fails is tried again: six attempts in all, at once and then after 1 minute, 5 minutes, 25 minutes, 2 hours and 10 hours. Every attempt logs its response and timing, viewable with forge webhook deliveries or on the Webhooks screen — so a receiver that was down is visible, not silent. Delivery is time-boxed, and a receiver that answers slowly never holds up the push, merge, or release that triggered it.
Notifications — for people, not machines
The human side of the same events. When your work gets attention, a notification lands in the dashboard’s Notifications screen, deep-linked to the thread and tracked as unread until you open it.
| You get one when | On |
|---|---|
| Review — someone reviews it | Your pull request |
| Comment — someone replies | Your pull request or issue |
| Merge — it lands | Your pull request |
Notifications are personal — you only ever see your own, and you can only mark your own read. They’re a lightweight inbox for what needs your eyes; the durable, org-wide record of who did what lives in the audit log.
Next: Deployments and Releases & builds for the events worth wiring up, or the CLI reference. Back to the TillForge overview.