Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m59s
Expand the CI/CD env-var table to list all SMTP_*, MAIL_ADMIN_TO, SMTP_FROM, SMTP_FROM_NAME and STARTTLS/timeout knobs that the contact and "Tell us about your project" forms now rely on. Adds a note that the production secrets live in the Default environment's platform env-var store and that the frontend service must be restarted after any change.
426 lines
12 KiB
Markdown
426 lines
12 KiB
Markdown
# 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=<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*
|