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/settings/access-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@v6
- 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@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with: { node-version: 24, 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
Automated HTML Reports — Deploy to a Stable URL on a Schedule
Deploy cron-driven HTML reports to a permanent URL. Replace Notion, Google Sheets, or Tableau with a simple pipeline and a link that's always current.
Move from Weebly to VibeHost
Weebly is closing in 67 countries on September 27, 2026. See the full country list, all three deadlines, and how to import your site — free, no export files.