Files
Website/qa/site-qa.sh
T
vpengops 01d6ad0e48 ci: QA suite (repo+live), Gitea Actions workflow, app config-as-code
- qa/site-qa.sh: repo structure/link checks + full live crawl (routes,
  links, assets, canonical-https)
- .gitea/workflows/qa.yml: repo QA on push, nightly live QA
- app/: config-as-code for the deployed sync job, scheduler entry,
  and Grav system.yaml (reverse-proxy HTTPS detection)
- local pre-push hook runs repo QA

https://projects.knownelement.com/issues/928
2026-09-07 12:27:54 -05:00

93 lines
3.9 KiB
Bash
Executable File

#!/bin/bash
# site-qa.sh - QA for the Grav Website repo and (with -l BASE) the live site.
# ./site-qa.sh repo checks: structure, frontmatter, link targets
# ./site-qa.sh -l BASE live checks: every known route returns 200, all
# internal links/assets resolve, pages have content
# Exit non-zero on any failure. Used by CI and the post-deploy sync job.
set -uo pipefail
cd "$(dirname "$0")/.."
FAIL=0
fail() { echo "FAIL: $*"; FAIL=$((FAIL+1)); }
route_of() { # pages-relative dir -> URL route (lowercase, strip NN. prefixes)
echo "$1" | tr 'A-Z' 'a-z' | sed -E 's|/([0-9]+\.)|/|g; s|^([0-9]+\.)||'
}
repo_checks() {
declare -A ROUTES
while IFS= read -r f; do
grep -q '^title:' "$f" || fail "no title frontmatter: $f"
d=$(dirname "$f"); r=$(route_of "${d#pages/}")
ROUTES["${r:-/}"]=1
done < <(find pages -name '*.md')
while IFS= read -r f; do
d=$(dirname "$f")
for link in $(grep -oE '\]\([^)#]+' "$f" | sed -E 's/^\]\(//'); do
case "$link" in http*|mailto:*) continue ;; esac
t=$(realpath -m --relative-to=. "$(dirname "$f")/$link" 2>/dev/null) || { fail "unresolvable link $link in $f"; continue; }
t=${t#pages}; t=$(route_of "$t")
[ -n "${ROUTES[${t:-/}]:-}" ] || fail "broken link '$link' in $f (no route '${t:-/}')"
done
done < <(find pages -name '*.md')
echo "repo: ${#ROUTES[@]} routes checked"
}
live_checks() {
local BASE=${1%/}
local ROUTES=("/")
while IFS= read -r f; do
d=$(dirname "$f"); r=$(route_of "${d#pages/}")
ROUTES+=("/${r#/}")
done < <(find pages -name '*.md' | grep -v 'pages/01.home/home.md')
local SITEMAP=/tmp/qa-sitemap.txt CHECKED=/tmp/qa-checked.txt
: > $SITEMAP; : > $CHECKED
for r in $(printf '%s\n' "${ROUTES[@]}" | sort -u); do
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 --retry 1 "$BASE$r")
if [ "$code" = "200" ]; then echo "$BASE$r" >> $SITEMAP; else fail "route $r -> $code"; fi
sleep 0.2
done
for url in $(cat $SITEMAP); do
body=$(curl -s -m 20 "$url"); sleep 0.2
[ ${#body} -lt 500 ] && fail "page too small ($url, ${#body} bytes)"
upath=${url#$BASE}; [ -z "$upath" ] && upath=/
for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do
case "$link" in
http*|mailto:*|\#*) [[ "$link" == "$BASE"* ]] || continue ;;
/*) link="$BASE$link" ;;
*) link="$BASE$(realpath -m "$upath/../$link" | sed 's|//\+|/|g')" ;;
esac
grep -qxF "$link" $CHECKED && continue
echo "$link" >> $CHECKED
case "$link" in
*.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.txt|*.xml) ;;
*) continue ;;
esac
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$link"); sleep 0.1
[ "$code" != "200" ] && fail "asset $link -> $code"
done
for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do
case "$link" in http*|mailto:*|\#*) continue ;; esac
case "$link" in
/*) L="$BASE$link" ;;
*) L="$BASE$(realpath -m "$upath/../$link" | sed 's|//\+|/|g')" ;;
esac
grep -qxF "$L" $CHECKED && continue
echo "$L" >> $CHECKED
case "$L" in
*.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.txt|*.xml) continue ;;
esac
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$L"); sleep 0.1
[ "$code" != "200" ] && fail "link $L -> $code"
done
done
# canonical must be https (reverse-proxy scheme detection)
can=$(curl -s -m 15 "$BASE/" | grep -oE '<link rel="canonical" href="[^"]*"' | head -1)
case "$can" in *https://*) : ;; "") fail "no canonical link on home" ;; *) fail "canonical not https: $can" ;; esac
echo "checked $(wc -l < $SITEMAP) routes, $(wc -l < $CHECKED) links/assets"
}
case "${1:-}" in
-l) live_checks "$2" ;;
*) repo_checks ;;
esac
[ $FAIL -eq 0 ] && echo "QA: PASS" || { echo "QA: $FAIL failure(s)"; exit 1; }