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:
@@ -0,0 +1,24 @@
|
|||||||
|
name: QA
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * *' # 09:00 UTC nightly live QA
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
repo-qa:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Repo checks (structure, frontmatter, link targets)
|
||||||
|
run: ./qa/site-qa.sh
|
||||||
|
|
||||||
|
live-qa:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Live checks (Rogue site routes, links, canonical)
|
||||||
|
run: ./qa/site-qa.sh -l https://roguetechnologies.net
|
||||||
@@ -7,3 +7,19 @@ found fully compromised (see Discourse topic 345).
|
|||||||
|
|
||||||
Commits from the site (Git Sync) are authored by **VpEngOps**.
|
Commits from the site (Git Sync) are authored by **VpEngOps**.
|
||||||
Related: https://projects.knownelement.com/issues/928
|
Related: https://projects.knownelement.com/issues/928
|
||||||
|
|
||||||
|
## QA / CI / deployment process
|
||||||
|
|
||||||
|
- `qa/site-qa.sh` — repo checks (structure, frontmatter, internal link targets).
|
||||||
|
Run before every push; also enforced by the local pre-push hook.
|
||||||
|
- `.gitea/workflows/qa.yml` — Gitea Actions: repo QA on every push, nightly
|
||||||
|
live QA (full route crawl + link/asset check + HTTPS-canonical check).
|
||||||
|
- Deployment: the site container runs `app/git-sync-job.sh` (config-as-code
|
||||||
|
copy here) every 15 minutes via the Grav scheduler
|
||||||
|
(`app/scheduler/git-sync.job.yaml`): pulls upstream, clears the Grav cache,
|
||||||
|
verifies `/` and `/shop` answer 200 — **rolls back to the previous commit
|
||||||
|
automatically if verification fails** — and pushes local content edits
|
||||||
|
(authored VpEngOps).
|
||||||
|
- Site management: edit here (gitea web UI works fine) and the site follows
|
||||||
|
within 15 minutes. There is no /admin UI: Grav 2 ships without one and this
|
||||||
|
deployment is git-first by design.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# git-sync-job.sh v2 - deploy upstream, verify, roll back on failure.
|
||||||
|
# Pull: if upstream moved -> deploy + clearcache + verify key routes; rollback on failure.
|
||||||
|
# Push: if local pages edits exist -> commit + push (VpEngOps identity).
|
||||||
|
cd /app/data/user || exit 1
|
||||||
|
GIT=/usr/bin/git
|
||||||
|
BASE="https://${CLOUDRON_APP_DOMAIN}"
|
||||||
|
PORT="${CLOUDRON_APP_PORT:-80}"
|
||||||
|
verify() { # returns 0 if key routes answer 200
|
||||||
|
sleep 2
|
||||||
|
for r in / /shop; do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' -m 10 "http://127.0.0.1:${PORT}${r}" 2>/dev/null)
|
||||||
|
[ "$code" != "200" ] && { echo "$(date -u +%FT%TZ) ALARM: route $r -> $code after deploy"; return 1; }
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$GIT fetch origin main 2>/dev/null || exit 0
|
||||||
|
LOCAL=$($GIT rev-parse HEAD 2>/dev/null || echo none)
|
||||||
|
REMOTE=$($GIT rev-parse origin/main 2>/dev/null || echo none)
|
||||||
|
if [ "$LOCAL" != "$REMOTE" ]; then
|
||||||
|
if [ "$LOCAL" = "none" ]; then
|
||||||
|
$GIT checkout -f -B main origin/main || exit 0
|
||||||
|
else
|
||||||
|
PREV=$LOCAL
|
||||||
|
$GIT reset --hard origin/main || exit 0
|
||||||
|
fi
|
||||||
|
$GIT clean -fdq pages/ 2>/dev/null
|
||||||
|
cd /app/code && php bin/grav clearcache >/dev/null 2>&1; cd /app/data/user
|
||||||
|
if ! verify; then
|
||||||
|
if [ -n "${PREV:-}" ]; then
|
||||||
|
$GIT reset --hard "$PREV" && $GIT clean -fdq pages/ 2>/dev/null
|
||||||
|
cd /app/code && php bin/grav clearcache >/dev/null 2>&1
|
||||||
|
echo "$(date -u +%FT%TZ) rolled back to $PREV"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$(date -u +%FT%TZ) deployed $REMOTE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! $GIT diff --quiet || ! $GIT diff --cached --quiet; then
|
||||||
|
$GIT add pages/
|
||||||
|
$GIT -c user.name=VpEngOps -c user.email=tsgstaff-coo-vpengops@turnsys.com commit -qm '(Grav site) content changes' || exit 0
|
||||||
|
fi
|
||||||
|
$GIT push origin main 2>/dev/null && echo "$(date -u +%FT%TZ) pushed" || true
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
enabled: true
|
||||||
|
run_at: '*/15 * * * *'
|
||||||
|
command: /bin/bash /app/data/git-sync-job.sh
|
||||||
|
output: logs/git-sync-job.log
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
site:
|
||||||
|
title: 'Rogue Technologies'
|
||||||
|
description: 'Fabrication, printing, and maker services in San Antonio, TX.'
|
||||||
|
author:
|
||||||
|
name: 'VpEngOps'
|
||||||
|
email: 'tsgstaff-coo-vpengops@turnsys.com'
|
||||||
|
reverse_proxy_setup: true
|
||||||
|
http_x_forwarded:
|
||||||
|
protocol: true
|
||||||
|
host: true
|
||||||
|
debugger:
|
||||||
|
enabled: false
|
||||||
|
errors:
|
||||||
|
display: false
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# runs repo QA before any push
|
||||||
|
"$(dirname "$0")/site-qa.sh"
|
||||||
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