First Init
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
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
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
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
|
||||
@@ -0,0 +1,454 @@
|
||||
name: E2E Test Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Nightly regression at 2:00 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
suite:
|
||||
description: 'Test suite to run'
|
||||
required: false
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- smoke
|
||||
- critical
|
||||
- api
|
||||
- chaos
|
||||
|
||||
# Cancel in-progress runs for the same branch
|
||||
concurrency:
|
||||
group: e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
PORT: 10000
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# Job 1: Smoke Tests (P0) - Every push, fast
|
||||
# ============================================================
|
||||
smoke:
|
||||
name: Smoke Tests (P0)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browsers (Chromium only for smoke)
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run smoke tests (Chromium only)
|
||||
run: npx playwright test tests/e2e-smoke-suite.spec.ts --project=chromium --reporter=list
|
||||
|
||||
- name: Upload smoke test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: smoke-test-results-${{ github.run_number }}
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 7
|
||||
|
||||
# ============================================================
|
||||
# Job 2: Critical User Journeys (P0) - Every push
|
||||
# ============================================================
|
||||
critical-paths:
|
||||
name: Critical User Journeys
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
needs: smoke
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium firefox --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run critical path tests
|
||||
run: npx playwright test tests/e2e-critical-paths.spec.ts --project=chromium --project=firefox
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: critical-paths-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# ============================================================
|
||||
# Job 3: API Integration Tests - Every push
|
||||
# ============================================================
|
||||
api-tests:
|
||||
name: API Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: smoke
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright (for API testing)
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run API integration tests
|
||||
run: npx playwright test tests/api-integration.spec.ts --project=chromium
|
||||
|
||||
- name: Upload API test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: api-test-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# ============================================================
|
||||
# Job 4: Form Tests - PR and nightly
|
||||
# ============================================================
|
||||
form-tests:
|
||||
name: Form Interaction Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run contact form tests
|
||||
run: npx playwright test tests/contact-form.spec.ts tests/e2e-form-interactions.spec.ts --project=chromium
|
||||
|
||||
- name: Run newsletter subscription tests
|
||||
run: npx playwright test tests/newsletter-subscription.spec.ts --project=chromium
|
||||
|
||||
- name: Upload form test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: form-test-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# ============================================================
|
||||
# Job 5: Chaos / Destructive Tests - Nightly and manual
|
||||
# ============================================================
|
||||
chaos-tests:
|
||||
name: Destructive & Chaos Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run destructive/chaos tests
|
||||
run: npx playwright test tests/destructive-chaos.spec.ts --project=chromium
|
||||
continue-on-error: true # Chaos tests may find real bugs
|
||||
|
||||
- name: Upload chaos test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: chaos-test-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# ============================================================
|
||||
# Job 6: Cross-Browser Regression - Nightly and manual
|
||||
# ============================================================
|
||||
cross-browser:
|
||||
name: Cross-Browser Regression
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chromium, firefox, webkit]
|
||||
fail-fast: false # Continue testing other browsers even if one fails
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browser (${{ matrix.browser }})
|
||||
run: npx playwright install ${{ matrix.browser }} --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run regression suite (${{ matrix.browser }})
|
||||
run: |
|
||||
npx playwright test \
|
||||
tests/e2e-smoke-suite.spec.ts \
|
||||
tests/navigation.spec.ts \
|
||||
tests/blog.spec.ts \
|
||||
tests/portfolio.spec.ts \
|
||||
--project=${{ matrix.browser }}
|
||||
|
||||
- name: Upload cross-browser results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: cross-browser-${{ matrix.browser }}-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# ============================================================
|
||||
# Job 7: Mobile Testing - Nightly
|
||||
# ============================================================
|
||||
mobile-tests:
|
||||
name: Mobile Device Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium webkit --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run mobile tests
|
||||
run: npx playwright test --project='Mobile Chrome' --project='Mobile Safari'
|
||||
|
||||
- name: Upload mobile test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: mobile-test-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# ============================================================
|
||||
# Job 8: Security Header Tests - PR and nightly
|
||||
# ============================================================
|
||||
security-tests:
|
||||
name: Security Header Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'schedule'
|
||||
|
||||
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
|
||||
|
||||
- name: Install Playwright
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Start server
|
||||
run: npm run start &
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:${{ env.PORT }} --timeout 60000
|
||||
|
||||
- name: Run security header tests
|
||||
run: npx playwright test tests/security-headers.test.ts --project=chromium
|
||||
|
||||
- name: Upload security test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: security-test-results-${{ github.run_number }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# ============================================================
|
||||
# Job 9: Test Report Summary
|
||||
# ============================================================
|
||||
report:
|
||||
name: Test Report Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [smoke, critical-paths, api-tests]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Download all test artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-results/
|
||||
|
||||
- name: Generate test summary
|
||||
run: |
|
||||
echo "## E2E Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Suite | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Smoke Tests | ${{ needs.smoke.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Critical Paths | ${{ needs.critical-paths.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| API Integration | ${{ needs.api-tests.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Ping Search Engines
|
||||
|
||||
# Automatically notify search engines when content changes
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'src/content/blog/**'
|
||||
- 'src/content/portfolio/**'
|
||||
- 'src/pages/**'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
ping-sitemap:
|
||||
name: Notify Search Engines
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Ping Google
|
||||
run: |
|
||||
echo "Pinging Google with sitemap..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://www.google.com/ping?sitemap=https://workroot.in/sitemap.xml")
|
||||
echo "Google response: $HTTP_CODE"
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "✅ Google pinged successfully"
|
||||
else
|
||||
echo "⚠️ Google ping returned HTTP $HTTP_CODE"
|
||||
fi
|
||||
|
||||
- name: Ping Bing
|
||||
run: |
|
||||
echo "Pinging Bing with sitemap..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://www.bing.com/ping?sitemap=https://workroot.in/sitemap.xml")
|
||||
echo "Bing response: $HTTP_CODE"
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "✅ Bing pinged successfully"
|
||||
else
|
||||
echo "⚠️ Bing ping returned HTTP $HTTP_CODE"
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✨ Sitemap ping workflow complete"
|
||||
echo "Search engines notified of content updates"
|
||||
echo "Check Search Console and Bing Webmaster Tools for indexing status"
|
||||
@@ -0,0 +1,294 @@
|
||||
name: Uptime Monitor
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 5 minutes
|
||||
- cron: '*/5 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
verbose:
|
||||
description: 'Verbose output'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
# Prevent concurrent monitoring runs
|
||||
concurrency:
|
||||
group: uptime-monitor
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
BASE_URL: https://workroot.in
|
||||
RESPONSE_TIME_THRESHOLD_MS: 3000
|
||||
# SSL check: days before expiry to alert
|
||||
SSL_ALERT_DAYS: 14
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# Job 1: Health & Response Time Check
|
||||
# ============================================================
|
||||
health-check:
|
||||
name: Health & Response Time
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
outputs:
|
||||
health-status: ${{ steps.health.outputs.status }}
|
||||
response-time: ${{ steps.health.outputs.response_time }}
|
||||
is-slow: ${{ steps.health.outputs.is_slow }}
|
||||
|
||||
steps:
|
||||
- name: Check health endpoint
|
||||
id: health
|
||||
run: |
|
||||
START=$(date +%s%3N)
|
||||
HTTP_RESPONSE=$(curl \
|
||||
--silent \
|
||||
--max-time 10 \
|
||||
--write-out "\n%{http_code}" \
|
||||
"${{ env.BASE_URL }}/api/health.json" \
|
||||
)
|
||||
END=$(date +%s%3N)
|
||||
RESPONSE_TIME=$((END - START))
|
||||
|
||||
HTTP_BODY=$(echo "$HTTP_RESPONSE" | head -n -1)
|
||||
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n 1)
|
||||
|
||||
echo "HTTP status: $HTTP_CODE"
|
||||
echo "Response time: ${RESPONSE_TIME}ms"
|
||||
echo "Body: $HTTP_BODY"
|
||||
|
||||
# Outputs
|
||||
echo "response_time=${RESPONSE_TIME}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
STATUS=$(echo "$HTTP_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))" 2>/dev/null || echo "parse-error")
|
||||
echo "status=${STATUS}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=down-${HTTP_CODE}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Slow response check
|
||||
if [ "$RESPONSE_TIME" -gt "${{ env.RESPONSE_TIME_THRESHOLD_MS }}" ]; then
|
||||
echo "is_slow=true" >> $GITHUB_OUTPUT
|
||||
echo "WARNING: Response time ${RESPONSE_TIME}ms exceeds threshold ${{ env.RESPONSE_TIME_THRESHOLD_MS }}ms"
|
||||
else
|
||||
echo "is_slow=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check critical pages
|
||||
id: pages
|
||||
run: |
|
||||
FAILED_PAGES=""
|
||||
PAGES="/ /services /portfolio /contact /about"
|
||||
|
||||
for PAGE in $PAGES; do
|
||||
START=$(date +%s%3N)
|
||||
HTTP_CODE=$(curl --silent --max-time 10 -o /dev/null -w "%{http_code}" "${{ env.BASE_URL }}${PAGE}")
|
||||
END=$(date +%s%3N)
|
||||
RT=$((END - START))
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
FAILED_PAGES="${FAILED_PAGES} ${PAGE}(${HTTP_CODE})"
|
||||
echo "FAIL: ${PAGE} → HTTP $HTTP_CODE"
|
||||
else
|
||||
echo "OK: ${PAGE} → HTTP $HTTP_CODE (${RT}ms)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$FAILED_PAGES" ]; then
|
||||
echo "Failed pages:${FAILED_PAGES}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check sitemap and robots.txt
|
||||
run: |
|
||||
for RESOURCE in "/sitemap.xml" "/robots.txt"; do
|
||||
HTTP_CODE=$(curl --silent --max-time 10 -o /dev/null -w "%{http_code}" "${{ env.BASE_URL }}${RESOURCE}")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "FAIL: ${RESOURCE} returned HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: ${RESOURCE} → HTTP $HTTP_CODE"
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
# Job 2: SSL Certificate Check
|
||||
# ============================================================
|
||||
ssl-check:
|
||||
name: SSL Certificate
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
outputs:
|
||||
days-until-expiry: ${{ steps.ssl.outputs.days_until_expiry }}
|
||||
ssl-valid: ${{ steps.ssl.outputs.ssl_valid }}
|
||||
|
||||
steps:
|
||||
- name: Check SSL certificate
|
||||
id: ssl
|
||||
run: |
|
||||
DOMAIN="workroot.in"
|
||||
|
||||
# Get certificate expiry date
|
||||
EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect "${DOMAIN}:443" 2>/dev/null \
|
||||
| openssl x509 -noout -enddate 2>/dev/null \
|
||||
| cut -d= -f2)
|
||||
|
||||
if [ -z "$EXPIRY" ]; then
|
||||
echo "ssl_valid=false" >> $GITHUB_OUTPUT
|
||||
echo "days_until_expiry=0" >> $GITHUB_OUTPUT
|
||||
echo "ERROR: Could not retrieve SSL certificate"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$EXPIRY" +%s 2>/dev/null)
|
||||
NOW_EPOCH=$(date +%s)
|
||||
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
|
||||
|
||||
echo "ssl_valid=true" >> $GITHUB_OUTPUT
|
||||
echo "days_until_expiry=${DAYS_LEFT}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "SSL certificate expires: $EXPIRY"
|
||||
echo "Days until expiry: $DAYS_LEFT"
|
||||
|
||||
if [ "$DAYS_LEFT" -le "${{ env.SSL_ALERT_DAYS }}" ]; then
|
||||
echo "WARNING: SSL certificate expires in $DAYS_LEFT days!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SSL OK: $DAYS_LEFT days remaining"
|
||||
|
||||
# ============================================================
|
||||
# Job 3: Alert on Downtime or Issues
|
||||
# ============================================================
|
||||
alert:
|
||||
name: Send Alerts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: [health-check, ssl-check]
|
||||
if: |
|
||||
always() &&
|
||||
(
|
||||
needs.health-check.result == 'failure' ||
|
||||
needs.ssl-check.result == 'failure' ||
|
||||
needs.health-check.outputs.health-status != 'ok' ||
|
||||
needs.health-check.outputs.is-slow == 'true'
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Build alert summary
|
||||
id: alert-content
|
||||
run: |
|
||||
HEALTH_STATUS="${{ needs.health-check.outputs.health-status }}"
|
||||
RESPONSE_TIME="${{ needs.health-check.outputs.response-time }}"
|
||||
IS_SLOW="${{ needs.health-check.outputs.is-slow }}"
|
||||
SSL_DAYS="${{ needs.ssl-check.outputs.days-until-expiry }}"
|
||||
SSL_VALID="${{ needs.ssl-check.outputs.ssl-valid }}"
|
||||
|
||||
# Determine alert type
|
||||
if [ "${{ needs.health-check.result }}" = "failure" ]; then
|
||||
ALERT_TYPE="DOWNTIME"
|
||||
SEVERITY="critical"
|
||||
elif [ "$IS_SLOW" = "true" ]; then
|
||||
ALERT_TYPE="SLOW_RESPONSE"
|
||||
SEVERITY="warning"
|
||||
elif [ "${{ needs.ssl-check.result }}" = "failure" ]; then
|
||||
ALERT_TYPE="SSL_EXPIRY"
|
||||
SEVERITY="warning"
|
||||
else
|
||||
ALERT_TYPE="DEGRADED"
|
||||
SEVERITY="warning"
|
||||
fi
|
||||
|
||||
echo "alert_type=${ALERT_TYPE}" >> $GITHUB_OUTPUT
|
||||
echo "severity=${SEVERITY}" >> $GITHUB_OUTPUT
|
||||
|
||||
# GitHub Step Summary
|
||||
echo "## Alert: ${ALERT_TYPE} [${SEVERITY^^}]" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Health Status | ${HEALTH_STATUS} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Response Time | ${RESPONSE_TIME}ms |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Response Slow | ${IS_SLOW} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SSL Valid | ${SSL_VALID} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SSL Days Left | ${SSL_DAYS} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Time:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Site:** ${{ env.BASE_URL }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[View Health Endpoint](${{ env.BASE_URL }}/api/health.json)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Slack notification (enable by adding SLACK_WEBHOOK_URL secret) ──
|
||||
# - name: Notify Slack
|
||||
# if: secrets.SLACK_WEBHOOK_URL != ''
|
||||
# run: |
|
||||
# ALERT_TYPE="${{ steps.alert-content.outputs.alert_type }}"
|
||||
# SEVERITY="${{ steps.alert-content.outputs.severity }}"
|
||||
# COLOR=$([ "$SEVERITY" = "critical" ] && echo "danger" || echo "warning")
|
||||
# EMOJI=$([ "$SEVERITY" = "critical" ] && echo ":red_circle:" || echo ":warning:")
|
||||
#
|
||||
# curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
|
||||
# -H 'Content-type: application/json' \
|
||||
# --data "{
|
||||
# \"text\": \"${EMOJI} WorkRoot Alert: ${ALERT_TYPE}\",
|
||||
# \"attachments\": [{
|
||||
# \"color\": \"${COLOR}\",
|
||||
# \"fields\": [
|
||||
# {\"title\": \"Alert\", \"value\": \"${ALERT_TYPE}\", \"short\": true},
|
||||
# {\"title\": \"Site\", \"value\": \"${{ env.BASE_URL }}\", \"short\": true},
|
||||
# {\"title\": \"Response Time\", \"value\": \"${{ needs.health-check.outputs.response-time }}ms\", \"short\": true},
|
||||
# {\"title\": \"SSL Days Left\", \"value\": \"${{ needs.ssl-check.outputs.days-until-expiry }}\", \"short\": true}
|
||||
# ],
|
||||
# \"footer\": \"WorkRoot Monitor | $(date -u '+%Y-%m-%d %H:%M UTC')\"
|
||||
# }]
|
||||
# }"
|
||||
|
||||
# ── Email/PagerDuty notification (via webhook) ──
|
||||
# - name: Notify via webhook
|
||||
# if: secrets.ALERT_WEBHOOK_URL != ''
|
||||
# run: |
|
||||
# curl -s -X POST "${{ secrets.ALERT_WEBHOOK_URL }}" \
|
||||
# -H 'Content-type: application/json' \
|
||||
# --data '{
|
||||
# "event": "${{ steps.alert-content.outputs.alert_type }}",
|
||||
# "severity": "${{ steps.alert-content.outputs.severity }}",
|
||||
# "site": "${{ env.BASE_URL }}",
|
||||
# "health_status": "${{ needs.health-check.outputs.health-status }}",
|
||||
# "response_time_ms": "${{ needs.health-check.outputs.response-time }}",
|
||||
# "ssl_days_left": "${{ needs.ssl-check.outputs.days-until-expiry }}",
|
||||
# "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
|
||||
# }'
|
||||
|
||||
- name: Fail workflow to make alert visible
|
||||
run: |
|
||||
echo "Alert triggered: ${{ steps.alert-content.outputs.alert_type }}"
|
||||
echo "Severity: ${{ steps.alert-content.outputs.severity }}"
|
||||
exit 1
|
||||
|
||||
# ============================================================
|
||||
# Job 4: Record Success (for uptime tracking)
|
||||
# ============================================================
|
||||
record-success:
|
||||
name: Record Uptime Success
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
needs: [health-check, ssl-check]
|
||||
if: |
|
||||
always() &&
|
||||
needs.health-check.result == 'success' &&
|
||||
needs.ssl-check.result == 'success'
|
||||
|
||||
steps:
|
||||
- name: Log success
|
||||
run: |
|
||||
echo "## Uptime Check Passed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Check | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Health | ${{ needs.health-check.outputs.health-status }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Response Time | ${{ needs.health-check.outputs.response-time }}ms |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SSL Days Left | ${{ needs.ssl-check.outputs.days-until-expiry }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Checked at:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
|
||||
Reference in New Issue
Block a user