Skip to content

Uptime monitor

Uptime monitor #168

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 "actions@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