Skip to content

DevOps

GitHub Actions for Velocity X — Lint, Type-Check, Test, Visual Regress on Every PR

Single .github/workflows/ci.yml runs 5 checks in parallel. Fails fast. Catches regressions before they land on main.

⚙️ 🚀
Here's the problem: a developer commits TypeScript that passes locally (no type errors), pushes to main, and suddenly the build fails in production. Or they fix a button component, visual tests pass on their machine, but on the CI runner the layout breaks. Or a dependency upgrade silently breaks an integration. By then, it's deployed. Users see it. You're debugging in prod. GitHub Actions CI prevents all three. A single workflow runs five checks in parallel on every PR: eslint, tsc --noEmit, vitest unit tests, Playwright visual regression, and Lighthouse CI. Total time: ~3 minutes. One check fails, the PR is blocked. No red builds on main. No surprises in production. Velocity X uses this. Here's the real workflow, the cache strategy that makes it fast, and the parallel patterns that keep it under 3 minutes.

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.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.