Complete guided release console workflow
This commit is contained in:
+176
-13
@@ -9,6 +9,7 @@ import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import zipfile
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -17,6 +18,7 @@ if str(RELEASE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(RELEASE_ROOT))
|
||||
|
||||
from govoplan_release.model import RepositorySpec # noqa: E402
|
||||
from govoplan_release.artifact_identity import inspect_python_wheel # noqa: E402
|
||||
from govoplan_release.git_state import sanitized_git_environment # noqa: E402
|
||||
from govoplan_release.release_execution import ( # noqa: E402
|
||||
ExecutorSpec,
|
||||
@@ -30,6 +32,7 @@ from govoplan_release.release_execution import ( # noqa: E402
|
||||
require_trusted_release_runtime,
|
||||
verify_frozen_repository_tag_receipt,
|
||||
verify_repository_preflight_binding,
|
||||
verify_candidate_installation,
|
||||
_clone_catalog_sources,
|
||||
_checkout_frozen_source,
|
||||
_flatpak_proxy_environment,
|
||||
@@ -489,20 +492,180 @@ class ReleaseExecutionTests(unittest.TestCase):
|
||||
expected_receipt={"kind": "repository_state"},
|
||||
)
|
||||
|
||||
def test_commit_step_has_no_durable_executor(self) -> None:
|
||||
self.assertIsNone(
|
||||
executor_spec(
|
||||
{
|
||||
"id": "govoplan-files:commit",
|
||||
"status": "planned",
|
||||
"mutating": True,
|
||||
"repo": "govoplan-files",
|
||||
"source_binding": {
|
||||
"kind": "repository_state",
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_commit_step_has_narrow_confirmation(self) -> None:
|
||||
spec = executor_spec(
|
||||
{
|
||||
"id": "govoplan-files:commit",
|
||||
"status": "planned",
|
||||
"mutating": True,
|
||||
"repo": "govoplan-files",
|
||||
"source_binding": {
|
||||
"kind": "repository_state",
|
||||
},
|
||||
}
|
||||
)
|
||||
self.assertIsNotNone(spec)
|
||||
self.assertEqual("commit", spec.kind)
|
||||
self.assertEqual("COMMIT", spec.confirmation)
|
||||
|
||||
def test_version_and_commit_executors_bind_only_release_metadata(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
repository = workspace / "govoplan-files"
|
||||
remote = workspace / "files.git"
|
||||
_git(workspace, "init", "--bare", str(remote))
|
||||
_git(workspace, "init", "-b", "main", str(repository))
|
||||
_git(repository, "config", "user.name", "Release Test")
|
||||
_git(repository, "config", "user.email", "release@example.test")
|
||||
(repository / "pyproject.toml").write_text(
|
||||
'[project]\nname = "govoplan-files"\nversion = "1.2.3"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(repository, "add", "pyproject.toml")
|
||||
_git(repository, "commit", "-m", "Initial")
|
||||
_git(repository, "remote", "add", "origin", str(remote))
|
||||
_git(repository, "push", "-u", "origin", "main")
|
||||
repository_spec = _repository_spec(remote)
|
||||
plan_step = {
|
||||
"id": "govoplan-files:version",
|
||||
"repo": "govoplan-files",
|
||||
"source_binding": {"kind": "repository_state"},
|
||||
}
|
||||
with (
|
||||
patch(
|
||||
"govoplan_release.release_execution.load_repository_specs",
|
||||
return_value=(repository_spec,),
|
||||
),
|
||||
patch(
|
||||
"govoplan_release.release_execution.require_trusted_release_runtime"
|
||||
),
|
||||
):
|
||||
before = repository_state_receipt(
|
||||
repo="govoplan-files",
|
||||
target_tag="v1.2.4",
|
||||
workspace_root=workspace,
|
||||
)
|
||||
_result, updated = execute_repository_step(
|
||||
spec=ExecutorSpec(
|
||||
"version",
|
||||
"UPDATE",
|
||||
"version_metadata_updated",
|
||||
True,
|
||||
),
|
||||
plan_step=plan_step,
|
||||
repo_versions={"govoplan-files": "1.2.4"},
|
||||
workspace_root=workspace,
|
||||
remote="origin",
|
||||
expected_receipt=before,
|
||||
)
|
||||
self.assertFalse(updated["worktree_clean"])
|
||||
self.assertNotEqual(
|
||||
before["worktree_sha256"],
|
||||
updated["worktree_sha256"],
|
||||
)
|
||||
|
||||
plan_step["id"] = "govoplan-files:commit"
|
||||
_result, committed = execute_repository_step(
|
||||
spec=ExecutorSpec(
|
||||
"commit",
|
||||
"COMMIT",
|
||||
"release_commit_created",
|
||||
True,
|
||||
),
|
||||
plan_step=plan_step,
|
||||
repo_versions={"govoplan-files": "1.2.4"},
|
||||
workspace_root=workspace,
|
||||
remote="origin",
|
||||
expected_receipt=updated,
|
||||
)
|
||||
|
||||
self.assertTrue(committed["worktree_clean"])
|
||||
self.assertNotEqual(before["head"], committed["head"])
|
||||
self.assertIn(
|
||||
'version = "1.2.4"',
|
||||
(repository / "pyproject.toml").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
def test_candidate_installation_verifies_installed_metadata(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
repository = workspace / "govoplan-files"
|
||||
repository.mkdir()
|
||||
(repository / "pyproject.toml").write_text(
|
||||
'[project]\nname = "govoplan-files"\nversion = "1.2.4"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
artifacts = workspace / "candidate" / "artifacts"
|
||||
artifacts.mkdir(parents=True)
|
||||
wheel = artifacts / "govoplan_files-1.2.4-py3-none-any.whl"
|
||||
members = {
|
||||
"govoplan_files/__init__.py": '__version__ = "1.2.4"\n',
|
||||
"govoplan_files-1.2.4.dist-info/METADATA": (
|
||||
"Metadata-Version: 2.1\n"
|
||||
"Name: govoplan-files\n"
|
||||
"Version: 1.2.4\n"
|
||||
),
|
||||
"govoplan_files-1.2.4.dist-info/WHEEL": (
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: release-test\n"
|
||||
"Root-Is-Purelib: true\n"
|
||||
"Tag: py3-none-any\n"
|
||||
),
|
||||
}
|
||||
record = "\n".join(f"{name},," for name in members)
|
||||
record += "\ngovoplan_files-1.2.4.dist-info/RECORD,,\n"
|
||||
with zipfile.ZipFile(wheel, "w") as archive:
|
||||
for name, content in members.items():
|
||||
archive.writestr(name, content)
|
||||
archive.writestr(
|
||||
"govoplan_files-1.2.4.dist-info/RECORD",
|
||||
record,
|
||||
)
|
||||
identity = inspect_python_wheel(wheel)
|
||||
channels = workspace / "candidate" / "channels"
|
||||
channels.mkdir()
|
||||
(channels / "stable.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"release": {
|
||||
"selected_units": [
|
||||
{
|
||||
"repo": "govoplan-files",
|
||||
"version": "1.2.4",
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"package_name": "govoplan-files",
|
||||
"archive_sha256": identity.archive_sha256,
|
||||
}
|
||||
],
|
||||
},
|
||||
"core_release": {},
|
||||
"modules": [
|
||||
{
|
||||
"python_package": "govoplan-files",
|
||||
"python_ref": (
|
||||
"govoplan-files @ "
|
||||
"git+ssh://git@example.test/GovOPlaN/"
|
||||
"govoplan-files.git@v1.2.4"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = verify_candidate_installation(
|
||||
candidate_path=workspace / "candidate",
|
||||
repo_versions={"govoplan-files": "1.2.4"},
|
||||
)
|
||||
|
||||
self.assertEqual("verified", result["status"])
|
||||
self.assertEqual("govoplan-files", result["packages"][0]["repo"])
|
||||
self.assertEqual("1.2.4", result["packages"][0]["version"])
|
||||
|
||||
def test_frozen_checkout_uses_receipt_commit_not_live_worktree(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
@@ -47,6 +47,38 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
self.assertEqual(migration.depends_on, ("core",))
|
||||
self.assertEqual(migration.branch_labels, ())
|
||||
|
||||
def test_development_wrapper_uses_release_revision_metadata(self) -> None:
|
||||
audit = load_audit_module()
|
||||
with tempfile.TemporaryDirectory(prefix="migration-audit-test-") as directory:
|
||||
root = Path(directory)
|
||||
versions = root / "versions"
|
||||
development = root / "dev_versions"
|
||||
versions.mkdir()
|
||||
development.mkdir()
|
||||
release = versions / "1234_example.py"
|
||||
release.write_text(
|
||||
'revision = "1234"\n'
|
||||
'down_revision = "base"\n'
|
||||
'depends_on = "core"\n'
|
||||
"branch_labels = None\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
wrapper = development / release.name
|
||||
wrapper.write_text(
|
||||
"revision = _migration.revision\n"
|
||||
"down_revision = _migration.down_revision\n"
|
||||
"depends_on = _migration.depends_on\n"
|
||||
"branch_labels = _migration.branch_labels\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
migration = audit.parse_migration_file("govoplan-core", wrapper)
|
||||
|
||||
self.assertIsNotNone(migration)
|
||||
self.assertEqual("1234", migration.revision)
|
||||
self.assertEqual(("base",), migration.down_revisions)
|
||||
self.assertEqual(("core",), migration.depends_on)
|
||||
|
||||
def test_release_baseline_matches_current_heads_in_strict_report(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
|
||||
@@ -23,7 +23,7 @@ from govoplan_release.selective_planner import build_selective_release_plan # n
|
||||
|
||||
|
||||
class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||
def test_unprepared_target_has_structured_remediation_and_recommendation(
|
||||
def test_unprepared_target_gets_bounded_version_and_commit_steps(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
@@ -40,20 +40,25 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||
repo_versions={"govoplan-files": "1.2.4"},
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", plan.status)
|
||||
self.assertFalse(plan.source_preflight_ready)
|
||||
finding = next(
|
||||
item
|
||||
for item in plan.gate_findings
|
||||
if item.code == "repository_version_alignment"
|
||||
self.assertEqual("attention", plan.status)
|
||||
self.assertTrue(plan.source_preflight_ready)
|
||||
self.assertFalse(
|
||||
any(
|
||||
item.code == "repository_version_alignment"
|
||||
for item in plan.gate_findings
|
||||
)
|
||||
)
|
||||
self.assertEqual("govoplan-files", finding.repo)
|
||||
self.assertEqual("pyproject.toml", finding.source)
|
||||
self.assertEqual("1.2.4", finding.expected)
|
||||
self.assertEqual("1.2.3", finding.actual)
|
||||
self.assertIn("Regenerate lockfiles", finding.remediation)
|
||||
self.assertEqual("resolve_release_gate", plan.recommended_action.id)
|
||||
self.assertEqual("govoplan-files", plan.recommended_action.repo)
|
||||
steps = {step.id: step for step in plan.dry_run_steps}
|
||||
self.assertEqual("planned", steps["govoplan-files:version"].status)
|
||||
self.assertEqual("planned", steps["govoplan-files:commit"].status)
|
||||
self.assertIn("pyproject.toml", steps["govoplan-files:version"].detail)
|
||||
self.assertTrue(
|
||||
any(
|
||||
"version metadata will be updated" in warning
|
||||
for warning in plan.units[0].warnings
|
||||
)
|
||||
)
|
||||
self.assertIn("python-package", plan.units[0].capabilities)
|
||||
|
||||
def test_aligned_target_recommends_non_mutating_tag_preview(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
@@ -74,6 +79,91 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||
self.assertEqual("preview_source_release", plan.recommended_action.id)
|
||||
self.assertIn("Preview Tag + Publish", plan.recommended_action.remediation)
|
||||
|
||||
def test_mixed_plan_tags_modules_before_core_and_aligns_before_push(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
for repo_name in ("govoplan-core", "govoplan-files"):
|
||||
repo_path = workspace / repo_name
|
||||
repo_path.mkdir()
|
||||
(repo_path / "pyproject.toml").write_text(
|
||||
f'[project]\nname = "{repo_name}"\nversion = "1.2.3"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
repositories = tuple(
|
||||
RepositorySnapshot(
|
||||
spec=RepositorySpec(
|
||||
name=repo_name,
|
||||
category="core" if repo_name == "govoplan-core" else "module",
|
||||
subtype="platform" if repo_name == "govoplan-core" else "infrastructure",
|
||||
remote=f"git@example.test:GovOPlaN/{repo_name}.git",
|
||||
path=repo_name,
|
||||
),
|
||||
absolute_path=str(workspace / repo_name),
|
||||
exists=True,
|
||||
is_git=True,
|
||||
has_head=True,
|
||||
branch="main",
|
||||
versions=VersionSnapshot(pyproject="1.2.3"),
|
||||
local_target_tag_exists=False,
|
||||
)
|
||||
for repo_name in ("govoplan-core", "govoplan-files")
|
||||
)
|
||||
base = dashboard(workspace=workspace, version="1.2.3")
|
||||
mixed = ReleaseDashboard(
|
||||
generated_at=base.generated_at,
|
||||
meta_root=base.meta_root,
|
||||
workspace_root=base.workspace_root,
|
||||
target_version=None,
|
||||
target_tag=None,
|
||||
online=False,
|
||||
include_migrations=False,
|
||||
summary=DashboardSummary(
|
||||
repository_count=2,
|
||||
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=repositories,
|
||||
catalog=base.catalog,
|
||||
)
|
||||
plan = build_selective_release_plan(
|
||||
mixed,
|
||||
selected_repos=("govoplan-core", "govoplan-files"),
|
||||
repo_versions={
|
||||
"govoplan-core": "1.2.3",
|
||||
"govoplan-files": "1.2.3",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
["govoplan-files", "govoplan-core"],
|
||||
[unit.repo for unit in plan.units],
|
||||
)
|
||||
step_ids = [step.id for step in plan.dry_run_steps]
|
||||
self.assertLess(
|
||||
step_ids.index("govoplan-files:tag"),
|
||||
step_ids.index("govoplan-core:tag"),
|
||||
)
|
||||
self.assertLess(
|
||||
step_ids.index("govoplan-core:tag"),
|
||||
step_ids.index("release:alignment"),
|
||||
)
|
||||
self.assertLess(
|
||||
step_ids.index("release:alignment"),
|
||||
step_ids.index("govoplan-files:push"),
|
||||
)
|
||||
self.assertEqual("release:install-verify", step_ids[-1])
|
||||
core = next(unit for unit in plan.units if unit.repo == "govoplan-core")
|
||||
self.assertIn("core-release-bundle", core.capabilities)
|
||||
|
||||
def test_malformed_version_metadata_is_a_structured_blocker(self) -> None:
|
||||
cases = (
|
||||
("package.json", "{not-json\n", "JSONDecodeError"),
|
||||
@@ -167,7 +257,8 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||
self.assertIn("data-run-step-id", webui)
|
||||
self.assertIn("invalidateReleaseDraft", webui)
|
||||
self.assertIn("Installation Verification", webui)
|
||||
self.assertIn("external release-integration CI gate", webui)
|
||||
self.assertIn("receipt-bound local gate", webui)
|
||||
self.assertIn('step.id === "release:install-verify"', webui)
|
||||
self.assertIn('const inspectionNotices = (summary.missing_count || 0)', webui)
|
||||
self.assertIn('(summary.repository_count || 0) === 0', webui)
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
RELEASE_ROOT = META_ROOT / "tools" / "release"
|
||||
if str(RELEASE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(RELEASE_ROOT))
|
||||
|
||||
from govoplan_release.version_metadata import ( # noqa: E402
|
||||
apply_version_metadata_mutations,
|
||||
version_metadata_mutations,
|
||||
)
|
||||
|
||||
|
||||
class ReleaseVersionMetadataTests(unittest.TestCase):
|
||||
def test_updates_recognized_metadata_without_changing_interface_versions(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
package = root / "src" / "govoplan_example"
|
||||
backend = package / "backend"
|
||||
webui = root / "webui"
|
||||
backend.mkdir(parents=True)
|
||||
webui.mkdir()
|
||||
(root / "pyproject.toml").write_text(
|
||||
'[project]\nname = "govoplan-example"\nversion = "1.2.3"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "package.json").write_text(
|
||||
'{"name":"root","version":"1.2.3"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(webui / "package.json").write_text(
|
||||
'{"name":"@govoplan/example-webui","version":"1.2.3"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(webui / "package-lock.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "@govoplan/example-webui",
|
||||
"version": "1.2.3",
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/example-webui",
|
||||
"version": "1.2.3",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(backend / "manifest.py").write_text(
|
||||
"def get_manifest():\n"
|
||||
" return ModuleManifest(\n"
|
||||
' id="example",\n'
|
||||
' version="1.2.3",\n'
|
||||
" provides_interfaces=(\n"
|
||||
' ModuleInterfaceProvider(name="example.api", version="4.0"),\n'
|
||||
" ),\n"
|
||||
" )\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package / "__init__.py").write_text(
|
||||
'__version__ = "1.2.3"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
preview = version_metadata_mutations(
|
||||
root,
|
||||
target_version="1.2.4",
|
||||
)
|
||||
changed = apply_version_metadata_mutations(
|
||||
root,
|
||||
target_version="1.2.4",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{mutation.path for mutation in preview},
|
||||
set(changed),
|
||||
)
|
||||
self.assertEqual(
|
||||
"1.2.4",
|
||||
json.loads((webui / "package-lock.json").read_text())[
|
||||
"packages"
|
||||
][""]["version"],
|
||||
)
|
||||
manifest = (backend / "manifest.py").read_text(encoding="utf-8")
|
||||
self.assertIn('version="1.2.4"', manifest)
|
||||
self.assertIn('version="4.0"', manifest)
|
||||
self.assertEqual(
|
||||
'__version__ = "1.2.4"\n',
|
||||
(package / "__init__.py").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user