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