Module: routes/trigger

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:

  1. A valid HMAC signature from the deployment provider (proves Vercel/ Netlify sent the payload — protects against forged calls).
  2. 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-Signature header). 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 guess VERCEL_WEBHOOK_SECRET cannot forge a valid signature.
  • Netlify: HMAC-SHA256 over the raw body (X-Netlify-Token header).

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 verify callback in middleware/appSetup.js.

signatureHeader string | undefined

the provider's signature header value; tolerates both raw hex and "<algo>=<hex>" prefix forms.

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
Name Type Attributes Description
runId string
project object

must carry id and (optionally) workspaceId.

dialsConfig object <optional>

validated dials (output of resolveDialsConfig(...)). When present, persisted on the run record as generateInput: { dialsConfig } so the RunDetail page can show which dials drove the crawl and the MNT-010 re-run feature can replicate the same configuration. Mirrors runs.js:81.

environmentId string | null <optional>

DIF-012: persisted on the run record so trigger-initiated crawl runs surface in the dashboard's per-environment aggregation alongside their UI-initiated counterparts. Mirrors runs.js:126 on the canonical POST /crawl path.

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
Name Type Attributes Default Description
runId string
project object

must carry id and (optionally) workspaceId.

tests Array.<object>

The full approved-test set (persisted order).

budgetSkipped Array.<object> <optional>

Tests truncated by budgetMinutes.

impactSkipped Array.<object> <optional>

Tests skipped by impact analysis.

riskById Map.<string, number> <optional>

testId → riskScore lookup.

budgetMinutes number | null <optional>

Normalized budget actually applied.

changedFiles Array.<string> <optional>

Git diff files used for impact analysis.

impactAnalysis Object | null <optional>

Resolved impact-analysis summary.

parallelWorkers number
shardCount number <optional>
1

CAP-002: explicit shard request from the caller (req.body.shards). Defaults to 1 so legacy callers that never pass shards don't surface a misleading shard badge — see routes/runs.js (BUG-0001 rationale).

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. rerequested after 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 X-GitHub-Delivery header, when present.

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: