VibeHost

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:

  1. Go to vibehost.com/settings/access-tokens and create a token.
  2. Choose the scopes apps:read and apps:deploy.
  3. Under resources, pick my-web, my-docs, and my-marketing.
  4. 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

.github/workflows/deploy.yml
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 }} --json

What this does:

  1. The changes job runs paths-filter, which returns true or false for each app depending on which files changed.
  2. The deploy job fans out into one matrix leg per app. Each leg checks its own changed flag and exits early if its app wasn't touched.
  3. A change under packages/** redeploys every app that uses the shared packages. For finer-grained dependency tracking, use turbo run or nx affected instead 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"
          done

Channel naming for monorepo previews

This is the naming convention we use internally:

  • production for the main branch.
  • pr-NN for each PR.
  • feature-foo for long-lived feature branches.
  • staging for 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 affected or nx affected is more precise than paths-filter.
  • Don't share PATs across workspaces. If your monorepo deploys to more than one workspace (e.g. dev-ws and prod-ws), use a separate PAT and GitHub environment for each.
  • Each directory links to one app. vibehost link writes .vibehost/project.json. In CI, pass --app explicitly instead of linking, because the link file isn't committed.

See also

On this page