Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
518 lines
17 KiB
YAML
518 lines
17 KiB
YAML
name: Deploy to Production
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
environment:
|
|
description: 'Target environment'
|
|
required: true
|
|
default: 'production'
|
|
type: choice
|
|
options:
|
|
- production
|
|
- staging
|
|
skip_tests:
|
|
description: 'Skip E2E tests (emergency deploy only)'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
# Prevent concurrent deployments
|
|
concurrency:
|
|
group: deploy-${{ github.ref }}
|
|
cancel-in-progress: false # Never cancel a deployment in progress
|
|
|
|
env:
|
|
NODE_VERSION: '20'
|
|
PORT: 10000
|
|
|
|
jobs:
|
|
# ============================================================
|
|
# Job 1: Build & Verify
|
|
# ============================================================
|
|
build:
|
|
name: Build & Verify
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
|
|
outputs:
|
|
build-artifact: ${{ steps.artifact-name.outputs.name }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci --prefer-offline
|
|
|
|
- name: Type check
|
|
run: npx tsc --noEmit
|
|
continue-on-error: true # Warn but don't block on type errors
|
|
|
|
- name: Build production bundle
|
|
run: npm run build
|
|
env:
|
|
NODE_ENV: production
|
|
|
|
- name: Verify build output
|
|
run: |
|
|
echo "Verifying build output..."
|
|
test -d dist/ || (echo "ERROR: dist/ directory not found" && exit 1)
|
|
test -f dist/server/entry.mjs || (echo "ERROR: Server entry point missing" && exit 1)
|
|
test -d dist/client/ || (echo "ERROR: Client assets missing" && exit 1)
|
|
echo "Build verification passed"
|
|
echo "Build size:"
|
|
du -sh dist/
|
|
|
|
- name: Generate artifact name
|
|
id: artifact-name
|
|
run: echo "name=build-${{ github.sha }}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Upload build artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ steps.artifact-name.outputs.name }}
|
|
path: |
|
|
dist/
|
|
package.json
|
|
package-lock.json
|
|
server.mjs
|
|
ecosystem.config.cjs
|
|
.env.example
|
|
retention-days: 3
|
|
|
|
# ============================================================
|
|
# Job 2: Smoke Tests (Pre-Deploy Gate)
|
|
# ============================================================
|
|
pre-deploy-tests:
|
|
name: Pre-Deploy Tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: build
|
|
if: inputs.skip_tests != true
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci --prefer-offline
|
|
|
|
- name: Install Playwright (Chromium only for speed)
|
|
run: npx playwright install chromium --with-deps
|
|
|
|
- name: Download build artifact
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: ${{ needs.build.outputs.build-artifact }}
|
|
path: .
|
|
|
|
- name: Start server
|
|
run: npm run start &
|
|
env:
|
|
NODE_ENV: production
|
|
PORT: ${{ env.PORT }}
|
|
|
|
- name: Wait for server
|
|
run: npx wait-on http://localhost:${{ env.PORT }}/api/health.json --timeout 60000
|
|
|
|
- name: Run smoke tests
|
|
run: npx playwright test tests/e2e-smoke-suite.spec.ts --project=chromium --reporter=list
|
|
env:
|
|
BASE_URL: http://localhost:${{ env.PORT }}
|
|
|
|
- name: Run critical API tests
|
|
run: npx playwright test tests/api-integration.spec.ts --project=chromium --reporter=list
|
|
env:
|
|
BASE_URL: http://localhost:${{ env.PORT }}
|
|
|
|
- name: Upload test results
|
|
uses: actions/upload-artifact@v4
|
|
if: failure()
|
|
with:
|
|
name: pre-deploy-test-failures-${{ github.run_number }}
|
|
path: |
|
|
playwright-report/
|
|
test-results/
|
|
retention-days: 7
|
|
|
|
# ============================================================
|
|
# Job 3a: Deploy to Railway
|
|
# Activate by setting DEPLOY_TARGET=railway secret
|
|
# ============================================================
|
|
deploy-railway:
|
|
name: Deploy to Railway
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
needs: [build, pre-deploy-tests]
|
|
if: |
|
|
always() &&
|
|
needs.build.result == 'success' &&
|
|
(needs.pre-deploy-tests.result == 'success' || needs.pre-deploy-tests.result == 'skipped') &&
|
|
vars.DEPLOY_TARGET == 'railway'
|
|
environment:
|
|
name: ${{ inputs.environment || 'production' }}
|
|
url: https://workroot.in
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Install Railway CLI
|
|
run: npm install -g @railway/cli
|
|
|
|
- name: Deploy to Railway
|
|
run: railway up --service workroot-website
|
|
env:
|
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
|
|
|
- name: Verify Railway deployment
|
|
run: |
|
|
echo "Waiting for Railway deployment to propagate..."
|
|
sleep 30
|
|
curl --fail --silent --max-time 30 https://workroot.in/api/health.json \
|
|
|| (echo "Health check failed after Railway deploy" && exit 1)
|
|
echo "Railway deployment verified"
|
|
|
|
# ============================================================
|
|
# Job 3b: Deploy to Render
|
|
# Activate by setting DEPLOY_TARGET=render secret
|
|
# ============================================================
|
|
deploy-render:
|
|
name: Deploy to Render
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
needs: [build, pre-deploy-tests]
|
|
if: |
|
|
always() &&
|
|
needs.build.result == 'success' &&
|
|
(needs.pre-deploy-tests.result == 'success' || needs.pre-deploy-tests.result == 'skipped') &&
|
|
vars.DEPLOY_TARGET == 'render'
|
|
environment:
|
|
name: ${{ inputs.environment || 'production' }}
|
|
url: https://workroot.in
|
|
|
|
steps:
|
|
- name: Trigger Render deploy hook
|
|
run: |
|
|
curl --fail --silent --show-error \
|
|
-X POST "${{ secrets.RENDER_DEPLOY_HOOK_URL }}" \
|
|
|| (echo "Failed to trigger Render deploy hook" && exit 1)
|
|
echo "Render deployment triggered"
|
|
|
|
- name: Wait for Render deployment
|
|
run: |
|
|
echo "Waiting for Render to deploy (up to 5 minutes)..."
|
|
for i in $(seq 1 30); do
|
|
sleep 10
|
|
STATUS=$(curl --silent --max-time 10 https://workroot.in/api/health.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status',''))" 2>/dev/null || echo "error")
|
|
if [ "$STATUS" = "ok" ]; then
|
|
echo "Render deployment verified after ${i}0 seconds"
|
|
exit 0
|
|
fi
|
|
echo "Attempt $i/30: status=$STATUS, retrying..."
|
|
done
|
|
echo "ERROR: Render deployment health check timed out"
|
|
exit 1
|
|
|
|
# ============================================================
|
|
# Job 3c: Deploy to VPS (SSH + PM2)
|
|
# Activate by setting DEPLOY_TARGET=vps secret
|
|
# ============================================================
|
|
deploy-vps:
|
|
name: Deploy to VPS (PM2)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: [build, pre-deploy-tests]
|
|
if: |
|
|
always() &&
|
|
needs.build.result == 'success' &&
|
|
(needs.pre-deploy-tests.result == 'success' || needs.pre-deploy-tests.result == 'skipped') &&
|
|
vars.DEPLOY_TARGET == 'vps'
|
|
environment:
|
|
name: ${{ inputs.environment || 'production' }}
|
|
url: https://workroot.in
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Download build artifact
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: ${{ needs.build.outputs.build-artifact }}
|
|
path: build-output/
|
|
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh
|
|
echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
chmod 600 ~/.ssh/deploy_key
|
|
echo "${{ secrets.VPS_HOST_KEY }}" >> ~/.ssh/known_hosts
|
|
|
|
- name: Create deployment package
|
|
run: |
|
|
tar -czf deploy-package.tar.gz -C build-output .
|
|
echo "Deployment package created: $(du -sh deploy-package.tar.gz | cut -f1)"
|
|
|
|
- name: Upload package to VPS
|
|
run: |
|
|
scp -i ~/.ssh/deploy_key \
|
|
-o StrictHostKeyChecking=yes \
|
|
deploy-package.tar.gz \
|
|
${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/tmp/workroot-deploy-${{ github.sha }}.tar.gz
|
|
|
|
- name: Deploy on VPS
|
|
run: |
|
|
ssh -i ~/.ssh/deploy_key \
|
|
-o StrictHostKeyChecking=yes \
|
|
${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} \
|
|
'bash -s' << 'DEPLOY_SCRIPT'
|
|
set -e
|
|
|
|
DEPLOY_DIR="/var/www/workroot"
|
|
BACKUP_DIR="/var/www/workroot-backup-$(date +%Y%m%d-%H%M%S)"
|
|
DEPLOY_PKG="/tmp/workroot-deploy-${{ github.sha }}.tar.gz"
|
|
|
|
echo "=== Starting deployment ==="
|
|
echo "Deploy time: $(date)"
|
|
echo "Commit: ${{ github.sha }}"
|
|
|
|
# Step 1: Backup current deployment
|
|
if [ -d "$DEPLOY_DIR" ]; then
|
|
echo "Backing up current deployment to $BACKUP_DIR..."
|
|
cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
|
|
fi
|
|
|
|
# Step 2: Extract new deployment
|
|
echo "Extracting deployment package..."
|
|
mkdir -p "$DEPLOY_DIR"
|
|
tar -xzf "$DEPLOY_PKG" -C "$DEPLOY_DIR"
|
|
|
|
# Step 3: Install production dependencies
|
|
echo "Installing production dependencies..."
|
|
cd "$DEPLOY_DIR"
|
|
npm ci --omit=dev --prefer-offline
|
|
|
|
# Step 4: Ensure logs directory exists
|
|
mkdir -p logs
|
|
|
|
# Step 5: Reload PM2 (zero-downtime restart)
|
|
echo "Reloading PM2..."
|
|
if pm2 list | grep -q "workroot-website"; then
|
|
pm2 reload ecosystem.config.cjs --update-env
|
|
else
|
|
pm2 start ecosystem.config.cjs --env production
|
|
pm2 save
|
|
fi
|
|
|
|
# Step 6: Health check
|
|
echo "Running health check..."
|
|
sleep 5
|
|
for i in $(seq 1 12); do
|
|
STATUS=$(curl --silent --max-time 5 http://localhost:10000/api/health.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status',''))" 2>/dev/null || echo "error")
|
|
if [ "$STATUS" = "ok" ]; then
|
|
echo "Health check passed after ${i}0 seconds"
|
|
break
|
|
fi
|
|
if [ $i -eq 12 ]; then
|
|
echo "ERROR: Health check failed after 2 minutes"
|
|
echo "=== Rolling back ==="
|
|
cp -r "$BACKUP_DIR/." "$DEPLOY_DIR/"
|
|
cd "$DEPLOY_DIR"
|
|
npm ci --omit=dev --prefer-offline
|
|
pm2 reload ecosystem.config.cjs --update-env
|
|
exit 1
|
|
fi
|
|
echo "Attempt $i/12: status=$STATUS, retrying..."
|
|
sleep 10
|
|
done
|
|
|
|
# Step 7: Cleanup
|
|
rm -f "$DEPLOY_PKG"
|
|
rm -rf "$BACKUP_DIR"
|
|
|
|
echo "=== Deployment complete ==="
|
|
pm2 status
|
|
DEPLOY_SCRIPT
|
|
|
|
# ============================================================
|
|
# Job 3d: Deploy to Fly.io
|
|
# Activate by setting DEPLOY_TARGET=fly secret
|
|
# ============================================================
|
|
deploy-fly:
|
|
name: Deploy to Fly.io
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: [build, pre-deploy-tests]
|
|
if: |
|
|
always() &&
|
|
needs.build.result == 'success' &&
|
|
(needs.pre-deploy-tests.result == 'success' || needs.pre-deploy-tests.result == 'skipped') &&
|
|
vars.DEPLOY_TARGET == 'fly'
|
|
environment:
|
|
name: ${{ inputs.environment || 'production' }}
|
|
url: https://workroot.in
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Fly CLI
|
|
uses: superfly/flyctl-actions/setup-flyctl@master
|
|
|
|
- name: Deploy to Fly.io
|
|
run: flyctl deploy --remote-only --wait-timeout 300
|
|
env:
|
|
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
|
|
- name: Verify Fly.io deployment
|
|
run: |
|
|
sleep 15
|
|
flyctl status
|
|
curl --fail --silent --max-time 30 https://workroot.in/api/health.json \
|
|
|| (echo "Health check failed after Fly.io deploy" && exit 1)
|
|
echo "Fly.io deployment verified"
|
|
env:
|
|
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
|
|
# ============================================================
|
|
# Job 4: Post-Deploy Verification
|
|
# ============================================================
|
|
post-deploy-verify:
|
|
name: Post-Deploy Verification
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: [deploy-railway, deploy-render, deploy-vps, deploy-fly]
|
|
if: |
|
|
always() &&
|
|
(
|
|
needs.deploy-railway.result == 'success' ||
|
|
needs.deploy-render.result == 'success' ||
|
|
needs.deploy-vps.result == 'success' ||
|
|
needs.deploy-fly.result == 'success'
|
|
)
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci --prefer-offline
|
|
|
|
- name: Install Playwright
|
|
run: npx playwright install chromium --with-deps
|
|
|
|
- name: Run production smoke tests
|
|
run: |
|
|
npx playwright test tests/e2e-smoke-suite.spec.ts \
|
|
--project=chromium \
|
|
--reporter=list
|
|
env:
|
|
BASE_URL: https://workroot.in
|
|
|
|
- name: Verify critical endpoints
|
|
run: |
|
|
echo "Checking production endpoints..."
|
|
|
|
# Health check
|
|
HEALTH=$(curl --silent --max-time 10 https://workroot.in/api/health.json)
|
|
echo "Health: $HEALTH"
|
|
|
|
# Homepage
|
|
HTTP_STATUS=$(curl --silent --max-time 10 -o /dev/null -w "%{http_code}" https://workroot.in/)
|
|
echo "Homepage: HTTP $HTTP_STATUS"
|
|
[ "$HTTP_STATUS" = "200" ] || (echo "Homepage returned $HTTP_STATUS" && exit 1)
|
|
|
|
# Contact page
|
|
HTTP_STATUS=$(curl --silent --max-time 10 -o /dev/null -w "%{http_code}" https://workroot.in/contact)
|
|
echo "Contact page: HTTP $HTTP_STATUS"
|
|
[ "$HTTP_STATUS" = "200" ] || (echo "Contact page returned $HTTP_STATUS" && exit 1)
|
|
|
|
# Sitemap
|
|
HTTP_STATUS=$(curl --silent --max-time 10 -o /dev/null -w "%{http_code}" https://workroot.in/sitemap.xml)
|
|
echo "Sitemap: HTTP $HTTP_STATUS"
|
|
[ "$HTTP_STATUS" = "200" ] || (echo "Sitemap returned $HTTP_STATUS" && exit 1)
|
|
|
|
echo "All critical endpoints verified"
|
|
|
|
- name: Upload post-deploy results
|
|
uses: actions/upload-artifact@v4
|
|
if: always()
|
|
with:
|
|
name: post-deploy-results-${{ github.run_number }}
|
|
path: |
|
|
playwright-report/
|
|
test-results/
|
|
retention-days: 14
|
|
|
|
# ============================================================
|
|
# Job 5: Notify on Failure
|
|
# ============================================================
|
|
notify-failure:
|
|
name: Notify on Failure
|
|
runs-on: ubuntu-latest
|
|
needs: [build, pre-deploy-tests, post-deploy-verify]
|
|
if: |
|
|
always() &&
|
|
(
|
|
needs.build.result == 'failure' ||
|
|
needs.pre-deploy-tests.result == 'failure' ||
|
|
needs.post-deploy-verify.result == 'failure'
|
|
)
|
|
|
|
steps:
|
|
- name: Create failure summary
|
|
run: |
|
|
echo "## Deployment Failed" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY
|
|
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Pre-Deploy Tests | ${{ needs.pre-deploy-tests.result }} |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Post-Deploy Verify | ${{ needs.post-deploy-verify.result }} |" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Author:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." >> $GITHUB_STEP_SUMMARY
|
|
|
|
# Uncomment and configure one notification method:
|
|
|
|
# Slack notification
|
|
# - name: Notify Slack
|
|
# uses: slackapi/slack-github-action@v1.27.0
|
|
# with:
|
|
# payload: |
|
|
# {
|
|
# "text": "Deployment failed on ${{ github.ref_name }} by ${{ github.actor }}",
|
|
# "attachments": [{
|
|
# "color": "danger",
|
|
# "text": "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
# }]
|
|
# }
|
|
# env:
|
|
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
# SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|