Skip to content

live-site canary

live-site canary #653

Workflow file for this run

name: live-site canary
# Hourly synthetic check that the live park pages render non-zero
# ride data. Catches the failure mode where the SSR DDB read
# silently returns an empty list (e.g. the pagination bug shipped
# in 2026-05-24 that broke all four park pages for ~7 days before
# being caught by accident).
#
# When this fails: GitHub emails the repo owner. Treat it like a
# Pushover alert — investigate within the hour. Either the live
# site is genuinely broken, or themeparks.wiki is having an
# outage long enough that no rides are operating (rare).
#
# This is NOT a substitute for unit tests — see TESTING.md
# "Failure modes we explicitly watch for". It's the runtime layer
# of a three-layer defense: code-time review + test-time mocks +
# runtime canary. Each catches different things; this one catches
# the bugs that ship through despite the other two.
#
# Assertion design (rewritten 2026-06-11): the meta line is
# rendered by React 19 SSR, which inserts "<!-- -->" comment
# separators between adjacent text nodes — so the live HTML reads
# `35<!-- --> attractions · <!-- -->30<!-- --> open · ...`, NOT
# `35 attractions`. The previous canary grepped for the literal
# empty-data phrase "0 attractions · 0 open ·" and therefore could
# NEVER match (separators broke it) — the stop-loss was dead code.
# The fix strips the separators and makes a POSITIVE assertion:
# extract the attractions count and require it to be present AND
# > 0. A positive assertion also fails loudly if the markup ever
# changes (count can't be parsed), instead of a negative match that
# silently passes. The check_canary.sh script holds the logic so the
# self-test below exercises the exact same code path against fixtures.
on:
schedule:
# Hourly at :07 to avoid alignment with the poller's :00 / :02
# cadence and any common cron-storm minute.
- cron: "7 * * * *"
workflow_dispatch:
# Also run on PRs that touch the web app's DDB layer or the
# user-facing read pages — a real-data cross-check before merge.
# Note: on a PR this still curls PRODUCTION, not the PR's code, so
# it's a "did we break prod" smoke check, not a test of the diff.
pull_request:
paths:
- "web/src/lib/dynamodb.ts"
- "web/src/lib/dynamodb-writes.ts"
- "web/src/app/parks/**"
- "web/src/app/me/**"
- ".github/workflows/canary.yml"
- ".github/scripts/check_canary.sh"
jobs:
# Self-test the assertion logic against captured fixtures BEFORE
# trusting it against the live site. Guards against the grep
# silently rotting again (the 2026-06-11 failure): if the parser
# can't catch a known-bad page or rejects a known-good one, this
# job fails and the canary never gives false assurance.
selftest-assertion:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Known-good HTML must pass; known-empty HTML must fail
run: |
set -euo pipefail
chmod +x .github/scripts/check_canary.sh
# React-SSR meta line WITH separators, non-zero counts → must PASS.
good='<p class="label-meta mt-4">35<!-- --> attractions · <!-- -->30<!-- --> open ·<!-- --> <!-- -->2<!-- --> down · <!-- -->3<!-- --> closed</p>'
if ! .github/scripts/check_canary.sh "$good" "good-fixture"; then
echo "::error::Self-test FAILED: known-good HTML was rejected"
exit 1
fi
# The empty-data regression shape (also with separators) → must FAIL.
bad='<p class="label-meta mt-4">0<!-- --> attractions · <!-- -->0<!-- --> open ·<!-- --> <!-- -->0<!-- --> down · <!-- -->0<!-- --> closed</p>'
if .github/scripts/check_canary.sh "$bad" "bad-fixture"; then
echo "::error::Self-test FAILED: known-empty HTML was NOT caught"
exit 1
fi
# Missing meta line entirely (structure changed) → must FAIL.
if .github/scripts/check_canary.sh "<p>no meta here</p>" "missing-fixture"; then
echo "::error::Self-test FAILED: missing meta line was NOT caught"
exit 1
fi
echo "Self-test passed: assertion catches empty + missing, accepts good."
check-parks-render-rides:
needs: selftest-assertion
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Curl each park, assert non-zero attractions count
run: |
set -euo pipefail
chmod +x .github/scripts/check_canary.sh
base="https://magicmonitor.megillini.dev"
failed=()
for park in magic_kingdom epcot hollywood_studios animal_kingdom; do
url="$base/parks/$park"
html=$(curl --silent --show-error --fail --max-time 30 "$url")
if .github/scripts/check_canary.sh "$html" "$park"; then
echo "OK: $park"
else
echo "::error::Park $park failed canary: $url"
failed+=("$park")
fi
done
if [ ${#failed[@]} -gt 0 ]; then
echo "::error::Canary failed for: ${failed[*]}"
exit 1
fi
echo "All four parks rendered non-zero ride data."