Continuous Integration
Refract ships with three GitHub Actions workflows. Together they make sure every PR is safe to merge and thatmain always has a clean coverage baseline.
Overview
All three workflows run onubuntu-latest and use Node 24 with pnpm 10.
integration.yml — runs on every ready PR
Trigger: any non-draft PR opened, updated, reopened, or marked ready against main. Actions → integration → Run workflow starts only the production-smoke job (tests/coverage and the other PR jobs stay on pull-request events). main-coverage.yml is unchanged and still runs on push to main.
This is the main gate. Jobs run in parallel; a red job is the merge stop signal (GitHub free plan has no branch protection). Draft PRs skip these jobs.
tests job
The most involved job. It:
- Spins up Postgres and Redis as service containers.
- Installs deps and builds all workspace packages (the backend test suite needs the compiled tool packages in
dist/). - Runs the shared/tool package tests (
pnpm test:modules). - Runs the frontend Vitest suite.
- Downloads the coverage baseline from
main— first from themain-coverageworkflow artifact, then from a previous integration run as a fallback. If neither exists, it checks outmain, generates coverage itself, then switches back to the PR branch. - Runs the backend Jest suite and posts a coverage diff comment on the PR via
jest-coverage-report-action.
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-smoke job
Replaces the former production-browser-static-verify job. Builds apps/backend/Dockerfile.production the same way Railway does (repo-root context, APP_URL + MARKETING_GRAPHQL_URL build args, BuildKit GitHub Actions layer cache), starts throwaway Postgres 18.6 and Redis 8.2.2 containers, and runs the Railway webapp pre-deploy command inside the image:
node apps/backend/dist/tools/rds/sequelize/migrateUp.js && node apps/backend/dist/tools/rds/sequelize/seedUp.js
It then fails if the image has no compiled migration/seed files or if migrate+seed left SequelizeMigrations / SequelizeSeeds empty or core tables missing. The app boots with PORT=3000 and production-like env (NODE_ENV=production, CSP_MODE=enforce, dummy values for required vars, Stripe via a 0600 --env-file), waits for /health (fails immediately if the container exits, prints logs on timeout; job timeout-minutes: 20), smokes marketing routes (/, /pricing, /about, /blog → 301, /blog/ → 200, /admin), and runs assertCspBrowser.mjs.
make local-verification runs the same make verify-production-smoke script. Locally the script warns and skips if Docker is missing or Stripe keys are unset; the CSP check warns and skips if Chrome is missing. In CI those are hard failures. Does not change the main-coverage baseline artifact flow.
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 smoke 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):
/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 smoke run in both places.
Piecewise commands
💡 Tip: if a CI job fails on something that passes locally, the most common cause is a staledist/— the CI always builds from scratch. Trymake clean-dockerandmake fresh-startto 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.
