Referenta

Continuous Integration

What the GitHub Actions CI workflow validates, how the gate-and-concurrency pattern keeps stale PR runs from piling up, and what each job in ci.yml actually does.

A single GitHub Actions workflow, .github/workflows/ci.yml, validates every push to main and every pull request. It runs builds, type checks, formatting and linting, vitest, the Playwright e2e suite (Release Candidate PRs only), the Presidio service tests, migration hygiene, and creates a release tag + GitHub Release when a Release Candidate PR merges to main.

CI does not deploy anything. Dashboard and marketing deploys go through Vercel's GitHub integration; database migrations go through Supabase Branching. See Deploy Automation for the full picture.

Triggers And Concurrency

The workflow listens to:

  • push on main (every commit landed on production).
  • pull_request on a fixed set of activity types — opened, synchronize, reopened, ready_for_review, closed — not every activity type. closed is included so the release job can react to PR-merge events.

Concurrency is keyed by workflow + ref, and cancel-in-progress is true only for pull requests. A new PR commit cancels the previous in-flight PR run, but a new push to main queues behind any in-progress main runs instead of cancelling them. This keeps PR feedback fast without ever dropping a main-branch CI run mid-flight.

The Gate Pattern

Every job except release depends on a tiny gate job:

gate:
  if: >-
    github.event_name == 'push' ||
    (github.event_name == 'pull_request' && github.event.action != 'closed')
  runs-on: blacksmith-4vcpu-ubuntu-2404
  timeout-minutes: 2
  outputs:
    presidio: ${{ steps.changes.outputs.presidio }}
  steps:
    - run: echo "ci enabled"

    - name: Detect changed paths
      id: changes
      if: github.event_name == 'pull_request'
      run: |
        # queries the PR's changed files over the GitHub API and sets
        # presidio=true when services/presidio/ or this workflow file itself
        # changed; fails open (presidio=true) if the query comes back empty.
        ...

The gate's if is what closed-but-not-merged PRs skip on: heavy jobs sit behind it as needs: gate, so the entire build/quality/test/e2e/presidio/migrations matrix is skipped. The release job has its own merged-into-main condition and doesn't depend on gate, so a PR closing without a merge still triggers a (cheap, no-op) evaluation but does no real work.

On pull requests, the gate also queries the GitHub API for the PR's changed files and exposes a presidio output, used by the presidio job's path filter (see below). The query fails open: if it returns nothing (API hiccup), the gate sets presidio=true rather than silently skipping coverage.

Jobs

build

Runs pnpm build end-to-end across the monorepo. The job's env exposes the three Supabase secrets the Next.js builds need to compile (public-default keys for build-time inlining, plus the service-role key for server-side build steps):

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
  • SUPABASE_SERVICE_ROLE_KEY

TURBO_TOKEN and TURBO_TEAM are also exposed at the job level so Turborepo can read from and write to the remote cache. Timeout: 15 minutes.

quality

Runs biome ci . against the entire repo using biomejs/setup-biome@v2 pinned at 2.4.10. This is the same Biome version lefthook uses locally for the pre-commit hook, so failures here usually mean someone bypassed the hook with --no-verify. Timeout: 10 minutes.

test

Runs pnpm check-types (TypeScript across all packages via turbo run check-types), then a dedicated type-check of the Playwright e2e specs via pnpm --filter=dashboard check-types:e2e, then pnpm test (vitest). The middle step exists because the dashboard's own check-types excludes e2e/ — and since the e2e suite itself only runs on the Release Candidate PR (see e2e below), this is what stops spec rot on every other PR. SKIP_PRESS_MONITOR_TESTS=true is set on the pnpm test step because the press-monitor integration tests need external services that aren't provisioned in CI. Timeout: 15 minutes.

e2e

Runs the Playwright suite against a locally started Supabase stack (migrations applied, users seeded) and a production build of the dashboard. It only runs — and blocks — on the Release Candidate PR (pull_request with base.ref == 'main'); day-to-day PRs skip it entirely and should run it locally instead (apps/dashboard/e2e/README.md). The test job's e2e type-check above still covers every PR, so specs can't silently rot between RCs.

Steps: cache and install Chromium via Playwright, start Supabase with pnpm exec supabase start -x logflare,vector (dropping the analytics stack sidesteps a transient 502 nothing in the suite needs), reset and seed the database with up to three retries (the reset is idempotent), export the Supabase connection env (failing loudly if supabase status yields nothing, so a parsing miss can't silently ship blank NEXT_PUBLIC_* values), build the dashboard, run pnpm --filter=dashboard test:e2e, then upload the Playwright report as a 7-day-retention artifact whenever the job wasn't cancelled. Timeout: 45 minutes.

presidio

Builds the Presidio service test image with docker build --target test -t referenta-presidio:test services/presidio, then runs it. Tests live inside the Docker target so they don't need pnpm or Node setup at the job level. On pull requests this job is path-filtered by the gate's presidio output: it only runs when services/presidio/ or .github/workflows/ci.yml itself changed, since the service is self-contained and a PR that doesn't touch it can't break it. Pushes to main always run it. Timeout: 15 minutes.

migrations

Validates everything in supabase/migrations/ independently of Supabase Branching. Three checks run in sequence:

  1. Filename format. Every *.sql must match ^[0-9]{14}_[a-z0-9_]+\.sql$ (14-digit timestamp prefix, snake_case name, .sql extension). Misnamed files fail with a ::error:: annotation.
  2. Duplicate timestamps. The 14-digit prefix must be unique across all migrations. Two migrations sharing a timestamp fails the job.
  3. Stray files. Anything in supabase/migrations/ that isn't .gitkeep, *.sql, or *.bak emits a ::warning::. The Supabase CLI silently skips these, so the warning is meant to catch accidental commits (editor backups, leftover scratch files, etc.).

This is purely lexical hygiene; it does not run migrations. The actual migration apply happens on Supabase preview branches (see Database Workflow). Timeout: 5 minutes.

release

Only runs when all of the following are true:

  • the event is a pull request,
  • the PR is closed,
  • the PR was merged, and
  • the base branch is main.

It extracts a version from the PR title (Release Candidate: vX.Y.Z, RC: vX.Y.Z, or Release: vX.Y.Z), creates an annotated git tag on the merge commit, and publishes a GitHub Release whose notes come from the PR body. The tag push uses GH_TAGS_TOKEN, not the default workflow token, because the default token can't push tags that other workflows watch for.

A title that looks like a release (starts with Release Candidate:, RC:, or Release:) but doesn't match <Prefix>: vX.Y.Z fails the check loudly (exit 1) rather than silently producing no tag or release — so a near-miss like a missing v or missing space is caught instead of shipping unnoticed. A title that isn't release-shaped at all is ignored: the version-extract step sets an empty output, the downstream steps are skipped, and there's no error. Renaming a closed-and-merged PR after the fact won't retroactively trigger a release.

Full breakdown of the release flow lives in Release Automation.

Setup Composite Action

Most jobs use the shared ./.github/actions/setup composite that installs the pinned toolchain:

runs:
  using: composite
  steps:
    - uses: pnpm/action-setup@v6
    - uses: actions/setup-node@v6
      with:
        node-version: 24
        cache: pnpm
    - run: pnpm install --frozen-lockfile
      shell: bash

CI uses Node 24 with pnpm's lockfile-strict install. If you update the lockfile locally, commit it; CI fails the install step otherwise.

The quality job is the exception. It uses biomejs/setup-biome@v2 directly because it doesn't need Node or pnpm installed.

What CI Does Not Do

  • Deploy. Vercel does dashboard and www. Supabase Branching does database migrations. There are no longer deploy-dashboard.yml or preview-dashboard.yml workflows.
  • Run migrations. The migrations job validates lexical hygiene only. Migration application happens on Supabase preview, staging, and production branches.
  • Run press-monitor tests. Those need external infrastructure and are skipped via SKIP_PRESS_MONITOR_TESTS=true.
  • Block merges by itself. GitHub branch protection rules decide which job results are required for merge; CI just reports them.

On this page