Add release integration workflow
This commit is contained in:
48
.gitea/workflows/release-integration.yml
Normal file
48
.gitea/workflows/release-integration.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Release Integration
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Configure SSH for release dependencies
|
||||
env:
|
||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
|
||||
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
- name: Install backend release integration dependencies
|
||||
run: |
|
||||
python -m venv .venv
|
||||
.venv/bin/python -m pip install --upgrade pip
|
||||
.venv/bin/python -m pip install -r requirements-release.txt
|
||||
.venv/bin/python -m pip install '.[dev]'
|
||||
- name: Install WebUI release dependencies
|
||||
working-directory: webui
|
||||
run: |
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); const rel=JSON.parse(fs.readFileSync('package.release.json')); for (const key of ['dependencies','devDependencies','peerDependencies','optionalDependencies','overrides']) if (rel[key]) pkg[key]=rel[key]; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');"
|
||||
rm -f package-lock.json
|
||||
npm cache clean --force
|
||||
npm install --prefer-online
|
||||
- name: Run release integration checks
|
||||
run: bash scripts/check-release-integration.sh
|
||||
230
scripts/check-release-integration.sh
Normal file
230
scripts/check-release-integration.sh
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/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",
|
||||
"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."
|
||||
Reference in New Issue
Block a user