# CI/CD Pipeline — WorkRoot Website > Production deployment pipeline documentation for the WorkRoot website. > Framework: Astro SSR + Express | Platform: GitHub Actions --- ## Overview The CI/CD system consists of two complementary pipelines: | Workflow | File | Purpose | |----------|------|---------| | **E2E Test Suite** | `.github/workflows/e2e-tests.yml` | Runs on every push/PR — 9 test jobs | | **Deploy to Production** | `.github/workflows/deploy.yml` | Deploys on merge to `main` | --- ## Pipeline Architecture ``` Push to main │ ├─── E2E Test Suite (parallel) ──────────────────────────────────┐ │ ├── Smoke Tests (P0) │ │ ├── Critical Paths │ │ └── API Integration │ │ │ └─── Deploy Pipeline ────────────────────────────────────────────┘ │ ▼ [Job 1] Build & Verify │ ✓ npm ci │ ✓ tsc --noEmit │ ✓ npm run build │ ✓ Verify dist/ structure │ ✓ Upload build artifact │ ▼ [Job 2] Pre-Deploy Tests │ ✓ Download build artifact │ ✓ Start server locally │ ✓ Run smoke tests (Chromium) │ ✓ Run API integration tests │ ▼ [Job 3] Deploy (one of:) ├── Railway (DEPLOY_TARGET=railway) ├── Render (DEPLOY_TARGET=render) ├── VPS/PM2 (DEPLOY_TARGET=vps) └── Fly.io (DEPLOY_TARGET=fly) │ ▼ [Job 4] Post-Deploy Verification │ ✓ Smoke tests against live production │ ✓ Critical endpoint checks │ ✓ Upload results (14-day retention) │ ▼ [Job 5] Notify on Failure (if any stage failed) ``` --- ## Deployment Targets ### Option A: Railway (Recommended for simplicity) Railway auto-deploys from GitHub. CI/CD adds a verification layer. **Required secrets:** | Secret | Value | |--------|-------| | `RAILWAY_TOKEN` | From Railway dashboard → Settings → Tokens | **Required variables:** | Variable | Value | |----------|-------| | `DEPLOY_TARGET` | `railway` | **Setup:** 1. Create a Railway project, connect the GitHub repo 2. Set `START_COMMAND`: `npm run start:prod` 3. Set `PORT`: `10000` 4. Add the `RAILWAY_TOKEN` secret to GitHub 5. Set `DEPLOY_TARGET=railway` in GitHub repo variables --- ### Option B: Render Render uses deploy hooks triggered by the CI pipeline. **Required secrets:** | Secret | Value | |--------|-------| | `RENDER_DEPLOY_HOOK_URL` | From Render dashboard → Service → Deploy Hook URL | **Required variables:** | Variable | Value | |----------|-------| | `DEPLOY_TARGET` | `render` | **Render service configuration:** - Environment: Node - Build command: `npm ci && npm run build` - Start command: `npm run start:prod` - Health check path: `/api/health.json` --- ### Option C: VPS with PM2 (Full Control) SSH-based deployment to any Linux VPS. Includes automatic rollback. **Required secrets:** | Secret | Description | |--------|-------------| | `VPS_HOST` | VPS IP or hostname | | `VPS_USER` | SSH user (e.g., `ubuntu`, `deploy`) | | `VPS_SSH_PRIVATE_KEY` | Private key (contents of `~/.ssh/id_rsa`) | | `VPS_HOST_KEY` | SSH host key fingerprint | **Required variables:** | Variable | Value | |----------|-------| | `DEPLOY_TARGET` | `vps` | **VPS setup (one-time):** ```bash # On your VPS: # 1. Create deployment directory mkdir -p /var/www/workroot chown -R deploy:www-data /var/www/workroot # 2. Install Node.js 20 (via nvm or nodesource) curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # 3. Install PM2 globally npm install -g pm2 pm2 startup # Follow the output instructions # 4. Create logs directory mkdir -p /var/www/workroot/logs # 5. Set up Nginx reverse proxy (port 80/443 → 10000) # See Nginx config below ``` **Nginx configuration:** ```nginx server { listen 80; server_name workroot.in www.workroot.in; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name workroot.in www.workroot.in; ssl_certificate /etc/letsencrypt/live/workroot.in/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/workroot.in/privkey.pem; location / { proxy_pass http://localhost:10000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` **Deployment behavior:** 1. Backs up current deployment before replacing 2. Extracts new files to `/var/www/workroot/` 3. Installs production dependencies only 4. Uses `pm2 reload` for zero-downtime restart 5. Runs health check (12 attempts × 10s = 2 minutes max) 6. **Auto-rollback** if health check fails --- ### Option D: Fly.io Container-based deployment with edge distribution. **Required secrets:** | Secret | Value | |--------|-------| | `FLY_API_TOKEN` | From `flyctl auth token` | **Required variables:** | Variable | Value | |----------|-------| | `DEPLOY_TARGET` | `fly` | **Setup:** ```bash # Install flyctl curl -L https://fly.io/install.sh | sh # Authenticate flyctl auth login # Launch app (first time only) flyctl launch --name workroot-website # Set production secrets flyctl secrets set NODE_ENV=production PORT=10000 ``` **fly.toml** (create in project root if using Fly.io): ```toml app = "workroot-website" primary_region = "sin" # Singapore - closest to India [build] [build.args] NODE_VERSION = "20" [env] PORT = "10000" HOST = "0.0.0.0" NODE_ENV = "production" [http_service] internal_port = 10000 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 1 [http_service.concurrency] type = "connections" hard_limit = 25 soft_limit = 20 [[vm]] cpu_kind = "shared" cpus = 1 memory_mb = 512 ``` --- ## GitHub Repository Setup ### Required Secrets (Settings → Secrets → Actions) Configure secrets for your chosen deployment target: ``` # For ALL targets: # (none required at the base level) # For Railway: RAILWAY_TOKEN= # For Render: RENDER_DEPLOY_HOOK_URL=https://api.render.com/deploy/... # For VPS: VPS_HOST=123.456.789.0 VPS_USER=deploy VPS_SSH_PRIVATE_KEY=-----BEGIN OPENSSH PRIVATE KEY-----... VPS_HOST_KEY=123.456.789.0 ssh-rsa AAAA... # Optional notifications: SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... ``` ### Required Variables (Settings → Variables → Actions) ``` DEPLOY_TARGET=railway # or: render, vps, fly ``` ### Environment Protection Rules Configure via Settings → Environments: 1. Create environment named `production` 2. Enable "Required reviewers" for manual approval before deploy 3. Set allowed branches to `main` only --- ## Trigger Conditions | Event | Build | Tests | Deploy | |-------|-------|-------|--------| | Push to `main` | ✓ | ✓ | ✓ | | Manual dispatch | ✓ | ✓ (unless skip_tests=true) | ✓ | | Push to `develop` | via e2e-tests.yml | ✓ | ✗ | | Pull Request | via e2e-tests.yml | ✓ | ✗ | --- ## Environment Variables in Production All env vars must be configured in your hosting platform, not in the workflow. | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `NODE_ENV` | Yes | — | Must be `production` | | `HOST` | Yes | — | `0.0.0.0` | | `PORT` | Yes | — | `10000` | | `CONTACT_EMAIL` | Yes (legacy) | — | Legacy admin recipient for contact form; kept for backward compatibility with older code paths. Mirror of `MAIL_ADMIN_TO`. | | `MAIL_ADMIN_TO` | Yes | — | Address that receives "Tell us about your project" and contact form submissions. | | `SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.hostinger.com`). | | `SMTP_PORT` | Yes | `587` | SMTP server port. Use `587` for STARTTLS, `465` for implicit TLS. | | `SMTP_USER` | Yes | — | SMTP auth username (full email address). | | `SMTP_PASS` | Yes | — | SMTP auth password — MUST be stored as a platform secret, never plaintext. | | `SMTP_FROM` | Yes | — | `From:` address used by outbound mail (should match `SMTP_USER` or an allowed alias). | | `SMTP_FROM_NAME` | Yes | — | Human-readable display name shown alongside `SMTP_FROM`. | | `SMTP_SECURE` | Yes | `false` | `true` only for implicit-TLS ports (465). For port 587 keep `false` and rely on STARTTLS. | | `SMTP_STARTTLS` | Yes | `true` | Negotiate STARTTLS on the SMTP connection. | | `SMTP_REQUIRE_TLS` | Yes | `true` | Refuse to send if the server does not advertise STARTTLS — prevents silent plaintext fallback. | | `SMTP_AUTH` | Yes | `true` | Authenticate with `SMTP_USER` / `SMTP_PASS`. | | `SMTP_CONNECTION_TIMEOUT_MS` | No | `10000` | Socket connect timeout in milliseconds. | | `SMTP_GREETING_TIMEOUT_MS` | No | `10000` | Wait time for the server greeting (EHLO/HELO) in milliseconds. | | `SMTP_SOCKET_TIMEOUT_MS` | No | `10000` | Idle socket timeout in milliseconds (covers read/write while transferring the message). | | `SENTRY_DSN` | No | — | Sentry error tracking | | `LOG_LEVEL` | No | `info` | Logging verbosity | > Production SMTP is provisioned on Hostinger and configured in the **Default** environment via the platform's env-var store (`SMTP_PASS` flagged as secret). After changing any `SMTP_*` value you MUST restart the `frontend` service so the new variables are picked up by the Astro SSR process. --- ## Rollback Procedures ### Railway / Render / Fly.io Use the hosting platform dashboard to redeploy a previous commit or use the rollback button. ### VPS (PM2) The deployment script auto-rolls back if health checks fail. For manual rollback: ```bash # SSH to VPS ssh deploy@your-vps-ip # Check what backups exist ls -la /var/www/ | grep workroot-backup # Rollback to most recent backup BACKUP=$(ls -t /var/www/ | grep workroot-backup | head -1) echo "Rolling back to: $BACKUP" # Stop, restore, restart pm2 stop workroot-website cp -r /var/www/$BACKUP/. /var/www/workroot/ cd /var/www/workroot && npm ci --omit=dev pm2 start ecosystem.config.cjs --env production pm2 save # Verify curl http://localhost:10000/api/health.json ``` ### Emergency Deploy (Skip Tests) Use `workflow_dispatch` with `skip_tests: true` only in genuine emergencies. Document the reason in the workflow run description. --- ## Monitoring After Deploy ### Immediate (0–5 minutes) - [ ] Workflow shows all jobs green - [ ] Health endpoint: `https://workroot.in/api/health.json` returns `{"status":"ok"}` - [ ] Homepage loads correctly - [ ] Contact form accessible ### Short-term (15–60 minutes) - [ ] No spike in error logs - [ ] PM2 showing stable process count (VPS only) - [ ] Nightly E2E suite runs clean ### Verification Commands (VPS) ```bash # Check PM2 status pm2 status pm2 logs workroot-website --lines 50 # Check Nginx logs sudo tail -f /var/log/nginx/access.log sudo tail -f /var/log/nginx/error.log # Health check curl -s http://localhost:10000/api/health.json | python3 -m json.tool ``` --- ## Pipeline Files Reference | File | Purpose | |------|---------| | `.github/workflows/deploy.yml` | Main deployment pipeline | | `.github/workflows/e2e-tests.yml` | Test suite (smoke, critical, API, chaos, cross-browser, mobile, security) | | `.github/workflows/sitemap-ping.yml` | Pings Google/Bing after content changes | | `ecosystem.config.cjs` | PM2 cluster configuration | | `server.mjs` | Express wrapper for Astro SSR | | `.env.example` | Environment variable template | --- ## Adding a New Deployment Target 1. Add a new job block in `deploy.yml` after the existing deploy jobs 2. Add the condition: `vars.DEPLOY_TARGET == 'your-target'` 3. Add required secrets to this document 4. Update the `post-deploy-verify` job's `needs` array to include the new job 5. Test with a manual `workflow_dispatch` trigger --- *Last updated: 2026-03-21 | Created by: devops-engineer agent*