Files
govoplan/tools/release/install-webui-release-dependencies.sh
T
zemion 09046e6e62
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 11m49s
Publish complete signed module catalogs
2026-08-06 21:13:33 +02:00

152 lines
5.2 KiB
Bash

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