232 lines
7.5 KiB
Bash
232 lines
7.5 KiB
Bash
#!/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 PATH="$WEBUI_BIN:$NODE_BIN:$PATH"
|
|
export NPM_CONFIG_USERCONFIG="$NPM_USERCONFIG"
|
|
unset npm_config_tmp NPM_CONFIG_TMP
|
|
|
|
run_step() {
|
|
echo
|
|
echo "==> $*"
|
|
}
|
|
|
|
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"
|
|
"$PYTHON" -m unittest discover -s "$WORK_ROOT/govoplan-mail/tests"
|
|
"$PYTHON" -m unittest discover -s "$WORK_ROOT/govoplan-calendar/tests"
|
|
"$PYTHON" -m unittest discover -s "$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"
|
|
cd "$WORK_ROOT/govoplan-mail/webui"
|
|
"$NPM" run test:mail-ui
|
|
|
|
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."
|