Vestibule/scripts/validate.sh

204 lines
7.4 KiB
Bash
Executable File

#!/bin/sh
# validate.sh — local pre-ship validation for the Vestibule release.
# Mirrors the CI checks that can run without Windows/flatpak/cargo.
#
# Usage: sh scripts/validate.sh (from anywhere inside the repo copy)
# Exit: 0 all pass, 1 failure
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "${REPO_ROOT}"
FAILURES=0
say() { printf '%s\n' "$1"; }
pass() { say " [pass] $1"; }
fail() { FAILURES=$((FAILURES+1)); say " [FAIL] $1"; }
say "==> POSIX sh syntax"
for f in scripts/*.sh scripts/vestibule-kiosk-launch packaging/vestibule-flatpak-cli packaging/build-flatpak.sh; do
if sh -n "$f" 2>/dev/null; then pass "$f"; else fail "$f"; fi
done
say "==> Python syntax"
for f in scripts/*.py; do
if python3 -m py_compile "$f" 2>/dev/null; then pass "$f"; else fail "$f"; fi
done
say "==> JSON validity"
for f in extension/manifest.json config/policies.json config/com.vestibule.usher.linux.json \
config/com.vestibule.usher.windows.json packaging/net.dcos.Vestibule.json; do
if python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then pass "$f"; else fail "$f"; fi
done
say "==> XML validity"
if python3 -c "import xml.dom.minidom; xml.dom.minidom.parse('packaging/net.dcos.Vestibule.metainfo.xml')" 2>/dev/null; then
pass "metainfo.xml"
else
fail "metainfo.xml"
fi
say "==> YAML validity (ci.yml)"
if python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" 2>/dev/null; then
pass "ci.yml"
else
fail "ci.yml"
fi
say "==> Version consistency (1.2.2)"
v_cargo=$(sed -n 's/^version = "\(.*\)"/\1/p' helper/Cargo.toml | head -1)
v_manifest=$(python3 -c "import json; print(json.load(open('extension/manifest.json'))['version'])")
v_metainfo=$(python3 -c "import re; print(re.search(r'release version=\"([^\"]+)\"', open('packaging/net.dcos.Vestibule.metainfo.xml').read()).group(1))")
v_iss=$(sed -n 's/^#define MyAppVersion "\(.*\)"/\1/p' packaging/vestibule.iss | head -1)
v_cli=$(sed -n 's/^VERSION="\(.*\)"/\1/p' packaging/vestibule-flatpak-cli | head -1)
for pair in "Cargo.toml:$v_cargo" "manifest.json:$v_manifest" "metainfo:$v_metainfo" "vestibule.iss:$v_iss" "flatpak-cli:$v_cli"; do
name=${pair%%:*}; val=${pair#*:}
if [ "$val" = "1.2.2" ]; then pass "$name = $val"; else fail "$name = $val (expected 1.2.2)"; fi
done
say "==> PowerShell sanity (structure checks)"
if python3 - <<'PYEOF'
import sys, re
files = [
"scripts/provision-kiosk.ps1",
"scripts/deprovision-kiosk.ps1",
"scripts/kiosk-launch.ps1",
"packaging/build-installer.ps1",
]
problems = []
for path in files:
src = open(path, encoding="utf-8").read()
lines = src.splitlines()
# Here-string terminators must start at column 0.
for i, ln in enumerate(lines, 1):
if re.match(r'^\s+@"|^\s+@\'', ln):
problems.append(f"{path}:{i} here-string opener must be at column 0")
# Brace/paren/bracket balance outside strings and comments (heuristic).
# Order matters: strip quoted strings FIRST (strings may contain '#'),
# then strip trailing comments (comments may contain quotes).
cleaned = []
in_herestring = False
for ln in lines:
if in_herestring:
if ln.startswith('"@') or ln.startswith("'@"):
in_herestring = False
continue
if ln.lstrip().startswith('@"') or ln.lstrip().startswith("@'"):
in_herestring = True
continue
no_strings = re.sub(r'"[^"]*"', '""', ln)
no_strings = re.sub(r"'[^']*'", "''", no_strings)
no_comment = re.sub(r'#.*$', '', no_strings)
cleaned.append(no_comment)
blob = "\n".join(cleaned)
for open_c, close_c in [("{", "}"), ("(", ")"), ("[", "]")]:
if blob.count(open_c) != blob.count(close_c):
problems.append(
f"{path}: unbalanced {open_c}{close_c} "
f"({blob.count(open_c)} vs {blob.count(close_c)})")
if problems:
print("\n".join(problems))
sys.exit(1)
print(f"{len(files)} files structurally consistent")
PYEOF
then
pass "PowerShell structural checks"
else
fail "PowerShell structural checks"
fi
say "==> URL policy unit tests (node)"
# The engine is pure JavaScript with no browser dependencies; Node runs
# the same file the background script loads. CI runs this gate on every
# push; on a node-less machine it is reported, not silently skipped.
if command -v node >/dev/null 2>&1; then
if node scripts/test-url-policy.js >/tmp/vestibule-urlpolicy.log 2>&1; then
pass "scripts/test-url-policy.js"
else
fail "scripts/test-url-policy.js"
tail -20 /tmp/vestibule-urlpolicy.log
fi
else
say " [warn] node not found — URL policy unit tests skipped (CI runs them)"
fi
say "==> XPI build smoke test"
tmp_xpi=$(mktemp -u).xpi
if python3 - "$REPO_ROOT/extension" "$tmp_xpi" <<'PYEOF'
import os, sys, zipfile
src_dir, dst = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as z:
for root, dirs, files in os.walk(src_dir):
dirs.sort()
for f in sorted(files):
full = os.path.join(root, f)
z.write(full, os.path.relpath(full, src_dir))
with zipfile.ZipFile(dst) as z:
m = __import__("json").loads(z.read("manifest.json"))
assert m["version"] == "1.2.2"
assert m["browser_specific_settings"]["gecko"]["id"] == "vestibule@vestibule.kiosk"
assert m["background"]["scripts"][0] == "url-policy.js", "policy engine must load first"
for name in ["background.js", "url-policy.js", "content.js", "admin.html", "unlock.html", "blocked.html"]:
assert name in z.namelist(), name
PYEOF
then
pass "XPI builds; manifest + all entry points present"
else
fail "XPI build"
fi
rm -f "$tmp_xpi"
say "==> Icon assets"
for f in packaging/icons/vestibule.ico packaging/icons/vestibule.svg packaging/icons/vestibule-256.png; do
if [ -s "$f" ]; then pass "$f"; else fail "$f"; fi
done
say "==> Historic artifact scrub (history/)"
# The 2001 VB6 artifact and every other file in the tree must stay free
# of the scrubbed identifiers: the school initials, the employer names,
# the original unlock code, and the employer-branded project name.
if python3 - <<'PYEOF'
import os
import sys
FORBIDDEN = ("gl" "ths", # school initials
"advanced technical " "solutions", # employer name
"ats" "inc", # original unlock code
"ats" "browser", # employer-branded project name
"school" "version") # renamed form's old filename
hits = []
for root, dirs, files in os.walk("."):
dirs[:] = [d for d in dirs if d not in (".git", "__pycache__")]
for name in files:
path = os.path.join(root, name)
try:
blob = open(path, "rb").read().decode("utf-8", "ignore").lower()
except OSError:
continue
hits.extend(f"{path}: {token}" for token in FORBIDDEN if token in blob)
if hits:
print("\n".join(hits))
sys.exit(1)
PYEOF
then
pass "no scrub-list identifiers anywhere in the tree"
else
fail "scrubbed identifiers present (see list above)"
fi
say "==> Rootless provision/deprovision integration test"
if sh scripts/test-provision-linux.sh > /tmp/vestibule-inttest.log 2>&1; then
pass "3-scenario integration test (LibreWolf native + Firefox native + Firefox Flatpak)"
else
fail "integration test"
tail -40 /tmp/vestibule-inttest.log
fi
say ""
if [ "${FAILURES}" -eq 0 ]; then
say "OK — all local validation passed."
exit 0
else
say "FAIL: ${FAILURES} check(s) failed"
exit 1
fi