From a18c7ea040fb0e64453a5b50666edfdc64109de3 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 12:51:25 +0200 Subject: [PATCH] fix(release): validate independent module versions --- requirements-release.txt | 7 ++-- tests/test_release_entrypoint_gates.py | 7 ++++ tools/checks/check-release-integration.sh | 44 +++++++++++++++++------ 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/requirements-release.txt b/requirements-release.txt index d0a77e7..0a80b61 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,6 +1,5 @@ -# Whole-product release install from tagged module repositories. -# Update GOVOPLAN_RELEASE_TAG together with pyproject/package versions when -# cutting a release. +# Whole-product release install from immutable, independently versioned module tags. +# Only add a module after its referenced tag has been published. ../govoplan-core[server] govoplan-tenancy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-tenancy.git@v0.1.8 govoplan-organizations @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git@v0.1.8 @@ -16,9 +15,7 @@ govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0. govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.8 govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.8 govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.8 -govoplan-poll @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-poll.git@v0.1.8 govoplan-scheduling @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-scheduling.git@v0.1.8 govoplan-notifications @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-notifications.git@v0.1.8 -govoplan-evaluation @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-evaluation.git@v0.1.8 govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.8 govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.8 diff --git a/tests/test_release_entrypoint_gates.py b/tests/test_release_entrypoint_gates.py index d413a12..cd91646 100644 --- a/tests/test_release_entrypoint_gates.py +++ b/tests/test_release_entrypoint_gates.py @@ -55,6 +55,13 @@ class ReleaseEntrypointGateTests(unittest.TestCase): self.assertIn("write_module_directory(", publisher) self.assertNotIn("shutil.copytree(candidate_modules", publisher) + def test_release_integration_honors_independent_module_versions(self) -> None: + script = (META_ROOT / "tools" / "checks" / "check-release-integration.sh").read_text() + + self.assertNotIn('expected_tag = os.environ["RELEASE_TAG"]', script) + self.assertIn('dependency_version = tag_match.group(1)', script) + self.assertIn('repo_tag="$(release_tag_for_package "$repo")"', script) + if __name__ == "__main__": unittest.main() diff --git a/tools/checks/check-release-integration.sh b/tools/checks/check-release-integration.sh index 216d5aa..8738364 100644 --- a/tools/checks/check-release-integration.sh +++ b/tools/checks/check-release-integration.sh @@ -32,9 +32,8 @@ 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 RELEASE_VERSION export META_ROOT export GOVOPLAN_CORE_SOURCE_ROOT="$ROOT" export GOVOPLAN_MIGRATION_TRACK=release @@ -91,6 +90,30 @@ install_cloned_webui_dependencies() { ) } +release_tag_for_package() { + local package_name="$1" + "$PYTHON" - "$META_ROOT/requirements-release.txt" "$package_name" <<'PY' +from __future__ import annotations + +from pathlib import Path +import re +import sys + + +requirements_path = Path(sys.argv[1]) +package_name = sys.argv[2] +pattern = re.compile( + rf"^{re.escape(package_name)}\s+@\s+git\+ssh://.+?\.git@(v[0-9]+\.[0-9]+\.[0-9]+)$" +) +for line in requirements_path.read_text(encoding="utf-8").splitlines(): + match = pattern.match(line.strip()) + if match: + print(match.group(1)) + raise SystemExit(0) +raise SystemExit(f"No immutable release reference found for {package_name}") +PY +} + run_step "Validate release refs and installed package metadata" "$PYTHON" "$META_ROOT/tools/checks/check-version-alignment.py" --release-composition "$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact @@ -109,7 +132,6 @@ root = Path.cwd() release_requirements = Path(os.environ["META_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]] = [] @@ -122,8 +144,6 @@ 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}.") @@ -147,15 +167,18 @@ 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}.") + tag_match = re.fullmatch(r"v([0-9]+\.[0-9]+\.[0-9]+)", tag) + if tag_match is None: + errors.append(f"{package_name} release dependency has invalid immutable tag {tag!r}.") + continue + dependency_version = tag_match.group(1) 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}.") + if installed.get("version") != dependency_version: + errors.append(f"{package_name} installed version {installed.get('version')!r}, expected {dependency_version!r} from {tag!r}.") shared_peers = ("lucide-react", "react", "react-dom", "react-router-dom") root_peers = release_package.get("peerDependencies", {}) @@ -252,7 +275,8 @@ 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" + repo_tag="$(release_tag_for_package "$repo")" + git clone --depth 1 --branch "$repo_tag" "git@git.add-ideas.de:add-ideas/${repo}.git" "$WORK_ROOT/$repo" done run_step "Run core backend release tests"