Docs /api/test-triggers

Automated Test Triggers API

Automated test triggers allow izri to run E2E tests automatically in response to GitHub push and pull request events. The system integrates with the existing webhook infrastructure and reports results back to GitHub via Commit Status Checks.


Overview

GitHub Push / PR Event
        │
        ▼
  Webhook Handler  (apps/api — Hono route)
        │
        ▼
  testTriggers.dispatchPush / dispatchPR  (tRPC)
        │
        ├─ Evaluate trigger conditions (branch, file patterns)
        ├─ Deduplicate (same commit SHA + event type)
        ├─ Debounce (optional, configurable window)
        │
        ▼
  TestTriggerService.runTrigger
        │
        ├─ Create test_runs record (status=PENDING)
        └─ Post GitHub commit status: pending
                │
                ▼
        Test runner executes tests
                │
        testTriggers.reportResult
                │
        ├─ Update test_runs record (PASSED / FAILED / ERROR)
        └─ Post GitHub commit status: success / failure / error

tRPC Procedures

All procedures require authentication (protected).

testTriggers.dispatchPush

Dispatch a test trigger from a GitHub push payload.

Input:

Field Type Required Description
projectId string (ULID) Target project
payload GitHubPushPayload Raw push webhook payload
configOverride TriggerConfig Override stored config for this call

Output:

// Triggered
{ triggered: true; testRunId: string; reason: string }

// Not triggered
{ triggered: false; reason: string }

Example:

const result = await trpc.testTriggers.dispatchPush.mutate({
  projectId: '01HX...',
  payload: githubPushPayload,
  configOverride: {
    branches: ['main', 'develop'],
    filePatterns: ['src/**/*.ts', 'packages/**'],
    debounceMs: 5000,
    prEvents: true,
  },
})

testTriggers.dispatchPR

Dispatch a test trigger from a GitHub pull_request payload.

Only fires on actions: opened, synchronize, reopened.

Input:

Field Type Required Description
projectId string (ULID) Target project
payload GitHubPRPayload Raw pull_request webhook payload
changedFiles string[] Files changed in the PR (from GitHub Files API)
configOverride TriggerConfig Override stored config

Output: Same shape as dispatchPush.

Example:

const result = await trpc.testTriggers.dispatchPR.mutate({
  projectId: '01HX...',
  payload: githubPRPayload,
  changedFiles: ['src/api/handler.ts', 'packages/trpc/src/router.ts'],
})

testTriggers.reportResult

Update a test run's result and post the outcome to GitHub's Commit Status API.

Input:

Field Type Required Description
testRunId string ID of the test run to update
status 'PASSED' | 'FAILED' | 'ERROR' Final outcome
passedTests number Number of passing tests
failedTests number Number of failing tests
totalTests number Total test count
error string Error message (for ERROR status)
repoFullName string e.g. "org/repo"
commitSha string Commit SHA to update status on
githubToken string GitHub token with repo:status scope
targetUrl string URL for the "Details" link in GitHub UI

Output: { ok: true }


testTriggers.getTestRun

Retrieve a single test run by ID.

Input: { testRunId: string }

Output: Full TestRun record or null.


testTriggers.listTestRuns

List test runs for a project with optional filtering.

Input:

Field Type Default Description
projectId string (ULID) Required
limit number 20 Max 100
status enum PENDING | RUNNING | PASSED | FAILED | ERROR
type enum push | pr | manual

Output: { runs: TestRun[] }


Trigger Configuration

Trigger conditions can be configured per project via the webhooks config (stored in webhook_configs) or overridden per-call with configOverride.

TriggerConfig shape

interface TriggerConfig {
  /** Branches to watch on push events. Empty = all branches. */
  branches: string[]

  /**
   * Glob patterns for changed files. Empty = any change triggers.
   *
   * Evaluated using minimatch with `dot: true`.
   * Supports: `*`, `**`, `?`, `{a,b}` brace expansion, `[abc]` character classes.
   */
  filePatterns: string[]

  /** Trigger on PR opened/synchronize/reopened events. Default: true */
  prEvents: boolean

  /** Debounce window in ms. 0 = immediate. Max: 30,000 (30s). Default: 0 */
  debounceMs: number

  /** GitHub token with repo:status scope for posting commit statuses. */
  githubToken?: string
}

Branch patterns

Exact string match against the branch name (after stripping refs/heads/).

branches: ["main", "develop", "release/v2"]

File patterns

Glob patterns evaluated using minimatch. Full glob syntax is supported:

Pattern Description Example match
* Any chars within one path segment src/*.tssrc/index.ts
** Zero or more path segments src/**/*.tssrc/api/auth.ts
? Any single character src/?.tssrc/a.ts
{a,b} Brace expansion *.{ts,tsx}Button.tsx
[abc] Character class src/[abc]*.tssrc/auth.ts

Examples:

filePatterns: ["**/*.test.ts"]           // any test file anywhere in the repo
filePatterns: ["src/**/*.e2e.ts"]        // E2E tests under src/
filePatterns: ["packages/**", "apps/**"] // monorepo source changes
filePatterns: ["*.{json,yaml}"]          // config files at repo root
filePatterns: ["src/**/*.{ts,tsx}"]      // all TypeScript source files
filePatterns: [".github/**"]             // CI/workflow changes

Debouncing & Deduplication

Deduplication

Before creating a test run, the system checks for an existing run scoped to:

  • Same projectId
  • Same commitSha
  • Same event type (push or pull_request)

within the last 1 hour. Duplicate triggers are rejected with a descriptive reason.

Debouncing

When debounceMs > 0, rapid triggers for the same projectId + branch + eventType are coalesced: each new trigger resets the timer, and only the final event (after the quiet period) is executed.

This prevents flooding the test runner when developers push multiple commits quickly.

configOverride: {
  debounceMs: 10_000, // wait 10s after last push before starting tests
}

GitHub Status Checks

When a githubToken is configured, the system automatically posts commit statuses at two points:

Lifecycle point State Description
Test run created pending "E2E tests are running…"
reportResult with PASSED success "X/Y tests passed"
reportResult with FAILED failure "X/Y tests failed"
reportResult with ERROR error Error message (truncated to 140 chars)

The default status context is izri/e2e. This can be customised per integration.

Required GitHub token scopes

  • repo:status — post commit statuses on public and private repos

Integration with Webhook System

The test trigger system extends the webhook pipeline (issue #22):

  1. GitHub sends a push/PR event to the Hono webhook endpoint (apps/api)
  2. The endpoint validates the HMAC-SHA256 signature and stores a webhook_events record
  3. The webhook service calls testTriggers.dispatchPush or testTriggers.dispatchPR
  4. Test trigger service evaluates conditions, creates a test_runs record, and posts a pending GitHub status
  5. An external test runner (CI agent / queue worker) picks up the pending run, executes tests, and calls testTriggers.reportResult

Data Model

Test runs are stored in the test_runs table:

id           VARCHAR(191) PRIMARY KEY  -- ULID
project_id   VARCHAR(191) NOT NULL
type         VARCHAR(32) NOT NULL      -- 'push' | 'pr' | 'manual'
status       VARCHAR(32) NOT NULL      -- 'PENDING' | 'RUNNING' | 'PASSED' | 'FAILED' | 'ERROR'
passed_tests INTEGER DEFAULT 0
failed_tests INTEGER DEFAULT 0
total_tests  INTEGER DEFAULT 0
skipped_tests INTEGER DEFAULT 0
coverage     REAL
duration     INTEGER                   -- ms
error        TEXT
logs         TEXT
results      JSONB                     -- includes commitSha for dedup
started_at   TIMESTAMP
completed_at TIMESTAMP
created_at   TIMESTAMP DEFAULT now()
user_id      VARCHAR(191) NOT NULL     -- triggering actor

Error Handling

  • Signature invalid: rejected at webhook layer before reaching trigger service
  • Unknown project: dispatchPush / dispatchPR returns triggered: false with reason
  • Duplicate trigger: returns triggered: false — not an error
  • GitHub Status API failure: logged but does not fail the test run creation (best-effort)
  • Debounced trigger: returns triggered: false with debounce reason — the real trigger fires asynchronously after the window

Related

Reading this with an agent? /docs/api/test-triggers.md serves the raw markdown.

All docs