Monorepo deploys
One repo, many apps. Per-package CI matrix, only deploy what changed.
The pattern: monorepo with multiple deployable apps (web, docs, marketing, admin). Each is its own VibeHost app. CI deploys only the apps that changed in a given commit / PR.
Layout assumption
my-monorepo/
├── apps/
│ ├── web/ → VibeHost app "my-web"
│ ├── docs/ → VibeHost app "my-docs"
│ └── marketing/ → VibeHost app "my-marketing"
├── packages/
│ └── ui/ (shared, not deployed)
└── package.json (pnpm / npm / yarn workspaces)One PAT per workspace, restricted to app IDs
Issue a single PAT for CI, restricted to the three app IDs:
- vibehost.com/account/tokens → Create token.
- Scopes:
apps:read,apps:deploy. - Resources: pick
my-web,my-docs,my-marketing. - Store as
VIBEHOST_TOKEN.
The PAT can only deploy these three apps; if a compromised CI tries to deploy elsewhere, it gets PAT_RESOURCE_NOT_ALLOWED.
Workflow — main branch
name: Deploy
on:
push:
branches: [main]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
web: ${{ steps.filter.outputs.web }}
docs: ${{ steps.filter.outputs.docs }}
marketing: ${{ steps.filter.outputs.marketing }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
web:
- 'apps/web/**'
- 'packages/**'
docs:
- 'apps/docs/**'
- 'packages/**'
marketing:
- 'apps/marketing/**'
deploy:
needs: changes
if: needs.changes.outputs.web == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.marketing == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- app: my-web
dir: apps/web
out: dist
changed: ${{ needs.changes.outputs.web }}
- app: my-docs
dir: apps/docs
out: out
changed: ${{ needs.changes.outputs.docs }}
- app: my-marketing
dir: apps/marketing
out: dist
changed: ${{ needs.changes.outputs.marketing }}
steps:
- if: matrix.changed != 'true'
run: |
echo "Skipping ${{ matrix.app }} — no changes"
exit 0
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm --filter ./${{ matrix.dir }} build
- run: curl -fsSL https://vibehost.com/install.sh | sh
- env: { VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }} }
run: |
~/.vibehost/bin/vibehost deploy ./${{ matrix.dir }}/${{ matrix.out }} \
--app ${{ matrix.app }} --jsonWhat this does:
- changes job —
paths-filterreturnstrue/falseper app based on which files changed. - deploy job — fans out per app in a matrix. Each leg checks its own
changedflag and exits early if untouched. packages/**triggers everything that depends on shared packages. If you want finer-grained dependency awareness, integrateturbo runornx affectedinstead of paths-filter.
Workflow — PR previews
Per-PR channel per affected app:
- if: matrix.changed == 'true'
env:
VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
PR: ${{ github.event.pull_request.number }}
run: |
URL=$(~/.vibehost/bin/vibehost deploy \
./${{ matrix.dir }}/${{ matrix.out }} \
--app ${{ matrix.app }} \
--channel "pr-$PR" \
--json | jq -r '.data.url')
echo "PREVIEW_${{ matrix.app }}=$URL" >> "$GITHUB_ENV"Then collect URLs and post a single comment listing all changed apps:
comment:
needs: deploy
runs-on: ubuntu-latest
steps:
- uses: thollander/actions-comment-pull-request@v3
with:
comment-tag: vibehost-previews
message: |
Previews for this PR:
- my-web: ${{ env.PREVIEW_my-web }}
- my-docs: ${{ env.PREVIEW_my-docs }}
- my-marketing: ${{ env.PREVIEW_my-marketing }}(Note: env carry-over between jobs requires actions/upload-artifact or job outputs in practice — simplify per your tooling.)
With Turborepo
Turbo's --filter + remote cache makes this much faster on cold runners:
- run: pnpm dlx turbo run build --filter=...[origin/main]That builds only packages affected by the diff. After build, you still deploy per-app:
- run: |
for app in my-web my-docs my-marketing; do
dir="apps/${app#my-}"
test -d "$dir/dist" || continue # was it built?
~/.vibehost/bin/vibehost deploy "$dir/dist" --app "$app"
donePair with turbo-cache.vibehost.cc if you want a hosted Turbo remote cache — see the Turborepo docs for setup.
Channel naming for monorepo previews
The naming convention we use internally:
production— main branch.pr-NN— per PR.feature-foo— long-lived feature branches.staging— pre-promote slot (deploy here, soak, promote to production).
Channel names live in URLs, so DNS-safe ([a-z][a-z0-9-]*, 1–32 chars, no underscores).
Pitfalls
packages/**change triggers everything — that's correct in most cases (shared lib touched → every consumer rebuilds). For very large monorepos, preferturbo affectedornx affectedover coarse paths-filter.- Don't share PATs across workspaces — if your monorepo deploys to multiple workspaces (e.g.
dev-ws+prod-ws), use separate PATs and GitHub environments. - One linked app per dir —
vibehost linkwrites.vibehost/project.json. In CI you usually want to pass--appexplicitly instead of linking (the link file isn't committed).
See also
- CI/CD with GitHub Actions — the single-app version
- PR previews — the single-app preview pattern
- Channels — model + limits