GitLab CI for a frontend — staged checks, review app, Playwright
I rely heavily on GitLab CI. The baseline I have in mind is a plain React SPA—the kind you get from a stock scaffold (e.g. Vite + React, or the classic CRA-style setup): client-side routing, npm run build producing static assets, no full-stack framework in the repo. One pipeline can still carry that app from dependency install through every quality gate, onto a review environment, and finally into integration tests that hit a real deployed URL. The shape below is the pattern I keep coming back to—not minimal YAML porn, but the stages and intent I want every serious frontend MR to pass.
Why this layout
MRs should prove three things before anyone spends time in a browser: the app builds, behavior is covered by fast tests, and the diff doesn’t introduce style, type, or security noise. Only after that do I pay the cost of a live preview and slower E2E runs. GitLab’s review apps make that second phase natural: deploy once, test against the same URL reviewers will open.
Stage 1 — install
A dedicated install stage (or a well-cached prepare job) installs dependencies once—npm ci, pnpm install --frozen-lockfile, or whatever matches the repo—and passes artifacts (e.g. node_modules, lockfile validation) to later jobs. The goal is: no duplicate installs across parallel test jobs unless the cache already makes them cheap.
Stage 2 — test (everything short of a browser against production)
When test runs, I treat it as a checklist, not a single script:
- Build —
npm run build(Vite/React CLI output: adist/orbuild/folder of static files). If the bundle doesn’t compile, nothing else matters. - Unit tests — fast, isolated tests (Vitest, Jest, etc.).
- SAST — GitLab’s static application security scanning (or an equivalent job) so obvious issues surface in the MR.
- Prettier — formatting as a hard gate (
--check), so style debates don’t land in review. - Lint — ESLint (or Biome lint) with the project’s rules.
- Typecheck —
tsc --noEmit(or the framework’s type-check command) separate from the build when it catches different failure modes. - Dependency / vulnerability scanning —
npm audit, GitLab Dependency Scanning, or SCA in the same spirit; easy to tighten over time without restructuring the pipeline.
Jobs here can run in parallel after install; failures are visible per concern (types vs lint vs audit) instead of one opaque “ci failed” step.
Stage 3 — deploy review environment
After test is green, the pipeline deploys to a review app (dynamic environment per branch or MR)—for a React SPA that usually means publishing the static build to whatever serves your previews (object storage + CDN, Nginx, Kubernetes ingress, GitLab Pages–style hosting, etc.). That gives a stable-enough URL for humans and for automation. The deploy job is the bridge between “compiles on CI” and “behaves like the real product behind HTTP.”
Stage 4 — integration / E2E (Playwright)
With the review URL available, integration tests—I use Playwright against the deployed app—run against the same environment reviewers use. That catches routing, auth cookies, API base URLs, and asset loading bugs that unit tests miss. These jobs belong after deploy by design: they’re slower and flakier, so they shouldn’t block the fast feedback loop until the cheap gates pass.
Extending without rewriting
The same skeleton accepts more gates later: container scanning, license compliance, visual regression, perf budgets, or stricter SAST rules—usually as extra jobs or stages, not a redesign. The mental model stays: install → verify locally on CI → ship to review → prove it in a browser.
If I had to name the genre: it’s a case-shaped guide—opinionated ordering grounded in how I actually run frontends on GitLab.