Skip to main content
HostwaresHostwares

CI/CD Advanced

Production-grade pipelines — branch previews, gated deploys, secrets, rollbacks, and multi-environment workflows.

Overview

Beyond a single push → deploy hook, production teams need branch previews, environment promotion, gated releases, and instant rollbacks. Hostwares exposes these via a small API surface that fits any CI system — GitHub Actions, GitLab CI, or plain curl.

PatternUse CaseMechanism
Branch previewsPR review without touching prodDeploy branch to preview site
Gated deploysRequire tests / approvalCI job → API deploy after pass
Env promotionStaging → productionSame image, different site/env
Instant rollbackBad releaseRollback to prior image (no rebuild)

Deployment Model

Hostwares treats each site as an environment. A typical setup:

SiteBranchURLEnv Vars
my-app-prodmainapp.example.comProd DB, prod keys
my-app-stagingstagingstaging.hostwares.appStaging DB, test keys
my-app-previewPR branchesPer-PR subdomainPreview DB / seeded data
# Core API — same for any CI
# Trigger deploy for a specific branch
curl -X PATCH https://hostwares.com/api/sites/$SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "deploy", "branch": "staging"}'

# Poll status
curl -s https://hostwares.com/api/sites/$SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" | jq .status
# QUEUED | BUILDING | DEPLOYING | RUNNING | FAILED

GitHub Actions

Simple: deploy on push to main

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tests
        run: npm ci && npm test

      - name: Deploy to Hostwares
        run: |
          curl -f -X PATCH "https://hostwares.com/api/sites/${{ secrets.HOSTWARES_SITE_ID }}" \
            -H "Authorization: Bearer ${{ secrets.HOSTWARES_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"action": "deploy"}'

      - name: Wait for healthy
        run: |
          for i in {1..30}; do
            STATUS=$(curl -s "https://hostwares.com/api/sites/${{ secrets.HOSTWARES_SITE_ID }}" \
              -H "Authorization: Bearer ${{ secrets.HOSTWARES_API_KEY }}" | jq -r .status)
            echo "Status: $STATUS"
            [[ "$STATUS" == "RUNNING" ]] && exit 0
            [[ "$STATUS" == "FAILED" ]] && exit 1
            sleep 10
          done
          echo "Timed out waiting for deploy" && exit 1

With concurrency control

# Prevent overlapping deploys per branch
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

jobs:
  deploy:
    # Only deploy if tests passed and branch is main
    if: github.ref == 'refs/heads/main'
    # ... same steps as above

Branch Previews

Give every PR its own live URL by deploying the branch to a preview site.

# .github/workflows/preview.yml
name: Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

      - name: Deploy preview
        run: |
          curl -f -X PATCH "https://hostwares.com/api/sites/${{ secrets.PREVIEW_SITE_ID }}" \
            -H "Authorization: Bearer ${{ secrets.HOSTWARES_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"action": "deploy", "branch": "${{ github.head_ref }}"}'

      - name: Comment PR with URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: "Preview: https://preview-${{ github.event.number }}.hostwares.app"
            });
Preview StrategyProsCons
One preview site, redeployed per PRSimple, cheapOnly one PR preview at a time
Site per PR (via API create)Parallel previewsRequires cleanup job
Single site with branch paramNo extra sitesBranch must exist on Hostwares remote

Gated Deploys

Require checks to pass before production deploys — tests, lint, typecheck, or manual approval.

# .github/workflows/gated.yml
name: Gated Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build   # fail fast before Hostwares build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production   # GitHub environment gate (requires approval if configured)
    steps:
      - name: Deploy to Hostwares
        run: |
          curl -f -X PATCH "https://hostwares.com/api/sites/${{ secrets.HOSTWARES_SITE_ID }}" \
            -H "Authorization: Bearer ${{ secrets.HOSTWARES_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"action": "deploy"}'

Use GitHub's Environments → Required reviewers to add a manual approval step before the deploy job runs.

Multi-Environment

Promote the same artifact from staging to production without rebuilding:

# Promote: redeploy prod site from the already-tested branch/commit
# Option A: deploy prod from main after staging passed
curl -X PATCH https://hostwares.com/api/sites/$PROD_SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "deploy", "branch": "main"}'

# Option B: separate sites per env, same repo
# staging site -> branch staging
# prod site    -> branch main
# Merge staging -> main only after QA

# GitLab CI equivalent
deploy_prod:
  stage: deploy
  only: [main]
  script:
    - curl -f -X PATCH "https://hostwares.com/api/sites/$PROD_SITE_ID"
        -H "Authorization: Bearer $HOSTWARES_API_KEY"
        -H "Content-Type: application/json"
        -d '{"action": "deploy"}'
  environment: production
EnvironmentSite ID SecretBranchDomain
StagingSTAGING_SITE_IDstagingstaging.hostwares.app
ProductionPROD_SITE_IDmainapp.example.com

Secrets & Scoping

  • Store HOSTWARES_API_KEY and HOSTWARES_SITE_ID as CI secrets — never in the repo.
  • Scope env vars per site — staging and prod sites have independent Site → Environment values.
  • Mark sensitive values as Encrypted so they are masked in logs and the dashboard.
# Rotate a secret via API
curl -X PATCH https://hostwares.com/api/sites/$SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "envVariables": [
      {"key": "JWT_SECRET", "value": "new-value-here", "encrypted": true}
    ]
  }'
# Then redeploy to apply
curl -X PATCH https://hostwares.com/api/sites/$SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "deploy"}'

Rollback Automation

Every successful deploy keeps its image. Roll back without rebuilding:

# Manual rollback
curl -X PATCH https://hostwares.com/api/sites/$SITE_ID \
  -H "Authorization: Bearer $HOSTWARES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "rollback", "deploymentId": "dep_prev123"}'

# Auto-rollback on health check failure (in CI)
HEALTH_URL="https://app.example.com/api/health"
if ! curl -f -s "$HEALTH_URL" | jq -e '.ok == true' > /dev/null; then
  echo "Health check failed — rolling back"
  curl -X PATCH "https://hostwares.com/api/sites/$SITE_ID" \
    -H "Authorization: Bearer $HOSTWARES_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"action": "rollback", "deploymentId": "dep_prev123"}'
  exit 1
fi
ScenarioRTOAction
Bad code< 30sRollback to previous image
Bad env var< 1 minFix var → redeploy or rollback
Bad migrationMinutesRestore DB backup → rollback code (see Backups)

Monorepos

Set Root directory per site so each app deploys from its package:

# Repo layout
apps/
  web/        -> site my-app-web, rootDirectory: apps/web
  api/        -> site my-app-api, rootDirectory: apps/api
packages/
  ui/         -> shared, not deployed directly

# In CI, only deploy when relevant paths changed
# .github/workflows/web.yml
on:
  push:
    branches: [main]
    paths: ["apps/web/**", "packages/ui/**"]

jobs:
  deploy-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm --workspace apps/web test
      - run: |
          curl -f -X PATCH "https://hostwares.com/api/sites/${{ secrets.WEB_SITE_ID }}" \
            -H "Authorization: Bearer ${{ secrets.HOSTWARES_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"action": "deploy"}'

Observability

  • Build logsSite → Deployments → [Deploy] → Logs or GET /api/sites/:id/deployments/:id/logs.
  • Runtime logsSite → Logs (tail) + AI log analysis.
  • Webhooks — subscribe to deploy.succeeded / deploy.failed to notify Slack (see Webhooks).
# Notify Slack on deploy result (add as final CI step)
curl -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"text": "Deploy to Hostwares: '"$STATUS"' — https://app.example.com"}'

# Or via Hostwares webhooks (server-side, no CI needed)
# Dashboard -> Webhooks -> Add endpoint: https://hooks.slack.com/...
# Events: deploy.succeeded, deploy.failed