This monorepo keeps all apps in a single repository. The same CI/CD patterns work when each app lives in its own repository. This document explains how to adapt.
- Separate teams own each app with independent release cycles
- Access control requires different permissions per app
- CI/CD isolation — one app's broken pipeline shouldn't block another
- Repository size — very large apps benefit from separate repos
| Concern | Monorepo | Multi-Repo |
|---|---|---|
| Change detection | dorny/paths-filter / rules:changes |
Not needed — any push triggers the pipeline |
| Shared packages | pnpm workspace (workspace:*) |
Publish to npm/GitHub Packages, import via version |
| Atomic changes | Single PR updates shared code + app | Two PRs: one to publish package, one to consume |
| CI config | One workflow file with conditional jobs | One workflow file per repo (simpler) |
| Deploy scripts | Same scripts/ directory, shared |
Copy scripts/deploy-spa.sh into each repo |
Copy these scripts into each app's repository:
scripts/deploy-spa.sh— deploy the built SPAscripts/ssh-setup.sh— configure SSH in CIscripts/validate-required-env.sh— validate env varsscripts/build-spa.sh— build the SPAscripts/health-check.sh— post-deploy verificationscripts/rollback-spa.sh— rollback to previous version
These scripts are self-contained and work without the monorepo structure.
Each repo gets a simpler workflow — no change detection needed:
name: Validate and Deploy
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: [lint, typecheck, unit-tests, build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Configure SSH
run: bash scripts/ssh-setup.sh
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
- name: Deploy
run: bash scripts/deploy-spa.sh my-app
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_MY_APP_PATH: ${{ secrets.DEPLOY_MY_APP_PATH }}Instead of workspace:* references:
- Publish
@repo/shared-uiand@repo/shared-utilsto a package registry (npm, GitHub Packages, GitLab Package Registry) - Each app installs them as regular dependencies:
"@repo/shared-ui": "^1.0.0" - When shared packages change, publish a new version, then update consumers
If multiple repos deploy to the same server:
- Each repo has its own
DEPLOY_*_PATHsecret pointing to its target directory - Deploys are independent — no coordination needed for static file deployments
- The nginx config on the server maps paths to directories (same as monorepo)
- Rollback is per-app, not all-or-nothing
Monorepo advantages:
- Single PR for cross-cutting changes
- Shared packages always in sync
- One CI config to maintain
- Change detection avoids unnecessary work
Multi-repo advantages:
- Simpler CI per repo (no change detection)
- Independent team autonomy
- Faster CI runs (smaller codebase per pipeline)
- Clearer ownership boundaries