Cover all retry call sites with isolated regressions in focused checks and installer CI. Add EN/DE operating guidance and record the unreleased Xrechnung and installer audit follow-ups without changing immutable release artifacts. Refs #54
153 lines
5.2 KiB
Bash
153 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
|
|
else
|
|
status=$?
|
|
fi
|
|
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
|