Scheduled Health Check #967
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Scheduled Health Check | |
| on: | |
| schedule: | |
| - cron: "0 * * * *" # Every hour | |
| workflow_dispatch: | |
| env: | |
| NODE_VERSION: "22" | |
| jobs: | |
| health-check-production: | |
| name: health-production | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check production login page | |
| run: | | |
| URL="${{ secrets.PRODUCTION_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] PRODUCTION_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/login" --max-time 15) | |
| if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "302" ] || [ "$HTTP_STATUS" = "307" ]; then | |
| echo "[OK] Production login page: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[FAIL] Production login page: HTTP ${HTTP_STATUS}" | |
| exit 1 | |
| fi | |
| - name: Check production health endpoint | |
| run: | | |
| URL="${{ secrets.PRODUCTION_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] PRODUCTION_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/api/health" --max-time 15) | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| echo "[OK] Production health endpoint: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[FAIL] Production health endpoint: HTTP ${HTTP_STATUS}" | |
| exit 1 | |
| fi | |
| - name: Check production PWA manifest | |
| run: | | |
| URL="${{ secrets.PRODUCTION_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] PRODUCTION_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/manifest.json" --max-time 15) | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| echo "[OK] Production PWA manifest: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[WARN] Production PWA manifest: HTTP ${HTTP_STATUS} (non-blocking)" | |
| fi | |
| - name: Check production auth guards | |
| run: | | |
| URL="${{ secrets.PRODUCTION_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] PRODUCTION_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/api/leads" --max-time 15) | |
| if [ "$HTTP_STATUS" = "401" ] || [ "$HTTP_STATUS" = "307" ] || [ "$HTTP_STATUS" = "302" ]; then | |
| echo "[OK] Production auth guards: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[WARN] Production auth guards: HTTP ${HTTP_STATUS} (expected 401 or 307)" | |
| fi | |
| health-check-staging: | |
| name: health-staging | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check staging login page | |
| run: | | |
| URL="${{ secrets.STAGING_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] STAGING_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/login" --max-time 15) | |
| if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "302" ] || [ "$HTTP_STATUS" = "307" ]; then | |
| echo "[OK] Staging login page: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[FAIL] Staging login page: HTTP ${HTTP_STATUS}" | |
| exit 1 | |
| fi | |
| - name: Check staging health endpoint | |
| run: | | |
| URL="${{ secrets.STAGING_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] STAGING_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/api/health" --max-time 15) | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| echo "[OK] Staging health endpoint: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[WARN] Staging health endpoint: HTTP ${HTTP_STATUS} (non-blocking)" | |
| fi | |
| cron-job-liveness: | |
| name: cron-liveness | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check daily digest liveness | |
| run: | | |
| URL="${{ secrets.PRODUCTION_APP_URL }}" | |
| if [ -z "$URL" ]; then | |
| echo "[SKIP] PRODUCTION_APP_URL not configured" | |
| exit 0 | |
| fi | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${URL}/api/cron/daily-digest" --max-time 15) | |
| if [ "$HTTP_STATUS" = "401" ] || [ "$HTTP_STATUS" = "200" ]; then | |
| echo "[OK] Cron endpoint reachable: HTTP ${HTTP_STATUS}" | |
| else | |
| echo "[WARN] Cron endpoint returned HTTP ${HTTP_STATUS} (non-blocking)" | |
| fi | |
| alert-on-failure: | |
| name: alert-on-failure | |
| runs-on: ubuntu-latest | |
| needs: [health-check-production, health-check-staging, cron-job-liveness] | |
| if: failure() | |
| steps: | |
| - name: Create incident issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const title = `[INCIDENT] Health Check Failure — ${new Date().toISOString().split('T')[0]}`; | |
| const body = `## Health Check Failure Detected | |
| **Time:** ${new Date().toISOString()} | |
| **Workflow:** ${{ github.workflow }} | |
| **Run:** ${{ github.run_id }} | |
| ### Failed Jobs | |
| - Production Health: ${{ needs.health-check-production.result }} | |
| - Staging Health: ${{ needs.health-check-staging.result }} | |
| - Cron Liveness: ${{ needs.cron-job-liveness.result }} | |
| ### Required Actions | |
| 1. Check Vercel deployment status | |
| 2. Verify Supabase connectivity | |
| 3. Review error logs | |
| **Auto-created by:** scheduled-health-check workflow`; | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: title, | |
| body: body, | |
| labels: ['incident', 'automated'] | |
| }); |