From be0677a58b8bed9d7961413f71c6f8418f841c42 Mon Sep 17 00:00:00 2001 From: VpEngOps Date: Mon, 7 Sep 2026 12:37:57 -0500 Subject: [PATCH] ci: QA suite (repo+live), Gitea Actions workflow, app config-as-code https://projects.knownelement.com/issues/928 --- qa/site-qa.sh | 97 +++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/qa/site-qa.sh b/qa/site-qa.sh index 53e8a8b..5b67479 100755 --- a/qa/site-qa.sh +++ b/qa/site-qa.sh @@ -1,88 +1,93 @@ #!/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. +# internal links/assets resolve, pages have content, +# canonical URLs are HTTPS +# +# Exit non-zero on any failure. Used by Gitea Actions, the local pre-push +# hook, and the post-deploy verification in the site's 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]+\.)||' + +# pages-relative dir -> URL route: lowercase, strip NN. numeric prefixes +route_of() { + local p=${1#/} + p=$(echo "$p" | tr 'A-Z' 'a-z' | sed -E 's@(^|/)[0-9]+[.]@\1@g') + printf '%s' "${p:-/}" } +# collect route -> file map from the pages tree +declare -A ROUTES PFILE +while IFS= read -r f; do + grep -q '^title:' "$f" || fail "no title frontmatter: $f" + d=${f#pages/} # e.g. 02.shop/01.Welcome/page.md + d=${d%/*} # 02.shop/01.Welcome + d=${d%/page} # (page.md pages keep their folder route) + r="/$(route_of "$d")" + # Grav's homepage alias: the configured home folder is served at / + [ "$r" = "/home" ] && r="/" + ROUTES["$r"]=$f + PFILE["$r"]=$f +done < <(find pages -name '*.md') +ROUTES["/"]=${PFILE["home"]:-} + 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") + local f r link t + for r in "${!ROUTES[@]}"; do + f=${ROUTES[$r]} + [ -n "$f" ] || continue 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:-/}')" + t="$r/$link" + t=$(awk -v p="$t" 'BEGIN{n=split(p,seg,"/");out="";for(i=1;i<=n;i++){s=tolower(seg[i]);sub(/^[0-9]+\./,"",s);if(s==""||s==".")continue;if(s==".."){sub(/\/[^\/]*$/,"",out)}else{out=out"/"s}};print (out==""?"/":out)}') + [ -n "${ROUTES[$t]:-}" ] || fail "broken link '$link' in ${PFILE[$r]} (no route '$t')" done - done < <(find pages -name '*.md') + done 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 + local ROUTELIST=("/") SITEMAP=/tmp/qa-sitemap.txt CHECKED=/tmp/qa-checked.txt + local r + for r in "${!ROUTES[@]}"; do [ "$r" = "/" ] || ROUTELIST+=("/$r"); done : > $SITEMAP; : > $CHECKED - for r in $(printf '%s\n' "${ROUTES[@]}" | sort -u); do + for r in $(printf '%s\n' "${ROUTELIST[@]}" | 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 + can=$(curl -s -m 15 "$BASE/" | grep -oE '/dev/null | 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 ;; + *.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.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" + [ "$code" != "200" ] && fail "link $link -> $code" done done - # canonical must be https (reverse-proxy scheme detection) - can=$(curl -s -m 15 "$BASE/" | grep -oE '