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
This commit is contained in:
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/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; }
|
||||
Reference in New Issue
Block a user