React SPA Monorepo CI/CD: How to Automate Testing and Deploy Only What Changed
Most monorepo articles stop at folder structure. They show you how to organize apps and packages, maybe add a shared config, and call it done. But folder structure is not what makes a monorepo production-ready. What actually matters is whether your CI/CD pipeline can detect what changed, run the right validation gates, and deploy only the apps that need to move.
That is the difference between a monorepo that looks clean and one that ships reliably. The react-spa-monorepo-cicd repository is built around that distinction. It is a reference architecture with 7 validation gates, intelligent change detection, Playwright E2E coverage, staging and production branches, and automated rollback over SSH and rsync. This article breaks down why each of those pieces matters and how they fit together into a release system that scales.
Why CI/CD is the real value in a SPA monorepo
A monorepo by itself does not reduce deployment risk. Putting three apps in one repository without shared automation is just a shared folder with extra merge conflicts. The actual benefit comes from consistent validation, shared automation, and selective deployment — none of which happen automatically because your apps share a package.json root.
The pipeline matters more than the folder tree. A monorepo with strong CI/CD gives every app the same validation standards, the same deployment path, and the same rollback process. A monorepo without CI/CD gives you three apps that are harder to release independently than if they lived in separate repositories.
This is why the react-spa-monorepo-cicd repo is positioned as a reference architecture for multiple apps with automated deployment and full CI/CD validation, not just a starter template. The value is in the release system, not the workspace layout.
How the pipeline works from pull request to production
The release path follows a linear promotion model with validation at every stage:
- Open a pull request — CI detects which areas changed and runs the full validation suite
- Merge to staging — the same checks re-run against the merged result, then affected apps deploy to the staging environment
- Verify staging — manual or automated smoke tests confirm the deploy is healthy
- Merge staging to main — validation runs again, and affected apps deploy to production
- Post-deploy health checks — the pipeline confirms each deployed app responds correctly
Tests intentionally re-run after merge. A pull request validates the branch in isolation, but the merged state can differ because of conflicts or concurrent changes. Post-merge validation ensures the exact merged code still passes every gate before anything is deployed.
This is safer than deploying directly from feature branches because every production deploy has been validated at least three times: on the PR, after merge to staging, and after merge to main. Each validation catches a different category of problem.
The seven validation gates that protect every deploy
The pipeline enforces seven categories of checks before any deployment happens, ordered from cheapest to most expensive:
- Format check — Prettier catches style inconsistencies in seconds
- Lint — ESLint flags code quality issues and potential bugs
- Type check — TypeScript compiler verifies correctness across all packages
- Unit tests — Vitest runs fast, isolated tests for components and utilities
- Build — Vite compiles each app into production-ready static assets
- Security audit — dependency scanning flags known vulnerabilities
- E2E tests — Playwright runs browser-level tests against the built apps
The ordering is deliberate. Format and lint checks cost almost nothing to run. If they fail, there is no reason to spend time building three apps and launching a Docker stack for E2E. Fail early on cheap checks so you only spend resources on expensive checks when the basics are already clean.
This is the same fast-fail principle that makes any CI pipeline efficient. The difference is that this repo applies it consistently across a monorepo with multiple deployable apps, which means the cost savings compound. If a lint error in the admin SPA fails the pipeline in 15 seconds, you avoid minutes of unnecessary builds, security scans, and browser tests for both SPAs and the main site.
Why selective deploys are the biggest monorepo win
Without change detection, every push rebuilds and redeploys every app. That wastes compute, slows feedback, and increases the blast radius of every release. A bug in the admin SPA should not require redeploying the portal SPA and the marketing site.
The repository uses intelligent change detection to map file changes to deploy targets:
- Changes in
apps/admin-spa/deploy only the admin SPA - Changes in
apps/portal-spa/deploy only the portal SPA - Changes in
apps/main-site/deploy only the main site (a static HTML site, not a React SPA) - Changes in
packages/trigger both SPA deploys because shared code could affect either consumer - Changes to docs or markdown skip deployment entirely
This directly affects three things teams care about: CI cost, feedback speed, and release risk. Building and deploying one app instead of three cuts compute time roughly in proportion. A targeted pipeline finishes faster, which means faster code review cycles. And deploying only the changed app reduces the surface area for regressions in unrelated code.
Selective deployment is where monorepos earn their keep. Without it, monorepos quickly become slow and expensive as the number of apps grows. With it, adding a new app to the repository does not slow down deployments for every other app.
Where Playwright fits in a serious frontend release process
E2E tests are the final confidence layer before deploy. They validate deployable behavior in a production-like environment, which is fundamentally different from unit tests running against isolated components.
The repository runs Playwright tests against built artifacts served by Docker and nginx, not against a dev server. That distinction matters because dev servers use hot module replacement, skip optimizations, and serve assets differently. Running E2E against production build output catches problems that only appear in real deployment conditions.
The E2E suite covers:
- App loading — each SPA renders its root component
- Route boundaries — client-side routing works within each app
- Cross-app navigation — links between SPAs resolve without errors
- Visual regression — screenshots are compared against baselines to catch unintended UI changes
- Accessibility — axe-core checks flag critical violations before deploy
This is pipeline-level confidence, not developer-level debugging. E2E tests should not replace unit tests. They should answer a different question: “Does the built artifact work correctly when served the way production will serve it?” If the answer is no, the deploy does not happen.
Why staging and production should behave differently
The repository separates environments by branch. Pushes to staging build with staging mode, use staging secrets, and deploy to the staging server. Pushes to main build with production mode, use production secrets, and deploy to the production server. No application code changes between environments — only the build mode, secrets, and deploy target differ.
Staging is not a demo environment. It is a confidence layer. The purpose of staging is to catch problems that only appear in a deployed environment — wrong environment variables, broken server configuration, stale assets — before those problems reach customers.
Production deploys can require manual approval through GitHub Environment protection rules. After validation passes, the deploy job pauses and waits for an authorized team member to approve the release. This is controlled automation: the pipeline handles the mechanics, humans handle the judgment call. Fully automated does not have to mean fully unreviewed.
The deployment model: rsync, backups, and rollback
The repository deploys using rsync over SSH. Each app has its own target path on the server, so deploying the admin SPA does not risk overwriting the portal SPA’s files. The deploy scripts support dry-run mode so you can verify intent before transferring files.
Before each deployment, the pipeline creates a timestamped backup of the current version. If a deploy introduces a problem, rollback is a single command that restores the most recent backup. This is not an emergency afterthought — it is part of the delivery design.
Post-deploy health checks close the loop. A successful file transfer does not guarantee a working app. The server configuration could be wrong, environment variables could be missing, or the build could have been created with the wrong mode. Health checks confirm the deployed app actually responds, and if they fail, the pipeline reports a failure even though the deploy itself succeeded.
Simple deployment mechanisms are often more maintainable than overbuilt platform complexity. Rsync, SSH, and timestamped backups are boring technologies, but they are predictable, debuggable, and understood by every ops engineer on the team.
If you are also thinking about securing what flows through your pipeline, see Software Supply Chain Security in the AI Era for dependency integrity and SBOM strategies, and Securing AI Coding Agent Workflows for sandboxing and review gates when AI-generated code enters monorepo pull requests.
GitHub Actions and GitLab CI in the same monorepo
The repository includes both .github/workflows/validate-and-deploy.yml and .gitlab-ci.yml with setup documentation for each. Supporting both is useful for teams migrating between platforms, serving different clients, or evaluating which CI system fits their workflow better.
The more important point is that the CI/CD pattern matters more than the vendor. The same validation ladder, the same selective deploy logic, and the same branch-based environment model work in both systems. The implementation syntax differs, but the architecture is portable. If you understand the pattern, you can implement it in any CI system.
What teams usually get wrong with monorepo CI/CD
The most common mistakes are not technical failures. They are architectural omissions:
- Treating the folder structure as the whole solution. A clean workspace does not protect you from broken merges or wasted CI minutes.
- Skipping change detection. Without it, every push rebuilds everything, which eliminates the speed advantage of a monorepo.
- Running E2E too early or too late. E2E before build wastes time on tests that cannot run. E2E after deploy means failures reach an environment before they are caught.
- Deploying everything on every merge. This turns every release into a full-stack event regardless of what actually changed.
- Having no rollback plan. If your only recovery option is “push a fix forward,” you are one bad deploy away from extended downtime.
- Mixing staging and production behavior. If staging deploys automatically but production also deploys automatically, staging is not actually gating anything.
Most of these are solved by designing the pipeline intentionally rather than letting it evolve organically. The react-spa-monorepo-cicd repo exists specifically to demonstrate what an intentional pipeline looks like.
Who this kind of setup is best for
This architecture fits teams that are running multiple SPAs or a mix of static and SPA apps from one repository. That includes:
- Product teams managing separate user-facing apps that share design systems or API clients
- Agencies building and maintaining multiple frontend surfaces for clients
- Internal platform teams with shared packages consumed by several apps
- Developers who want simple but production-capable CI/CD without adopting a heavyweight monorepo framework
If you are already using Nx or Turborepo and are happy with the complexity, this approach may feel too manual. But if you want to understand what those tools abstract away, or you want a lighter-weight alternative that you fully control, this is a strong starting point.
What to do next
If you want to implement this pipeline hands-on, follow the companion tutorial: How to Set Up CI/CD and Automated Tests for a React SPA Monorepo. It walks through every step from cloning the repo through configuring validation gates, running Playwright E2E tests, setting up selective deploys, and configuring staging and production releases.
For teams already running CI/CD pipelines, consider mapping the patterns in this article to your existing setup:
- Add change detection if you are rebuilding everything on every push
- Order your validation gates from cheapest to most expensive
- Separate staging from production with branch-based environment rules
- Add post-deploy health checks so a successful transfer does not end the story
- Build rollback into the pipeline instead of treating it as an emergency procedure
If you want a starting point instead of a blank architecture doc, this repo gives you one: a monorepo release system with validation gates, selective deployment, E2E testing, and rollback already built in. The next move is to adapt those patterns to your own apps and deployment targets instead of reinventing CI/CD from scratch.
Get the react-spa-monorepo-cicd repo →
Follow the step-by-step tutorial →
For more on the topics covered in this guide:
- Software Supply Chain Security in the AI Era covers dependency integrity, SBOM generation, and provenance verification for CI/CD pipelines.
- Securing AI Coding Agent Workflows addresses sandboxing and review gates when AI-generated code enters monorepo pull requests.
- API Security for AI Apps and Modern SaaS Integrations covers authentication, authorization, and rate limiting patterns relevant to any deployment pipeline.
- Harden Your CI/CD Pipeline with Sigstore, SLSA, and SBOMs walks through artifact signing and provenance enforcement step by step.
Frequently asked questions
What is the difference between a monorepo and a polyrepo for CI/CD?
A monorepo keeps multiple apps and shared packages in one repository, which enables shared CI/CD pipelines, consistent validation, and selective deployment based on change detection. A polyrepo uses separate repositories per app, which simplifies per-app pipelines but requires more coordination for shared dependencies and cross-app testing.
How does selective deployment work in a monorepo?
Selective deployment uses change detection to map modified files to deploy targets. If only one app’s directory changed, only that app is built and deployed. Changes to shared packages trigger deploys for all apps that depend on them. This reduces CI cost, speeds up feedback, and limits the blast radius of each release.
Should E2E tests run before or after deployment?
E2E tests should run before deployment, against built artifacts served in a production-like environment. This catches integration and deployment problems before they reach staging or production. Post-deploy health checks then confirm the actual deployed app responds correctly.
Can I use this CI/CD pattern with Nx or Turborepo?
Yes. The validation ladder, selective deploy logic, and branch-based environment model are architecture patterns, not tool-specific configurations. If you already use Nx or Turborepo for task orchestration and caching, you can layer these CI/CD patterns on top. The react-spa-monorepo-cicd repo demonstrates the patterns without a heavyweight framework so you can see exactly what those tools abstract away.
How do I add a new app to the monorepo pipeline?
Create the app under apps/, add its directory to the change detection script (scripts/changed-files.sh), and wire up corresponding build, deploy, and E2E targets in the CI workflow. The selective deploy model scales linearly — each new app gets its own conditional path without slowing down deploys for existing apps.
How does this approach handle shared package changes?
Changes under packages/ trigger rebuilds and deploys for all apps that depend on those shared packages. This is intentional — a bug in shared code could break any consumer, so every dependent app must be rebuilt, retested, and redeployed to catch regressions.