CI/CD webhook trigger routes (ENH-011). Mounted at /api/v1 (INF-005)
without requireAuth — this router handles its own token-based authentication so
CI pipelines can call it with a per-project Bearer token rather than a user JWT.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/projects/:id/trigger |
Bearer token | Start a CI/CD test run |
GET |
/api/v1/projects/:id/trigger-tokens |
JWT (requireAuth) | List tokens — see runs.js |
POST |
/api/v1/projects/:id/trigger-tokens |
JWT (requireAuth) | Create token — see runs.js |
DELETE |
/api/v1/projects/:id/trigger-tokens/:tid |
JWT (requireAuth) | Revoke token — see runs.js |
Token management endpoints (list/create/delete) live in runs.js and are
protected by requireAuth. Only POST /trigger is here, unprotected.
- Source:
Members
(inner, constant) TRIGGERING_GITHUB_EVENTS
Webhook handlers require BOTH:
- A valid HMAC signature from the deployment provider (proves Vercel/ Netlify sent the payload — protects against forged calls).
- A project-scoped trigger token via
requireTrigger(proves which project should run — without this, a single global webhook secret would let any signed payload trigger any project ID in the URL).
- Source:
Methods
(static) concludeGithubCheck(finishedRun, project)
Conclude a GitHub Check Run once the Sentri run reaches a terminal state.
Placement rationale (deviates from NEXT.md INT-002 sketch):
NEXT.md suggests this hook lives in testRunner.js onComplete. We keep
it here because (a) testRunner.js has no internal completion hook —
runWithAbort.onComplete is the hook, fired from this file already;
(b) only the trigger path carries GitHub repo/sha context, so wiring it
through the runner would require threading run.githubCheck plumbing
into a module that has no other reason to know about integrations;
(c) keeping integration side-effects at the route layer matches the
pattern used by fireNotifications (FEA-001) just above this call.
Errors are always logged + swallowed so a GitHub outage never fails the
underlying Sentri run (INT-002 anti-pattern guard).
Parameters:
| Name | Type | Description |
|---|---|---|
finishedRun |
Object | |
project |
Object |
- Source:
(static) verifyWebhookSignature(provider, rawBody, signatureHeader) → {boolean}
HMAC signature verification for deployment-webhook payloads.
Per-provider algorithm choice is dictated by each provider, not by us:
- Vercel: HMAC-SHA1 over the raw body (
X-Vercel-Signatureheader). Vercel's current webhook spec still signs with SHA-1; any change would have to come from Vercel. HMAC-SHA1 (unlike plain SHA-1) is not known to be vulnerable — the keyed prefix construction defeats the collision attacks that retired SHA-1 for certificate signing. Pre-image resistance in HMAC depends on the key, not the hash, so an attacker who cannot guessVERCEL_WEBHOOK_SECRETcannot forge a valid signature. - Netlify: HMAC-SHA256 over the raw body (
X-Netlify-Tokenheader).
If you're auditing this and wondering "why SHA-1?" — the answer is
interoperability with the provider's signing scheme. When Vercel upgrades
their webhook signatures, bump algo for the "vercel" branch here.
Parameters:
| Name | Type | Description |
|---|---|---|
provider |
"vercel" | "netlify" | "github" | |
rawBody |
Buffer | undefined | captured by the webhook-scoped
express.json |
signatureHeader |
string | undefined | the provider's signature header
value; tolerates both raw hex and |
- Source:
Returns:
- Type
- boolean
(inner) buildCrawlRun(args) → {object}
Build a type: "crawl" run object aligned with routes/runs.js:71-83.
Parameters:
| Name | Type | Description | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
args |
object |
Properties
|
- Source:
Returns:
the run record ready for runRepo.create().
- Type
- object
(inner) buildTestRun(args) → {object}
Build a type: "test_run" run object aligned with routes/runs.js:188-213.
AUTO-001: persisted testQueue mirrors the approved-test order (audit
fidelity) with per-row riskScore; budget-skipped tests are pre-seeded
into results as skipped (over_budget) so every approved test has an
observable resolution. total reflects the approved set, not the
post-budget dispatch slice.
Parameters:
| Name | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
args |
object |
Properties
|
- Source:
Returns:
the run record ready for runRepo.create().
- Type
- object
(async, inner) handleTrigger(req, res)
POST /api/projects/:id/trigger Token-authenticated endpoint for CI/CD pipelines (ENH-011).
Authentication
Pass the project trigger token as a Bearer token:
Authorization: Bearer <plaintext-token>
This endpoint does NOT accept JWTs — only tokens created via
POST /api/projects/:id/trigger-tokens.
Request body (all fields optional)
{
"callbackUrl": "https://ci.example.com/hooks/sentri",
"dialsConfig": { "parallelWorkers": 2 }
}
Response 202 Accepted
{ "runId": "RUN-42", "statusUrl": "https://sentri.example.com/api/runs/RUN-42" }
Poll statusUrl until status is no longer "running".
Error responses
| Code | Reason |
|---|---|
| 400 | No approved tests |
| 401 | Missing or invalid Bearer token |
| 403 | Token belongs to a different project |
| 404 | Project not found |
| 409 | Another run already in progress |
| 429 | Rate limit exceeded (expensiveOpLimiter) |
Parameters:
| Name | Type | Description |
|---|---|---|
req |
Object | Express request |
res |
Object | Express response |
- Source:
(async, inner) launchPreviewCrawl()
Launch a crawl run against a deployment-preview URL. Shared by the Vercel
and Netlify webhook handlers below — kept in one place so the run-object
shape and runWithAbort wiring stay aligned with runs.js:71-127
(the canonical crawl entry point).
The caller must have already (a) verified the provider's HMAC signature,
(b) authenticated the trigger token via requireTrigger, and (c) validated
previewUrl via SSRF guard.
AUTO-015b: emits a dedicated crawl.start.deployment activity row (see
below) alongside the standard crawl.start, so the "Last deployment run"
badge on the project header can distinguish webhook-launched crawls from
manually-triggered ones via the activity log without a schema change.
- Source:
(async, inner) prepareGithubCheck(project, body, runId, deliveryId) → {Promise.<(Object|null)>}
Prepare a GitHub Check Run for a Sentri run, returning the metadata to
persist on run.githubCheck (or null when no check should be created).
Idempotency contract (INT-002):
GitHub retries failed webhook deliveries (any non-2xx) with exponential
backoff for up to 24h, always using the same X-GitHub-Delivery UUID.
The delivery ID — not the commit SHA — is the correct idempotency key:
- Same delivery ID → GitHub is retrying. Return existing check, do not create a new run, do not transition state on GitHub.
- New delivery ID for the same SHA → distinct event (e.g.
rerequestedafter a user clicks "Re-run"). Create a fresh Check Run.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
project |
Object | ||
body |
Object | — trigger request body (already merged with normalizeGithubPayload). |
|
runId |
string | ||
deliveryId |
string | null | null | — value of the |
- Source:
Returns:
- Type
- Promise.<(Object|null)>
(async, inner) safeFetchCallback(url, payload)
Thin wrapper around safeFetch for the callbackUrl POST. Best-effort: errors are silently caught so a failing callback never affects the run outcome.
Parameters:
| Name | Type | Description |
|---|---|---|
url |
string | The validated callbackUrl. |
payload |
string | JSON string body. |
- Source: