What the Velocity X CI Does
Every push to a branch or PR triggers .github/workflows/ci.yml. Five jobs run in parallel:
- Lint: eslint catches code style, unused vars, import issues
- Type-Check: tsc --noEmit flags TypeScript errors without building
- Test: vitest runs all .test.ts unit tests with coverage
- Visual Regress: Playwright runs 8 key pages on desktop + mobile, compares against baseline
- Lighthouse: Three Lighthouse audits per page, fails if LCP > 1500ms or score < 95
If any job fails, the PR shows a red X. You can't merge to main. You fix the regression or revert the commit. No exceptions.
Why Parallel Jobs Beat Sequential
If you run these five checks one after another, you're waiting 15–20 minutes. If they run in parallel and the slowest (Playwright + Lighthouse) takes 3 minutes, you're done in 3 minutes. GitHub Actions' job matrix lets you define independent jobs that all kick off at once. Lint doesn't wait for tests. Tests don't wait for visual checks.
The only serial dependency: both visual and Lighthouse need the build to be ready. So build runs once, every other job reads the output.
Real Velocity X ci.yml
{`name: CI
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- run: npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- run: npm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: build-output
path: dist/
visual-regression:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- uses: actions/download-artifact@v3
with:
name: build-output
path: dist/
- uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- run: npm run test:visual
lighthouse:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- uses: actions/download-artifact@v3
with:
name: build-output
path: dist/
- run: npm install -g @lhci/cli@latest
- run: lhci autorun
env:
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
LHCI_GITHUB_APP_TOKEN: \${{ secrets.LHCI_GITHUB_APP_TOKEN }}
`}
Five jobs. Lint, typecheck, and test run independently and in parallel. Build runs once and uploads the dist/ folder as an artifact. Visual regression and Lighthouse both download that artifact (reducing redundant builds) and run their own checks. Five steps become three minutes because they're concurrent.
Cache Strategy: node_modules and Playwright Browsers
Every job starts with npm ci, which installs node_modules. Without caching, that's 60 seconds per job. With caching, it's a download hit — 5–10 seconds. The cache key is the package-lock.json hash. If you upgrade a dependency, the hash changes, a fresh install runs, and the new cache builds. Simple.
Playwright also caches browsers. The first run downloads Chromium, Firefox, WebKit (~200MB). On the next run, if the browser version hasn't changed, it's a local cache hit. No redownload. Same pattern: key by package-lock.json hash.
Six FAQs
Why not npm install instead of npm ci?
npm ci (clean install) respects package-lock.json exactly and is deterministic. npm install can upgrade minor versions and introduce non-determinism. On CI, you want the same versions every run.
How do I debug a failure locally?
Run the same command on your machine. If lint passes locally but fails on CI, cache is probably stale. Clear ~/.cache, reinstall, and try again. For visual tests, run npm run test:visual in the repo. Same logic, your machine.
Can I skip CI for small commits?
Yes, but don't. Even "small" commits break things. Typos in copy. Unused imports. CSS that breaks on mobile. CI is 3 minutes. The cost of a regression in prod is 30 minutes of debugging + frustrated users. Run CI every time.
What if Playwright baseline is out of date?
Update it explicitly. Run npm run test:visual -- --update-snapshots locally, commit the new baseline with a message like "chore: update visual regression baseline after redesign," and push. CI then compares against the new baseline.
How do I add a new check to CI?
Add a job to the workflow, give it a unique name, define its steps, and either run it independently or make it depend on build with needs: build. GitHub Actions will display it on the PR. All jobs must pass for a green check.
What if a job times out?
Increase the timeout-minutes: 10 (default is 6). If you're hitting timeouts on Lighthouse or visual tests, something is slow. Profile it: Is the build large? Are you testing too many pages? Are Playwright tests waiting for slow selectors? Fix the root cause, not the timeout.
The Bottom Line
A single ci.yml running five jobs in parallel catches lint errors, type bugs, test failures, visual regressions, and performance slips before they land on main. 3 minutes. No red builds in prod. Combined with Lighthouse CI performance budgets, the pipeline is a full gate: code quality, type safety, test coverage, visual correctness, and speed all enforced at once.
Ready to ship with confidence? Check Velocity X pricing to see how a modern CI/CD pipeline fits your team.