396 lines
17 KiB
Python
396 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
META_ROOT = Path(__file__).resolve().parents[1]
|
|
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
|
if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
|
|
|
|
from govoplan_release.version_alignment import ( # noqa: E402
|
|
candidate_catalog_version_issues,
|
|
release_composition_issues,
|
|
selected_release_webui_bundle_issues,
|
|
repository_version_issues,
|
|
selected_repository_version_issues,
|
|
)
|
|
from govoplan_release.model import ( # noqa: E402
|
|
CatalogSnapshot,
|
|
DashboardSummary,
|
|
ReleaseDashboard,
|
|
RepositorySnapshot,
|
|
RepositorySpec,
|
|
VersionSnapshot,
|
|
)
|
|
from govoplan_release.selective_planner import build_selective_release_plan, build_unit # noqa: E402
|
|
from govoplan_release.selective_catalog import enforce_selected_version_alignment # noqa: E402
|
|
|
|
|
|
class VersionAlignmentTests(unittest.TestCase):
|
|
def test_candidate_catalog_refs_and_selected_units_must_match_versions(self) -> None:
|
|
payload = {
|
|
"core_release": {
|
|
"version": "1.2.3",
|
|
"python_ref": "govoplan-core @ git+ssh://git@example.test/acme/govoplan-core.git@v1.2.3",
|
|
},
|
|
"modules": [
|
|
{
|
|
"module_id": "files",
|
|
"version": "1.2.3",
|
|
"python_ref": "govoplan-files @ git+ssh://git@example.test/acme/govoplan-files.git@v1.2.4",
|
|
"webui_package": "@govoplan/files-webui",
|
|
"webui_ref": "git+ssh://git@example.test/acme/govoplan-files.git#v1.2.3",
|
|
}
|
|
],
|
|
"release": {
|
|
"selected_units": [
|
|
{"repo": "govoplan-files", "version": "1.2.5", "tag": "v1.2.6"},
|
|
]
|
|
},
|
|
}
|
|
|
|
issues = candidate_catalog_version_issues(payload)
|
|
|
|
messages = {issue.message for issue in issues}
|
|
self.assertIn("Python ref tag must match the catalog entry version", messages)
|
|
self.assertIn("selected-unit tag must match its version", messages)
|
|
self.assertIn("selected-unit version must match the catalog entry", messages)
|
|
|
|
def test_catalog_candidate_gate_rejects_selected_repo_drift(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
repo = workspace / "govoplan-files"
|
|
(repo / "webui").mkdir(parents=True)
|
|
(repo / "pyproject.toml").write_text('[project]\nname="govoplan-files"\nversion="1.2.3"\n')
|
|
(repo / "webui" / "package.json").write_text('{"version":"1.2.4"}\n')
|
|
|
|
with self.assertRaisesRegex(ValueError, "Version alignment gate failed"):
|
|
enforce_selected_version_alignment(
|
|
repo_versions={"govoplan-files": "1.2.5"},
|
|
workspace=workspace,
|
|
)
|
|
|
|
def test_catalog_candidate_gate_rejects_requested_version_that_differs_from_source(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
repo = workspace / "govoplan-files"
|
|
(repo / "webui").mkdir(parents=True)
|
|
(repo / "pyproject.toml").write_text('[project]\nname="govoplan-files"\nversion="1.2.3"\n')
|
|
(repo / "webui" / "package.json").write_text('{"version":"1.2.3"}\n')
|
|
|
|
with self.assertRaisesRegex(ValueError, "source version must match the requested release version"):
|
|
enforce_selected_version_alignment(
|
|
repo_versions={"govoplan-files": "1.2.4"},
|
|
workspace=workspace,
|
|
)
|
|
|
|
def test_selected_webui_release_must_match_core_bundle_input_and_lock(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
core_webui = workspace / "govoplan-core" / "webui"
|
|
campaign_webui = workspace / "govoplan-campaign" / "webui"
|
|
core_webui.mkdir(parents=True)
|
|
campaign_webui.mkdir(parents=True)
|
|
dependency = "@govoplan/campaign-webui"
|
|
old_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.2"
|
|
(campaign_webui / "package.json").write_text(
|
|
json.dumps({"name": dependency, "version": "1.2.3"})
|
|
)
|
|
(core_webui / "package.release.json").write_text(
|
|
json.dumps({"dependencies": {dependency: old_ref}})
|
|
)
|
|
(core_webui / "package-lock.release.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"packages": {
|
|
"": {"dependencies": {dependency: old_ref}},
|
|
f"node_modules/{dependency}": {"version": "1.2.2"},
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
issues = selected_release_webui_bundle_issues(
|
|
repo_versions={"govoplan-core": "2.0.0", "govoplan-campaign": "1.2.3"},
|
|
workspace=workspace,
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "Core release WebUI dependency"):
|
|
enforce_selected_version_alignment(
|
|
repo_versions={"govoplan-campaign": "1.2.3"},
|
|
workspace=workspace,
|
|
)
|
|
|
|
self.assertEqual(2, len(issues))
|
|
self.assertTrue(all(issue.repo == "govoplan-campaign" for issue in issues))
|
|
self.assertEqual(
|
|
{
|
|
"Core release WebUI dependency must match the selected module version",
|
|
"Core release lock must resolve the selected module version",
|
|
},
|
|
{issue.message for issue in issues},
|
|
)
|
|
|
|
def test_selected_webui_gate_ignores_unselected_module_pins(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
core_webui = workspace / "govoplan-core" / "webui"
|
|
campaign_webui = workspace / "govoplan-campaign" / "webui"
|
|
core_webui.mkdir(parents=True)
|
|
campaign_webui.mkdir(parents=True)
|
|
campaign_package = "@govoplan/campaign-webui"
|
|
files_package = "@govoplan/files-webui"
|
|
campaign_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.3"
|
|
files_ref = "git+ssh://git@example.test/acme/govoplan-files.git#v0.9.0"
|
|
(campaign_webui / "package.json").write_text(
|
|
json.dumps({"name": campaign_package, "version": "1.2.3"})
|
|
)
|
|
dependencies = {campaign_package: campaign_ref, files_package: files_ref}
|
|
(core_webui / "package.release.json").write_text(json.dumps({"dependencies": dependencies}))
|
|
(core_webui / "package-lock.release.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"packages": {
|
|
"": {"dependencies": dependencies},
|
|
f"node_modules/{campaign_package}": {"version": "1.2.3"},
|
|
f"node_modules/{files_package}": {"version": "0.9.0"},
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
issues = selected_release_webui_bundle_issues(
|
|
repo_versions={"govoplan-campaign": "1.2.3"},
|
|
workspace=workspace,
|
|
)
|
|
|
|
self.assertEqual((), issues)
|
|
|
|
def test_selective_plan_blocks_mismatched_selected_webui_bundle(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
core_webui = workspace / "govoplan-core" / "webui"
|
|
campaign_webui = workspace / "govoplan-campaign" / "webui"
|
|
core_webui.mkdir(parents=True)
|
|
campaign_webui.mkdir(parents=True)
|
|
package_name = "@govoplan/campaign-webui"
|
|
old_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.2"
|
|
(campaign_webui / "package.json").write_text(
|
|
json.dumps({"name": package_name, "version": "1.2.3"})
|
|
)
|
|
(core_webui / "package.release.json").write_text(
|
|
json.dumps({"dependencies": {package_name: old_ref}})
|
|
)
|
|
(core_webui / "package-lock.release.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"packages": {
|
|
"": {"dependencies": {package_name: old_ref}},
|
|
f"node_modules/{package_name}": {"version": "1.2.2"},
|
|
}
|
|
}
|
|
)
|
|
)
|
|
campaign = RepositorySnapshot(
|
|
spec=RepositorySpec(
|
|
name="govoplan-campaign",
|
|
category="module",
|
|
subtype="domain",
|
|
remote="git@example.test:acme/govoplan-campaign.git",
|
|
path="govoplan-campaign",
|
|
),
|
|
absolute_path=str(workspace / "govoplan-campaign"),
|
|
exists=True,
|
|
is_git=True,
|
|
has_head=True,
|
|
branch="main",
|
|
versions=VersionSnapshot(pyproject="1.2.3", webui_package="1.2.3"),
|
|
)
|
|
dashboard = ReleaseDashboard(
|
|
generated_at="2026-01-01T00:00:00Z",
|
|
meta_root=str(workspace / "govoplan"),
|
|
workspace_root=str(workspace),
|
|
target_version=None,
|
|
target_tag=None,
|
|
online=False,
|
|
include_migrations=False,
|
|
summary=DashboardSummary(
|
|
repository_count=1,
|
|
missing_count=0,
|
|
dirty_count=0,
|
|
ahead_count=0,
|
|
behind_count=0,
|
|
no_head_count=0,
|
|
error_count=0,
|
|
safe_directory_count=0,
|
|
local_target_tag_missing_count=0,
|
|
status="ready",
|
|
),
|
|
repositories=(campaign,),
|
|
catalog=CatalogSnapshot(
|
|
channel="stable",
|
|
website_path="",
|
|
catalog_path="",
|
|
catalog_exists=False,
|
|
keyring_path="",
|
|
keyring_exists=False,
|
|
),
|
|
)
|
|
|
|
plan = build_selective_release_plan(
|
|
dashboard,
|
|
selected_repos=("govoplan-campaign",),
|
|
repo_versions={"govoplan-campaign": "1.2.3"},
|
|
)
|
|
|
|
self.assertEqual("blocked", plan.status)
|
|
self.assertEqual("blocked", plan.units[0].status)
|
|
self.assertTrue(any("Core release WebUI dependency" in item for item in plan.units[0].blockers))
|
|
|
|
def test_selected_repository_gate_reports_missing_version_metadata(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
(workspace / "govoplan-files").mkdir()
|
|
|
|
issues = selected_repository_version_issues(
|
|
repo_versions={"govoplan-files": "1.2.3"},
|
|
workspace=workspace,
|
|
)
|
|
|
|
self.assertEqual(1, len(issues))
|
|
self.assertEqual("repository has no version metadata", issues[0].message)
|
|
|
|
def test_selective_release_unit_is_blocked_by_version_drift(self) -> None:
|
|
repo = RepositorySnapshot(
|
|
spec=RepositorySpec(
|
|
name="govoplan-example",
|
|
category="module",
|
|
subtype="domain",
|
|
remote="git@example.test:acme/govoplan-example.git",
|
|
path="govoplan-example",
|
|
),
|
|
absolute_path="/tmp/govoplan-example",
|
|
exists=True,
|
|
is_git=True,
|
|
has_head=True,
|
|
versions=VersionSnapshot(pyproject="1.2.3", webui_package="1.2.4", manifests=("1.2.3",)),
|
|
)
|
|
|
|
unit = build_unit(repo, target_version="1.2.5", contracts=None)
|
|
|
|
self.assertEqual("blocked", unit.status)
|
|
self.assertIn("version metadata is not aligned", unit.blockers[0])
|
|
|
|
def test_repository_versions_and_lockfiles_must_match(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp) / "govoplan-example"
|
|
(root / "src" / "govoplan_example" / "backend").mkdir(parents=True)
|
|
(root / "webui").mkdir()
|
|
(root / "pyproject.toml").write_text('[project]\nname = "govoplan-example"\nversion = "1.2.3"\n')
|
|
(root / "package.json").write_text('{"name":"@govoplan/example","version":"1.2.3"}\n')
|
|
(root / "webui" / "package.json").write_text('{"name":"@govoplan/example-webui","version":"1.2.4"}\n')
|
|
(root / "webui" / "package-lock.json").write_text(
|
|
json.dumps({"packages": {"": {"version": "1.2.2"}}})
|
|
)
|
|
(root / "src" / "govoplan_example" / "backend" / "manifest.py").write_text(
|
|
'manifest = ModuleManifest(\n version="1.2.3",\n)\n'
|
|
)
|
|
|
|
issues = repository_version_issues(root)
|
|
|
|
self.assertEqual(2, len(issues))
|
|
self.assertEqual({"webui/package.json", "webui/package-lock.json"}, {issue.source for issue in issues})
|
|
|
|
def test_public_runtime_version_must_match_package_metadata(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp) / "govoplan-example"
|
|
package = root / "src" / "govoplan_example"
|
|
package.mkdir(parents=True)
|
|
(root / "pyproject.toml").write_text(
|
|
'[project]\nname = "govoplan-example"\nversion = "1.2.3"\n'
|
|
)
|
|
(package / "__init__.py").write_text('__version__ = "1.2.2"\n')
|
|
|
|
issues = repository_version_issues(root)
|
|
|
|
self.assertEqual(1, len(issues))
|
|
self.assertEqual("package __init__.py 1", issues[0].source)
|
|
self.assertEqual("1.2.3", issues[0].expected)
|
|
self.assertEqual("1.2.2", issues[0].actual)
|
|
|
|
def test_release_backend_and_frontend_refs_must_match(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
meta = Path(tmp) / "govoplan"
|
|
core = Path(tmp) / "govoplan-core"
|
|
meta.mkdir()
|
|
(core / "webui").mkdir(parents=True)
|
|
(meta / "requirements-release.txt").write_text(
|
|
"govoplan-example @ git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3\n"
|
|
)
|
|
(core / "webui" / "package.release.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"dependencies": {
|
|
"@govoplan/example-webui": (
|
|
"git+ssh://git@example.test/acme/govoplan-example.git#v1.2.4"
|
|
)
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
issues = release_composition_issues(meta, core_root=core)
|
|
|
|
self.assertEqual(1, len(issues))
|
|
self.assertEqual("1.2.3", issues[0].expected)
|
|
self.assertEqual("1.2.4", issues[0].actual)
|
|
|
|
def test_release_refs_must_point_at_installable_versioned_artifacts(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
workspace = Path(tmp)
|
|
meta = workspace / "govoplan"
|
|
core = workspace / "govoplan-core"
|
|
module = workspace / "govoplan-example"
|
|
meta.mkdir()
|
|
(core / "webui").mkdir(parents=True)
|
|
module.mkdir()
|
|
(meta / "requirements-release.txt").write_text(
|
|
"govoplan-example @ git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3\n"
|
|
)
|
|
(core / "webui" / "package.release.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"dependencies": {
|
|
"@govoplan/example-webui": (
|
|
"git+ssh://git@example.test/acme/govoplan-example.git#v1.2.3"
|
|
)
|
|
}
|
|
}
|
|
)
|
|
)
|
|
subprocess.run(["git", "init", "-q", str(module)], check=True)
|
|
subprocess.run(["git", "-C", str(module), "config", "user.email", "test@example.test"], check=True)
|
|
subprocess.run(["git", "-C", str(module), "config", "user.name", "Test"], check=True)
|
|
(module / "README.md").write_text("uninstallable release\n")
|
|
subprocess.run(["git", "-C", str(module), "add", "README.md"], check=True)
|
|
subprocess.run(["git", "-C", str(module), "commit", "-qm", "release"], check=True)
|
|
subprocess.run(["git", "-C", str(module), "tag", "v1.2.3"], check=True)
|
|
|
|
issues = release_composition_issues(meta, core_root=core)
|
|
|
|
self.assertEqual(2, len(issues))
|
|
self.assertEqual(
|
|
{"v1.2.3:pyproject.toml", "v1.2.3:webui/package.json"},
|
|
{issue.source for issue in issues},
|
|
)
|
|
self.assertTrue(all("must contain" in issue.message for issue in issues))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|