Uptime Monitor #152
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: Uptime Monitor | |
| on: | |
| schedule: | |
| - cron: '*/15 * * * *' # Every 15 minutes | |
| workflow_dispatch: | |
| inputs: | |
| check_all_languages: | |
| description: 'Check all 14 language versions' | |
| type: boolean | |
| default: true | |
| required: false | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| uptime-check: | |
| name: Site Availability Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 | |
| with: | |
| egress-policy: audit | |
| - name: Check homepage availability | |
| id: homepage | |
| run: | | |
| echo "🔍 Checking homepage availability..." | |
| # Single curl request to capture both HTTP code and response time | |
| CURL_OUTPUT=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" -L --connect-timeout 10 --max-time 30 https://riksdagsmonitor.com) | |
| HTTP_CODE=$(echo "$CURL_OUTPUT" | awk '{print $1}') | |
| RESPONSE_TIME=$(echo "$CURL_OUTPUT" | awk '{print $2}') | |
| echo "http_code=$HTTP_CODE" >> $GITHUB_OUTPUT | |
| echo "response_time=$RESPONSE_TIME" >> $GITHUB_OUTPUT | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "✅ Homepage is UP (HTTP $HTTP_CODE, ${RESPONSE_TIME}s)" | |
| else | |
| echo "❌ Homepage returned HTTP $HTTP_CODE" | |
| exit 1 | |
| fi | |
| - name: Check all 14 language versions | |
| id: languages | |
| if: github.event.inputs.check_all_languages != 'false' | |
| continue-on-error: true | |
| run: | | |
| echo "🌐 Checking all 14 language versions..." | |
| LANGUAGES=(en sv da no fi de fr es nl ar he ja ko zh) | |
| FAILED_LANGUAGES="" | |
| SUCCESS_COUNT=0 | |
| FAIL_COUNT=0 | |
| for lang in "${LANGUAGES[@]}"; do | |
| if [ "$lang" = "en" ]; then | |
| URL="https://riksdagsmonitor.com/index.html" | |
| else | |
| URL="https://riksdagsmonitor.com/index_$lang.html" | |
| fi | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -L --connect-timeout 10 --max-time 30 "$URL") | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "✅ $lang: HTTP $HTTP_CODE" | |
| SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) | |
| else | |
| echo "❌ $lang: HTTP $HTTP_CODE" | |
| FAILED_LANGUAGES="$FAILED_LANGUAGES $lang" | |
| FAIL_COUNT=$((FAIL_COUNT + 1)) | |
| fi | |
| done | |
| echo "success_count=$SUCCESS_COUNT" >> $GITHUB_OUTPUT | |
| echo "fail_count=$FAIL_COUNT" >> $GITHUB_OUTPUT | |
| echo "failed_languages=$FAILED_LANGUAGES" >> $GITHUB_OUTPUT | |
| if [ $FAIL_COUNT -gt 0 ]; then | |
| echo "⚠️ $FAIL_COUNT language version(s) failed: $FAILED_LANGUAGES" | |
| echo "Treating as degraded service (not critical failure)" | |
| else | |
| echo "✅ All 14 language versions are UP" | |
| fi | |
| - name: Check critical assets | |
| id: assets | |
| run: | | |
| echo "🎨 Checking critical assets..." | |
| ASSETS=( | |
| "https://riksdagsmonitor.com/styles.css" | |
| "https://riksdagsmonitor.com/manifest.json" | |
| "https://riksdagsmonitor.com/sitemap.xml" | |
| "https://riksdagsmonitor.com/robots.txt" | |
| ) | |
| FAILED_ASSETS="" | |
| SUCCESS_COUNT=0 | |
| FAIL_COUNT=0 | |
| for asset in "${ASSETS[@]}"; do | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -L --connect-timeout 10 --max-time 30 "$asset") | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "✅ $(basename $asset): HTTP $HTTP_CODE" | |
| SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) | |
| else | |
| echo "❌ $(basename $asset): HTTP $HTTP_CODE" | |
| FAILED_ASSETS="$FAILED_ASSETS $(basename $asset)" | |
| FAIL_COUNT=$((FAIL_COUNT + 1)) | |
| fi | |
| done | |
| echo "success_count=$SUCCESS_COUNT" >> $GITHUB_OUTPUT | |
| echo "fail_count=$FAIL_COUNT" >> $GITHUB_OUTPUT | |
| echo "failed_assets=$FAILED_ASSETS" >> $GITHUB_OUTPUT | |
| if [ $FAIL_COUNT -gt 0 ]; then | |
| echo "⚠️ $FAIL_COUNT asset(s) failed: $FAILED_ASSETS" | |
| # Don't fail workflow for asset issues | |
| else | |
| echo "✅ All critical assets are accessible" | |
| fi | |
| - name: Check HTTPS and security headers | |
| id: security | |
| run: | | |
| echo "🔒 Checking HTTPS and security headers..." | |
| # Check HTTPS redirect | |
| HTTP_REDIRECT=$(curl -s -o /dev/null -w "%{redirect_url}" --connect-timeout 10 --max-time 30 http://riksdagsmonitor.com) | |
| if [[ "$HTTP_REDIRECT" == https://* ]]; then | |
| echo "✅ HTTPS redirect: Working" | |
| else | |
| echo "⚠️ HTTPS redirect: Not detected" | |
| fi | |
| # Check security headers | |
| HEADERS=$(curl -sI --connect-timeout 10 --max-time 30 https://riksdagsmonitor.com) | |
| # Check for key security headers | |
| if echo "$HEADERS" | grep -qi "strict-transport-security"; then | |
| echo "✅ HSTS header: Present" | |
| else | |
| echo "⚠️ HSTS header: Missing" | |
| fi | |
| if echo "$HEADERS" | grep -qi "x-frame-options"; then | |
| echo "✅ X-Frame-Options: Present" | |
| else | |
| echo "⚠️ X-Frame-Options: Missing" | |
| fi | |
| if echo "$HEADERS" | grep -qi "x-content-type-options"; then | |
| echo "✅ X-Content-Type-Options: Present" | |
| else | |
| echo "⚠️ X-Content-Type-Options: Missing" | |
| fi | |
| if echo "$HEADERS" | grep -qi "content-security-policy"; then | |
| echo "✅ CSP header: Present" | |
| else | |
| echo "⚠️ CSP header: Missing" | |
| fi | |
| - name: Generate uptime summary | |
| if: always() | |
| run: | | |
| echo "## 🚦 Uptime Monitor Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Homepage status | |
| echo "### Homepage Status" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **HTTP Status**: ${{ steps.homepage.outputs.http_code }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Response Time**: ${{ steps.homepage.outputs.response_time }}s" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Language versions status | |
| if [ -n "${{ steps.languages.outputs.success_count }}" ]; then | |
| echo "### Language Versions" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Total**: 14 languages" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Success**: ✅ ${{ steps.languages.outputs.success_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Failed**: ❌ ${{ steps.languages.outputs.fail_count }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.languages.outputs.fail_count }}" != "0" ]; then | |
| echo "- **Failed Languages**: ${{ steps.languages.outputs.failed_languages }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Assets status | |
| if [ -n "${{ steps.assets.outputs.success_count }}" ]; then | |
| echo "### Critical Assets" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Success**: ✅ ${{ steps.assets.outputs.success_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Failed**: ❌ ${{ steps.assets.outputs.fail_count }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.assets.outputs.fail_count }}" != "0" ]; then | |
| echo "- **Failed Assets**: ${{ steps.assets.outputs.failed_assets }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Overall status | |
| echo "### 📊 Overall Status" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Check homepage, languages, and assets for overall status | |
| LANGUAGES_OK=false | |
| if [ "${{ steps.languages.outcome }}" = "success" ] || [ "${{ steps.languages.outcome }}" = "skipped" ]; then | |
| LANGUAGES_OK=true | |
| fi | |
| ASSETS_OK=true | |
| if [ -n "${{ steps.assets.outputs.fail_count }}" ] && [ "${{ steps.assets.outputs.fail_count }}" != "0" ]; then | |
| ASSETS_OK=false | |
| fi | |
| if [ "${{ steps.homepage.outcome }}" = "success" ] && [ "$LANGUAGES_OK" = "true" ] && [ "$ASSETS_OK" = "true" ]; then | |
| echo "🟢 **ALL SYSTEMS OPERATIONAL**" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ steps.homepage.outcome }}" = "success" ]; then | |
| echo "🟡 **DEGRADED SERVICE** - Homepage up, but some issues detected" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "🔴 **SERVICE DOWN** - Homepage unreachable" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "*Check timestamp: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*" >> $GITHUB_STEP_SUMMARY | |
| - name: Create incident issue | |
| if: failure() && steps.homepage.outcome == 'failure' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const httpCode = '${{ steps.homepage.outputs.http_code }}'; | |
| const timestamp = new Date().toISOString(); | |
| // Check if there's already an open incident | |
| const issues = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| state: 'open', | |
| labels: 'incident,uptime-monitor' | |
| }); | |
| if (issues.data.length === 0) { | |
| // Create new incident issue | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| title: '🚨 Site Down - HTTP ' + httpCode + ' - ' + timestamp, | |
| body: '## Incident Report\n\n**Status**: 🔴 SITE DOWN\n**Timestamp**: ' + timestamp + '\n**HTTP Code**: ' + httpCode + '\n**Detected by**: Uptime Monitor Workflow\n\n### Details\nHomepage is returning HTTP ' + httpCode + ' instead of expected 200.\n\n### Action Required\n1. Check deployment status\n2. Review GitHub Pages configuration\n3. Verify DNS settings\n4. Check CDN status\n\n### Logs\n[Workflow Run](https://github.com/' + context.repo.owner + '/' + context.repo.name + '/actions/runs/' + context.runId + ')\n\n---\nThis issue was automatically created by the uptime monitor workflow', | |
| labels: ['incident', 'uptime-monitor', 'critical'] | |
| }); | |
| console.log('Incident issue created'); | |
| } else { | |
| // Update existing incident | |
| const issueNumber = issues.data[0].number; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| issue_number: issueNumber, | |
| body: 'Update: ' + timestamp + '\n\nSite still down - HTTP ' + httpCode + '\n\n[Latest workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.name + '/actions/runs/' + context.runId + ')' | |
| }); | |
| console.log('Incident issue updated'); | |
| } | |
| - name: Close resolved incidents | |
| if: success() && steps.homepage.outcome == 'success' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| // Find open incident issues | |
| const issues = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| state: 'open', | |
| labels: 'incident,uptime-monitor' | |
| }); | |
| for (const issue of issues.data) { | |
| // Close and comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| issue_number: issue.number, | |
| body: '### ✅ Incident Resolved\n\nSite is now operational.\n\n**Resolved at**: ' + new Date().toISOString() + '\n**Duration**: From ' + issue.created_at + ' to now\n\n[Verification workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.name + '/actions/runs/' + context.runId + ')' | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.name, | |
| issue_number: issue.number, | |
| state: 'closed', | |
| labels: ['incident', 'uptime-monitor', 'resolved'] | |
| }); | |
| console.log(`Closed incident issue #${issue.number}`); | |
| } |