#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PYTHON="${PYTHON:-$ROOT/.venv/bin/python}" NPM="${NPM:-/home/zemion/.nvm/versions/node/v22.22.3/bin/npm}" NODE_BIN="$(dirname "$NPM")" WEBUI_BIN="$ROOT/webui/node_modules/.bin" NPM_USERCONFIG="$(mktemp "${TMPDIR:-/tmp}/govoplan-npmrc.XXXXXXXX")" WORK_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/govoplan-release-integration.XXXXXXXX")" trap 'rm -rf "$WORK_ROOT"; rm -f "$NPM_USERCONFIG"' EXIT if [[ ! -x "$PYTHON" ]]; then PYTHON=python fi if [[ ! -x "$NPM" ]]; then NPM=npm NODE_BIN="$(dirname "$(command -v "$NPM")")" fi cd "$ROOT" RELEASE_VERSION="$("$PYTHON" - <<'PY' from pathlib import Path import tomllib with (Path.cwd() / "pyproject.toml").open("rb") as handle: print(tomllib.load(handle)["project"]["version"]) PY )" RELEASE_TAG="${GOVOPLAN_RELEASE_TAG:-v$RELEASE_VERSION}" export RELEASE_TAG RELEASE_VERSION export GOVOPLAN_CORE_SOURCE_ROOT="$ROOT" export PATH="$WEBUI_BIN:$NODE_BIN:$PATH" export NPM_CONFIG_USERCONFIG="$NPM_USERCONFIG" unset npm_config_tmp NPM_CONFIG_TMP run_step() { echo echo "==> $*" } 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 } run_discovered_tests() { local suite_dir="$1" local status if ! find "$suite_dir" -type f -name 'test*.py' -print -quit | grep -q .; then echo "No unittest test files found under $suite_dir; skipping." return 0 fi set +e "$PYTHON" -m unittest discover -s "$suite_dir" status=$? set -e if [[ "$status" == 5 ]]; then echo "No unittest tests discovered under $suite_dir; skipping." return 0 fi return "$status" } install_cloned_webui_dependencies() { local webui_dir="$1" if [[ ! -f "$webui_dir/package.json" ]]; then echo "No package.json found under $webui_dir; skipping WebUI dependency install." return 0 fi ( cd "$webui_dir" retry "$NPM" install --prefer-online --include=dev --no-save ) } run_step "Validate release refs and installed package metadata" "$PYTHON" - <<'PY' from __future__ import annotations import importlib.metadata as metadata import json import os import re import subprocess import sys from pathlib import Path root = Path.cwd() release_requirements = root / "requirements-release.txt" release_webui = root / "webui" / "package.release.json" expected_version = os.environ["RELEASE_VERSION"] expected_tag = os.environ["RELEASE_TAG"] errors: list[str] = [] python_requirements: list[tuple[str, str, str]] = [] for line in release_requirements.read_text(encoding="utf-8").splitlines(): match = re.match(r"^(govoplan-[a-z0-9-]+)\s+@\s+git\+ssh://(.+?\.git)@(v[0-9]+\.[0-9]+\.[0-9]+)$", line.strip()) if match: python_requirements.append((match.group(1), f"ssh://{match.group(2)}", match.group(3))) if not python_requirements: errors.append("No GovOPlaN git+ssh release requirements found.") for package_name, repo_url, tag in python_requirements: if tag != expected_tag: errors.append(f"{package_name} release requirement uses {tag!r}, expected {expected_tag!r}.") version = metadata.version(package_name) if version != tag.removeprefix("v"): errors.append(f"{package_name} installed version {version!r} does not match {tag!r}.") result = subprocess.run( ["git", "ls-remote", "--tags", repo_url, f"refs/tags/{tag}", f"refs/tags/{tag}^{{}}"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=45, check=False, ) if result.returncode != 0 or f"refs/tags/{tag}" not in result.stdout: errors.append(f"{package_name} tag {tag} is not readable from {repo_url}: {result.stderr.strip()}") release_package = json.loads(release_webui.read_text(encoding="utf-8")) if release_package.get("version") != expected_version: errors.append(f"Core release WebUI version is {release_package.get('version')!r}, expected {expected_version!r}.") webui_dependencies = release_package.get("dependencies", {}) for package_name, spec in sorted(webui_dependencies.items()): if not package_name.startswith("@govoplan/"): continue tag = spec.rsplit("#", 1)[-1] if "#" in spec else "" if tag != expected_tag: errors.append(f"{package_name} release dependency uses {tag!r}, expected {expected_tag}.") package_json = root / "webui" / "node_modules" / package_name / "package.json" if not package_json.exists(): errors.append(f"{package_name} is missing from release node_modules.") continue installed = json.loads(package_json.read_text(encoding="utf-8")) if installed.get("version") != expected_version: errors.append(f"{package_name} installed version {installed.get('version')!r}, expected {expected_version!r}.") shared_peers = ("lucide-react", "react", "react-dom", "react-router-dom") root_peers = release_package.get("peerDependencies", {}) for package_json in sorted((root / "webui" / "node_modules" / "@govoplan").glob("*/package.json")): package = json.loads(package_json.read_text(encoding="utf-8")) peers = package.get("peerDependencies", {}) for name in shared_peers: if name in peers and name in root_peers and peers[name] != root_peers[name]: errors.append(f"{package.get('name')} peer {name}={peers[name]!r}, expected {root_peers[name]!r}.") if errors: print("\n".join(errors), file=sys.stderr) raise SystemExit(1) print(f"Release refs and metadata passed for {len(python_requirements)} Python packages and {len(webui_dependencies)} WebUI packages.") PY run_step "Validate installed module manifests and registry" "$PYTHON" - <<'PY' from __future__ import annotations import sys from govoplan_core.server.registry import available_module_manifests, build_platform_registry expected = { "access", "admin", "audit", "calendar", "campaigns", "dashboard", "docs", "files", "identity", "idm", "mail", "ops", "organizations", "policy", "tenancy", } manifests = available_module_manifests() missing = sorted(expected - set(manifests)) if missing: print(f"Missing installed module manifests: {', '.join(missing)}", file=sys.stderr) raise SystemExit(1) registry = build_platform_registry(tuple(sorted(expected))) snapshot = registry.validate() print("Registry modules:", ", ".join(manifest.id for manifest in snapshot.manifests)) PY run_step "Run SQLite migration smoke for release modules" "$PYTHON" - <<'PY' from __future__ import annotations import tempfile from pathlib import Path from govoplan_core.db.migrations import migrate_database enabled_modules = ( "tenancy", "organizations", "identity", "idm", "access", "admin", "dashboard", "policy", "audit", "campaigns", "files", "mail", "calendar", "docs", "ops", ) with tempfile.TemporaryDirectory(prefix="govoplan-release-migration-") as tmp: database_url = f"sqlite:///{Path(tmp) / 'release-integration.sqlite3'}" result = migrate_database(database_url=database_url, enabled_modules=enabled_modules) if not result.current_revision: raise SystemExit("Migration smoke did not produce an Alembic revision.") print(f"SQLite release migration smoke reached {result.current_revision}.") PY run_step "Clone release module test sources" for repo in govoplan-mail govoplan-calendar govoplan-campaign; do git clone --depth 1 --branch "$RELEASE_TAG" "git@git.add-ideas.de:add-ideas/${repo}.git" "$WORK_ROOT/$repo" done run_step "Run core backend release tests" "$PYTHON" -m unittest \ tests.test_module_system \ tests.test_access_contracts \ tests.test_identity_organization_contracts \ tests.test_core_events \ tests.test_policy_contracts "$PYTHON" scripts/check_dependency_boundaries.py run_step "Run cloned module backend tests" run_discovered_tests "$WORK_ROOT/govoplan-mail/tests" run_discovered_tests "$WORK_ROOT/govoplan-calendar/tests" run_discovered_tests "$WORK_ROOT/govoplan-campaign/tests" run_step "Run core WebUI release tests and build" cd "$ROOT/webui" "$NPM" run test:mail-components "$NPM" run test:module-capabilities "$NPM" run test:module-permutations "$NPM" run build run_step "Run cloned module WebUI tests" install_cloned_webui_dependencies "$WORK_ROOT/govoplan-mail/webui" cd "$WORK_ROOT/govoplan-mail/webui" "$NPM" run test:mail-ui install_cloned_webui_dependencies "$WORK_ROOT/govoplan-campaign/webui" cd "$WORK_ROOT/govoplan-campaign/webui" "$NPM" run test:policy-ui "$NPM" run test:template-preview "$NPM" run test:import-utils echo echo "Release integration check passed."