VibeHost

CI/CD with GitHub Actions

Auto-deploy on every push to main, smoke check, fail loud.

A production-ready GitHub Actions workflow that deploys a static site to VibeHost on every push to main, smoke-checks the URL, and posts the result back to the run.

Prerequisites

  • A repo with a Vite / Astro / Hugo / whatever-emits-a-build-dir project.
  • A PAT scoped to apps:deploy + apps:read, restricted to the target app ID. See Personal access tokens.
  • The PAT stored as a repository / environment secret named VIBEHOST_TOKEN.

The workflow

.github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: deploy-main
  cancel-in-progress: false   # don't cancel a deploy mid-upload

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - uses: pnpm/action-setup@v4

      - name: Install
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build

      - name: Install VibeHost CLI
        run: curl -fsSL https://vibehost.com/install.sh | sh

      - name: Deploy
        id: deploy
        env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
        run: |
          ~/.vibehost/bin/vibehost deploy ./dist \
            --app my-site \
            --json | tee deploy.json
          URL=$(jq -r '.data.url' deploy.json)
          IMMUTABLE=$(jq -r '.data.immutableUrl' deploy.json)
          echo "url=$URL" >> "$GITHUB_OUTPUT"
          echo "immutable=$IMMUTABLE" >> "$GITHUB_OUTPUT"

      - name: Smoke check
        env:
          URL: ${{ steps.deploy.outputs.url }}
        run: |
          # Retry up to 5 times — fresh deploys can take a few seconds to be globally routable.
          for i in 1 2 3 4 5; do
            CODE=$(curl -fsSL -o /dev/null -w "%{http_code}" "$URL" || echo "000")
            if [ "$CODE" = "200" ]; then
              echo "✓ $URL → $CODE"
              exit 0
            fi
            echo "Attempt $i: $URL → $CODE (retrying in 5s)"
            sleep 5
          done
          echo "✗ Smoke check failed: $URL never returned 200"
          exit 1

      - name: Summary
        if: always()
        env:
          URL: ${{ steps.deploy.outputs.url }}
          IMMUTABLE: ${{ steps.deploy.outputs.immutable }}
        run: |
          {
            echo "## Deploy"
            echo ""
            echo "**Live:** $URL"
            echo ""
            echo "**Immutable:** $IMMUTABLE"
          } >> "$GITHUB_STEP_SUMMARY"

That's the whole thing. Push to main; workflow runs; URL ends up in the GitHub Actions summary.

What each step does

  1. Checkout + setup-node + setup-pnpm — standard.
  2. Install + Build — runs on the runner, same as locally. We never run the build on VibeHost.
  3. Install VibeHost CLI — the install script is idempotent and small (single static binary).
  4. Deploy — uses the PAT from VIBEHOST_TOKEN. Parses the JSON response for the URLs.
  5. Smoke check — retries 5 times because a fresh deploy can take a few seconds to propagate globally.
  6. Summary — writes the URL to the GitHub run summary so anyone viewing the run sees it immediately.

The concurrency block ensures two pushes don't race; the later one waits for the earlier deploy to finish.

Variations

Deploy each PR to a unique channel, comment the URL on the PR, clean up on close. See the full recipe at PR previews.

Separate production and staging apps, each with its own PAT:

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - # ... build ...
      - env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN_STAGING }}
        run: ~/.vibehost/bin/vibehost deploy ./dist --app my-site-staging

  deploy-production:
    if: github.ref == 'refs/heads/main'
    environment: production
    needs: deploy-staging
    steps:
      - # ... build ...
      - env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN_PROD }}
        run: ~/.vibehost/bin/vibehost deploy ./dist --app my-site

Two separate apps, two separate PATs, two separate GitHub environments. Avoids accidentally pushing to production from a develop branch.

Only build + deploy apps that actually changed:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      docs: ${{ steps.filter.outputs.docs }}
      web: ${{ steps.filter.outputs.web }}
    steps:
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            docs: ['apps/docs/**']
            web:  ['apps/web/**']

  deploy-docs:
    needs: changes
    if: ${{ needs.changes.outputs.docs == 'true' }}
    # ... build + deploy apps/docs ...

  deploy-web:
    needs: changes
    if: ${{ needs.changes.outputs.web == 'true' }}
    # ... build + deploy apps/web ...

See the full monorepo recipe.

Trigger on a release tag for "ship a specific version":

on:
  push:
    tags: ['v*']
  workflow_dispatch:
    inputs:
      ref:
        description: 'Tag or commit to deploy'
        required: true

The workflow_dispatch path lets a maintainer roll back to an older tag without touching git.

Rolling back from CI

name: Rollback
on:
  workflow_dispatch:

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: curl -fsSL https://vibehost.com/install.sh | sh
      - env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
        run: |
          ~/.vibehost/bin/vibehost rollback --app my-site --json

Workflow dispatch button in GitHub UI → one-click rollback. Cheap insurance.

Troubleshooting

SymptomCauseFix
UNAUTHENTICATED in deploy stepPAT not set / wrong workspaceRe-check secret name; PAT must be from the workspace containing the app
TOKEN_WORKSPACE_MISMATCHPAT minted in workspace A, target app in workspace BRe-mint the PAT in the correct workspace
Smoke check 404 / 503Deploy too fresh; not yet routableWorkflow's retry loop handles this — bump retries if you see it consistently
RATE_LIMITED after many runsWorkspace deploy quota hit (~30/min)Reduce CI fanout, or upgrade plan
Build hangs / OOMs on the runnerDisk / memory limit on free runnerUse runs-on: ubuntu-latest-large or split the build step

See also

On this page