Monorepo deploys
One repo, many apps. One CI matrix with a leg per app deploys only what changed.
Your monorepo holds several deployable apps (web, docs, marketing, admin), and each one is its own VibeHost app. CI deploys only the apps that changed in a given commit or PR.
Assumed layout
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:
- Go to vibehost.com/settings/access-tokens and create a token.
- Choose the scopes
apps:readandapps:deploy. - Under resources, pick
my-web,my-docs, andmy-marketing. - Store it as
VIBEHOST_TOKEN.
The PAT can only deploy these three apps. If a compromised CI job tries to deploy anywhere else, it gets PAT_RESOURCE_NOT_ALLOWED.
Workflow for the 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 -o vibehost-install.sh https://vibehost.com/install.sh && sh vibehost-install.sh && rm vibehost-install.sh
- env:
VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
run: |
~/.vibehost/cli/vibehost deploy ./${{ matrix.dir }}/${{ matrix.out }} \
--app ${{ matrix.app }} --jsonWhat this does:
- The
changesjob runspaths-filter, which returnstrueorfalsefor each app depending on which files changed. - The
deployjob fans out into one matrix leg per app. Each leg checks its ownchangedflag and exits early if its app wasn't touched. - A change under
packages/**redeploys every app that uses the shared packages. For finer-grained dependency tracking, useturbo runornx affectedinstead of paths-filter.
Workflow for PR previews
Deploy each affected app to a channel for the PR:
- if: matrix.changed == 'true'
env:
VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
PR: ${{ github.event.pull_request.number }}
run: |
URL=$(~/.vibehost/cli/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 }}In practice, env doesn't carry over between jobs, so pass the URLs with actions/upload-artifact or job outputs, whichever suits your setup.
With Turborepo
Turbo's --filter and remote cache make this faster on cold runners:
- run: pnpm dlx turbo run build --filter=...[origin/main]That builds only the packages the diff affects. After the build, you still deploy each 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/cli/vibehost deploy "$dir/dist" --app "$app"
doneChannel naming for monorepo previews
This is the naming convention we use internally:
productionfor the main branch.pr-NNfor each PR.feature-foofor long-lived feature branches.stagingfor changes waiting to be promoted. Deploy here, let it soak, then deploy the same build to production.
Channel names appear in URLs, so they must be DNS-safe: [a-z][a-z0-9-]*, 1 to 32 characters, no underscores.
Pitfalls
- A change under
packages/**redeploys everything. That's usually right, since every app that uses a shared library should rebuild when it changes. For very large monorepos,turbo affectedornx affectedis more precise than paths-filter. - Don't share PATs across workspaces. If your monorepo deploys to more than one workspace (e.g.
dev-wsandprod-ws), use a separate PAT and GitHub environment for each. - Each directory links to one app.
vibehost linkwrites.vibehost/project.json. In CI, pass--appexplicitly instead of linking, because the link file isn't committed.
See also
- CI/CD with GitHub Actions is the single-app version.
- PR previews sets up previews for a single app.
- Channels explains how channels work and their limits.
Deploy HTML reports on a schedule
Deploy cron-driven HTML reports to a permanent URL that always shows the latest data, instead of sharing them through Notion, Google Sheets, or Tableau.
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 for free without export files.