#!/bin/bash # Static checks for everything in the bundle. Run before committing / deploying. set -uo pipefail cd "$(dirname "$0")" fail=0 say() { printf '%-28s %s\n' "$1" "$2"; } for f in install.sh addCommunityMediaServer.sh installJellyfinPlugin.sh registerGiteaRunner.sh createGiteaDb.sh deployLive.sh addClipUser.sh deploy.sh deployBotBundle.sh switchToClone.sh firewall/firewall.sh bin/*.sh home/*.sh bot/entrypoint.sh mariadb/init/*.sh; do [[ -f $f ]] || continue bash -n "$f" && say "$f" ok || { say "$f" "BASH SYNTAX"; fail=1; } done for f in configure.py bin/dumpd.py bin/vgstrip.py bin/vgstitch.py bin/vghosts-check.py observability/kibana-setup.py observability/kibana-alarms.py; do python3 -m py_compile "$f" && say "$f" ok || { say "$f" "PYTHON SYNTAX"; fail=1; } done rm -rf __pycache__ bin/__pycache__ # bin/install.list is the single source for what lands in /usr/local/bin. Fail if it names something that # does not exist, or if a file in bin/ is neither listed nor explicitly excluded — that drift is exactly how # deploy.sh ended up installing five of ten scripts. if python3 - <<'PYMAN' >/dev/null 2>&1 import re, sys from pathlib import Path text = Path("bin/install.list").read_text() listed = dict((m.group(2), m.group(1)) for m in re.finditer(r"^(0\d{3}) (\S+)$", text, re.M)) excluded = set(re.findall(r"^#\s+exclude (\S+)", text, re.M)) present = set("bin/" + p.name for p in Path("bin").iterdir() if p.is_file() and p.name != "install.list") missing = set(listed) - present unclaimed = present - set(listed) - excluded assert not missing, f"install.list names files that do not exist: {sorted(missing)}" assert not unclaimed, f"bin/ files neither listed nor excluded in install.list: {sorted(unclaimed)}" PYMAN then say "bin/install.list" ok; else say "bin/install.list" "DRIFTED — see the assertion"; fail=1; fi # Only files that are actually here. The server checkout is partial (scp, not a full clone), and a tool that # reports "broken" when it means "absent" gets ignored — the same reason the deadman reports on change only. yamls=(); for f in docker-compose.yml mediamtx.example.yml observability/filebeat.yml observability/metricbeat.yml observability/kibana.yml; do [[ -f $f ]] && yamls+=("$f"); done python3 -c "import yaml,sys; [yaml.safe_load(open(f)) for f in sys.argv[1:]]" "${yamls[@]}" \ && say "yaml (${#yamls[@]} files)" ok || { say "yaml" "INVALID"; fail=1; } if ! command -v node >/dev/null; then say "page JS" "(skipped: node not installed here)" else node -e " const fs=require('fs'); for (const f of ['dock/index.html','dock/clock/index.html','clips/index.html','about/index.html','streaming/index.html']) { if (!fs.existsSync(f)) continue; const html=fs.readFileSync(f,'utf8'); for (const m of html.matchAll(/]*)>([\s\S]*?)<\/script>/g)) if (m[1].trim()) new Function(m[1]); } if (fs.existsSync('dock/clock/vgclock.js')) new Function(fs.readFileSync('dock/clock/vgclock.js','utf8'));" \ && say "page JS" ok || { say "page JS" "SYNTAX ERROR"; fail=1; } fi if [[ -f live/src/vgclock.js && -f dock/clock/vgclock.js ]]; then diff <(sed '$d' live/src/vgclock.js | sed 's/})(typeof window.*/})(window);/') dock/clock/vgclock.js >/dev/null \ && say "vgclock.js in sync (dock/portal)" ok \ || say "vgclock.js in sync (dock/portal)" "DIFFERS — copy dock/clock/vgclock.js to live/src and re-add the export line" else say "vgclock.js in sync" "(skipped: only one copy in this checkout)" fi # generated Caddyfile / mediamtx / copyparty from a dry configure run must at least be produced + parse T=$(mktemp -d); cp configure.py "$T/"; ( cd "$T" && VG_DRY=1 python3 -c " import configure as c, yaml c.write_caddy('media.example.com','auth.example.com','watch.example.com','git.example.com','bot.example.com','live.example.com','files.example.com','sync.example.com','clips.example.com') c.write_mediamtx('media.example.com'); yaml.safe_load(open('mediamtx.yml')); c.write_copyparty() assert 'authMethod: http' in open('mediamtx.yml').read(); assert 'idp-h-usr' in open('copyparty.conf').read() " ) && say "configure.py generators" ok || { say "configure.py generators" "FAILED"; fail=1; } rm -rf "$T" # dumpd GET /metrics: valid Prometheus text out of script counters + a fake media tree T=$(mktemp -d); mkdir -p "$T/.markers/metrics" "$T/streams/u/Title/.live" printf 'runs 2\njobs:encode:done:stream 1\nseconds:encode:stream 12.5\nlast_finish 1762210000\n' > "$T/.markers/metrics/sweep.counters" touch -d '2020-01-01' "$T/streams/u/Title/.live/old.mp4" if MEDIA_ROOT="$T" MTX_API_URL=http://127.0.0.1:1 python3 - <<'PYMET' >/dev/null 2>&1 import re, sys sys.path.insert(0, "bin") import dumpd text = dumpd.metrics_text() for line in text.splitlines(): assert line.startswith("#") or re.fullmatch(r'[a-z_]+(\{[^}]*\})? -?[0-9.]+', line), line assert 'vanguard_media_jobs_total{component="sweep",kind="stream",outcome="done",verdict="encode"} 1' in text assert 'vanguard_media_last_finish_timestamp_seconds{component="sweep"} 1762210000' in text # not 1.76e+09 assert 'vanguard_media_pending{kind="stream"} 1' in text assert 'vanguard_mediamtx_up 0' in text and 'vanguard_streams_active' not in text # no fake zeros PYMET then say "dumpd /metrics" ok; else say "dumpd /metrics" "FAILED"; fail=1; fi rm -rf "$T" bin/__pycache__ if command -v caddy >/dev/null; then T=$(mktemp -d); cp configure.py "$T/"; ( cd "$T" && VG_DRY=1 python3 -c "import configure as c; c.write_caddy('media.example.com','a.example.com','w.example.com','g.example.com','b.example.com','l.example.com','f.example.com','s.example.com','c.example.com')" && caddy validate --adapter caddyfile --config Caddyfile >/dev/null 2>&1 ) && say "Caddyfile (caddy validate)" ok || { say "Caddyfile (caddy validate)" "INVALID"; fail=1; } rm -rf "$T" fi [[ -d live/node_modules ]] && ( cd live && npx --no-install vite build --logLevel error >/dev/null 2>&1 ) && say "live portal build" ok || say "live portal build" "(skipped or failed — run npm run build in live/)" exit $fail