Skip to main content

Continuous Integration

Refract ships with three GitHub Actions workflows. Together they make sure every PR is safe to merge and that main always has a clean coverage baseline.

Overview

All three workflows run on ubuntu-latest and use Node 24 with pnpm 10.

integration.yml — runs on every PR

Trigger: any PR opened, updated, or reopened against main. This is the main gate. It runs four parallel jobs, and all four must pass before a PR can merge.

tests job

The most involved job. It:
  1. Spins up Postgres and Redis as service containers.
  2. Installs deps and builds all workspace packages (the backend test suite needs the compiled tool packages in dist/).
  3. Runs the shared/tool package tests (pnpm test:modules).
  4. Runs the frontend Vitest suite.
  5. Downloads the coverage baseline from main — first from the main-coverage workflow artifact, then from a previous integration run as a fallback. If neither exists, it checks out main, generates coverage itself, then switches back to the PR branch.
  6. Runs the backend Jest suite and posts a coverage diff comment on the PR via jest-coverage-report-action.
The coverage comparison tells you at a glance whether your PR is increasing or decreasing test coverage.

builds job

Runs pnpm -r run build across every workspace package. Catches TypeScript compile errors that don’t show up at runtime.

lints job

Runs pnpm -r run lint across every workspace package. Fails on any ESLint error.

codegen-verification job

Runs make gql-codegen (backend schema dump + frontend TypeScript hooks generation) and then checks whether any files changed. If the generated code in apps/portal/src/gql/hooks.ts or the schema file doesn’t match what’s committed, this job fails and tells you to run make gql-codegen and commit the result. make local-verification runs the same dirty-tree check after its codegen step. This means you can never accidentally ship a frontend that’s out of sync with the backend’s GraphQL schema.

production-browser-static-verify job

Builds Dockerfile.production and smokes marketing routes (/, /pricing, /about, /blog → 301, /blog/ → 200, /admin). make local-verification runs the same make verify-production-browser-static target as its last step.

main-coverage.yml — runs on push to main

Trigger: any push to main (which in practice means every merged PR). This workflow has one job: run the full backend Jest suite and upload the results as a main-jest-coverage artifact. That artifact is what integration.yml’s tests job downloads for coverage comparison. The artifact is retained for 90 days. Keeping main’s coverage up to date means every new PR gets an accurate baseline to compare against rather than a stale one.

mintlify-docs-sync.yml — runs on PR merge or manual dispatch

Trigger: PRs merged into main, or manually via workflow_dispatch. This workflow converts the source Markdown in apps/documentation/ to MDX, rewrites internal links to Mintlify-compatible routes, validates the output, and commits it back to the repo. Mintlify picks up the commit automatically. See Documentation for a full breakdown of the sync script.

Running it manually

Go to Actions → mintlify-docs-sync → Run workflow. You can optionally set: The workflow now hard-locks both source and target docs roots to apps/documentation so no manual dispatch can accidentally write docs to another path (for example /documentation). The sync report is uploaded as an artifact on every run (even dry runs) so you can inspect what changed.

Running CI locally

You can’t run the full CI pipeline locally (it needs GitHub’s service containers and parallel jobs), but you can approximate the quality gate.

Preferred: make local-verification

Same migrate → lint → build → test → codegen → dirty-tree → production browser-static checklist as agents use before merge. Default is Docker Compose. On a runner with Postgres and Redis on localhost (cloud VM, or laptop without relying on docker compose exec for lint/test):
Full contract (backends, step order, /shared, skip-install): Local verification. What CI still does that this gate does not: coverage PR comments, parallel job speed, and other specialized jobs (dev-proxy Caddyfile, Dockerfile glob sync, Lighthouse). Dirty-tree codegen and production browser-static smoke run in both places.

Piecewise commands

💡 Tip: if a CI job fails on something that passes locally, the most common cause is a stale dist/ — the CI always builds from scratch. Try make clean-docker and make fresh-start to get a clean slate.

What’s next?

  • Local verification — compose vs native exec backend
  • Testing — how tests are structured, coverage thresholds, and mocking patterns.
  • Linter — ESLint and Prettier configuration.
  • Documentation — how the docs sync workflow fits together.