#!/usr/bin/env bash set -euo pipefail META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" CORE_ROOT="${GOVOPLAN_CORE_ROOT:-$META_ROOT/../govoplan-core}" CORE_ROOT="$(cd "$CORE_ROOT" && pwd)" WEBUI_DIR="${1:-$CORE_ROOT/webui}" WORK_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/govoplan-webui-release-deps.XXXXXXXX")" GOVOPLAN_DEPS="$WORK_ROOT/govoplan-webui-deps.tsv" export GOVOPLAN_DEPS PACKAGE_LOCK="${GOVOPLAN_WEBUI_PACKAGE_LOCK:-}" PACKAGE_DIR="${GOVOPLAN_WEBUI_PACKAGE_DIR:-}" PYTHON_BIN="${PYTHON:-python3}" trap 'rm -rf "$WORK_ROOT"' EXIT retry() { local attempt status for attempt in 1 2 3; do if "$@"; then return 0 fi status=$? if [[ "$attempt" == 3 ]]; then return "$status" fi sleep $((attempt * 10)) done } cd "$WEBUI_DIR" node <<'JS' const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); const rel = JSON.parse(fs.readFileSync("package.release.json", "utf8")); const govoplanDeps = []; const dependencies = {...(rel.dependencies || {})}; for (const [name, spec] of Object.entries(dependencies)) { if (name.startsWith("@govoplan/")) { govoplanDeps.push([name, spec]); delete dependencies[name]; } } for (const key of ["devDependencies", "peerDependencies", "optionalDependencies", "overrides"]) { if (rel[key]) pkg[key] = rel[key]; } pkg.dependencies = dependencies; fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); fs.writeFileSync(process.env.GOVOPLAN_DEPS, govoplanDeps.map((item) => item.join("\t")).join("\n") + "\n"); JS rm -f package-lock.json npm cache clean --force retry npm install --prefer-online if [[ -n "$PACKAGE_LOCK" || -n "$PACKAGE_DIR" ]]; then [[ -n "$PACKAGE_LOCK" && -n "$PACKAGE_DIR" ]] || { echo "GOVOPLAN_WEBUI_PACKAGE_LOCK and GOVOPLAN_WEBUI_PACKAGE_DIR must be set together" >&2 exit 1 } "$PYTHON_BIN" - "$PACKAGE_LOCK" "$PACKAGE_DIR" "$GOVOPLAN_DEPS" <<'PY' from __future__ import annotations import hashlib import json import os from pathlib import Path import sys lock_path = Path(sys.argv[1]).resolve() package_dir = Path(sys.argv[2]).resolve() output = Path(sys.argv[3]) lock = json.loads(lock_path.read_text(encoding="utf-8")) if lock.get("schema_version") != "1" or not isinstance(lock.get("webui"), list): raise SystemExit("WebUI package lock is malformed") unsigned = dict(lock) expected_lock_hash = unsigned.pop("lock_sha256", None) actual_lock_hash = hashlib.sha256( json.dumps(unsigned, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest() if expected_lock_hash != actual_lock_hash: raise SystemExit("WebUI package lock hash does not match its contents") rows = {} for item in lock["webui"]: if not isinstance(item, dict) or not isinstance(item.get("name"), str): raise SystemExit("WebUI package lock contains a malformed artifact") if item["name"] in rows: raise SystemExit(f"WebUI package lock contains duplicate artifact {item['name']}") rows[item["name"]] = item install_all = os.environ.get("GOVOPLAN_WEBUI_INSTALL_ALL_PACKAGES", "").strip().lower() in {"1", "true", "yes", "on"} names = ( sorted(name for name in rows if name != "@govoplan/core-webui") if install_all else [ line.split("\t", 1)[0] for line in output.read_text(encoding="utf-8").splitlines() if line ] ) requested = [] for name in names: row = rows.get(name) if not isinstance(row, dict): raise SystemExit(f"WebUI package lock has no artifact for {name}") filename = row.get("filename") if not isinstance(filename, str) or Path(filename).name != filename: raise SystemExit(f"WebUI package lock has an invalid filename for {name}") artifact = package_dir / filename if artifact.is_symlink() or not artifact.is_file(): raise SystemExit(f"WebUI package artifact is missing for {name}") encoded = artifact.read_bytes() if len(encoded) != row.get("size") or hashlib.sha256(encoded).hexdigest() != row.get("sha256"): raise SystemExit(f"WebUI package artifact hash does not match for {name}") requested.append(f"{name}\tfile:{artifact}") output.write_text("\n".join(requested) + "\n", encoding="utf-8") PY fi module_paths=() while IFS=$'\t' read -r package_name spec; do [[ -n "${package_name:-}" ]] || continue if [[ "$spec" == file:* ]]; then echo "Installing $package_name from verified package artifact" module_paths+=("$spec") continue fi git_url="${spec%%#*}" git_ref="${spec#*#}" if [[ "$git_url" == "$spec" || -z "$git_ref" ]]; then echo "Unsupported GovOPlaN WebUI git spec for $package_name: $spec" >&2 exit 1 fi clone_url="${git_url#git+}" clone_dir="$WORK_ROOT/${package_name#@govoplan/}" echo "Installing $package_name from $clone_url#$git_ref" retry git clone --depth 1 --branch "$git_ref" "$clone_url" "$clone_dir" module_paths+=("file:$clone_dir") done < "$GOVOPLAN_DEPS" if [[ "${#module_paths[@]}" -gt 0 ]]; then # Module repositories historically declared their local Vite/TypeScript # toolchain as peers. The release host owns that build toolchain; resolving # tagged source packages must not let an older, unused peer range block the # verified composition. retry npm install --prefer-online --no-save --install-links --legacy-peer-deps "${module_paths[@]}" fi