Uptime monitor #974
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 | |
| # Checks pearos.xyz every 15 minutes (offset off the exact quarter-hour, same | |
| # congestion reasoning as the other crons) and sends a WhatsApp alert only on | |
| # a state *transition* (up->down or down->up) -- never repeats the same | |
| # alert every run while it stays down, and never spams if it stays up. | |
| on: | |
| schedule: | |
| - cron: "4,19,34,49 * * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checking out source code | |
| uses: actions/checkout@v4 | |
| - name: Check pearos.xyz and detect a state change | |
| id: check | |
| run: | | |
| STATE_FILE="assets/data/uptime-state.json" | |
| [ -f "$STATE_FILE" ] || echo '{"status":"up"}' > "$STATE_FILE" | |
| prev_status=$(jq -r '.status' "$STATE_FILE") | |
| code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 https://pearos.xyz/ || echo "000") | |
| if [ "$code" = "200" ]; then | |
| new_status="up" | |
| else | |
| new_status="down" | |
| fi | |
| echo "Previous: $prev_status, now: $new_status (HTTP $code)" | |
| if [ "$prev_status" != "$new_status" ]; then | |
| # Only rewrite (and later commit) the state file on an actual | |
| # transition -- otherwise checked_at would differ every single | |
| # run and force a commit every 15 minutes for no real reason. | |
| jq -n --arg status "$new_status" --arg checked_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| '{status:$status, checked_at:$checked_at}' > "$STATE_FILE" | |
| echo "transitioned=true" >> "$GITHUB_OUTPUT" | |
| echo "new_status=$new_status" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "transitioned=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Send WhatsApp alert on state change | |
| if: steps.check.outputs.transitioned == 'true' | |
| env: | |
| WHATSER_APPKEY: ${{ secrets.WHATSER_APPKEY }} | |
| WHATSER_AUTHKEY: ${{ secrets.WHATSER_AUTHKEY }} | |
| WHATSER_TO: ${{ secrets.WHATSER_TO }} | |
| run: | | |
| ALERT_MESSAGE=$(python3 .github/scripts/uptime_alert.py "${{ steps.check.outputs.new_status }}") | |
| RESPONSE=$(curl -s --location --request POST "https://whatser.xyz/api/create-message" \ | |
| --form "appkey=$WHATSER_APPKEY" \ | |
| --form "authkey=$WHATSER_AUTHKEY" \ | |
| --form "to=$WHATSER_TO" \ | |
| --form "message=$ALERT_MESSAGE") | |
| echo "$RESPONSE" | |
| # curl alone doesn't fail this step on a 200 that carries an error | |
| # body -- Whatser returns {"message_status":"Success",...} when it | |
| # actually sent, or {"success":false,...} on a validation error, so | |
| # check explicitly instead of trusting the HTTP status. | |
| echo "$RESPONSE" | jq -e '.message_status == "Success"' > /dev/null | |
| - name: DNS failover to Cloudflare Pages mirror if GitHub Pages is down | |
| # Runs every check (not just on a transition) and is stateless -- it | |
| # re-derives what to do from the DNS records' current content, same | |
| # as the independent `pearos-failover` Cloudflare Worker. Runs | |
| # alongside that worker so the switch still happens even if GitHub | |
| # Actions itself is unavailable when GitHub Pages goes down. | |
| env: | |
| CF_API_TOKEN: ${{ secrets.CF_DNS_TOKEN }} | |
| CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} | |
| run: python3 .github/scripts/dns_failover.py | |
| - name: Commit updated state | |
| run: | | |
| git config user.name "pearOS build bot" | |
| git config user.email "327192259+pear-software@users.noreply.github.com" | |
| if git diff --quiet -- assets/data/uptime-state.json; then | |
| exit 0 | |
| fi | |
| git add assets/data/uptime-state.json | |
| git commit -m "Update uptime state" | |
| # Other workflows (hourly stats sync, DNS failover reruns) commit | |
| # to main independently -- a plain push here can lose a race and | |
| # fail outright, which then leaves this state file stuck stale | |
| # (causing every later run to see a wrong transition and re-alert). | |
| # Retry with a rebase instead of failing on the first collision. | |
| for i in 1 2 3 4 5; do | |
| git push origin HEAD:main && exit 0 | |
| git fetch origin main | |
| git rebase origin/main | |
| sleep $((RANDOM % 5 + 1)) | |
| done | |
| git push origin HEAD:main |