#!/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