Release v0.1.7

This commit is contained in:
2026-07-11 02:34:55 +02:00
parent 8c82d5b4f8
commit 24edb7eb8a
9 changed files with 385 additions and 23 deletions

View File

@@ -31,6 +31,7 @@ from govoplan_core.core.configuration_control import (
)
from govoplan_core.core.module_management import (
PROTECTED_MODULES,
ModuleInstallPlan,
ModuleInstallPlanItem,
ModuleManagementError,
configured_enabled_modules,
@@ -49,6 +50,7 @@ from govoplan_core.core.module_installer import (
default_installer_runtime_dir,
list_module_installer_runs,
list_module_installer_requests,
module_install_catalog_companion_module_ids,
module_installer_daemon_status,
module_install_preflight,
module_installer_lock_status,
@@ -429,17 +431,27 @@ def _webui_root() -> Path | None:
def _upsert_install_plan_item(session: Session, item: ModuleInstallPlanItem):
return _upsert_install_plan_items(session, [item])
def _upsert_install_plan_items(session: Session, items: list[ModuleInstallPlanItem]):
existing = saved_module_install_plan(session)
replacement_ids = {item.module_id for item in items if item.status == "planned"}
retained = [
current
for current in existing.items
if not (current.status == "planned" and current.module_id == item.module_id)
if not (current.status == "planned" and current.module_id in replacement_ids)
]
return save_module_install_plan(session, [*retained, item])
return save_module_install_plan(session, [*retained, *items])
def _catalog_plan_item(module_id: str, available_module_ids: set[str]) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
result = validate_module_package_catalog()
def _catalog_plan_item(
module_id: str,
available_module_ids: set[str],
*,
validation: dict[str, object] | None = None,
) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
result = validation or validate_module_package_catalog()
if not result.get("valid"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
@@ -801,7 +813,19 @@ def plan_module_install_from_catalog(
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
available = dict(lifecycle.available_modules) if isinstance(lifecycle, ModuleLifecycleManager) else available_module_manifests(ignore_load_errors=True)
item, validation = _catalog_plan_item(module_id, set(available))
plan = _upsert_install_plan_item(session, item)
existing = saved_module_install_plan(session)
retained = [
current
for current in existing.items
if not (current.status == "planned" and current.module_id == item.module_id)
]
candidate_plan = ModuleInstallPlan(items=tuple([*retained, item]))
companion_ids = module_install_catalog_companion_module_ids(candidate_plan, available, validation=validation)
companion_items = [
_catalog_plan_item(companion_id, set(available), validation=validation)[0]
for companion_id in companion_ids
]
plan = _upsert_install_plan_items(session, [item, *companion_items])
record_module_package_catalog_acceptance(validation)
except ModuleManagementError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
@@ -816,6 +840,7 @@ def plan_module_install_from_catalog(
module_id=module_id,
outcome="planned",
items=[item.as_dict() for item in plan.items],
companion_module_ids=list(companion_ids),
catalog_validation={
"valid": validation.get("valid"),
"channel": validation.get("channel"),
@@ -825,7 +850,10 @@ def plan_module_install_from_catalog(
)
session.commit()
planned_action = "update" if item.action == "update" else "install"
return _module_install_plan_response(session, request, notes=[f"Catalog {planned_action} entry planned for {module_id}."])
notes = [f"Catalog {planned_action} entry planned for {module_id}."]
if companion_ids:
notes.append("Required companion catalog entries added: " + ", ".join(companion_ids))
return _module_install_plan_response(session, request, notes=notes)
@router.post("/system/modules/{module_id}/uninstall-plan", response_model=ModuleInstallPlanResponse)

View File

@@ -182,6 +182,7 @@ class ModuleInstallPlanItem(BaseModel):
python_ref: str | None = Field(default=None, max_length=1000)
webui_package: str | None = Field(default=None, max_length=200)
webui_ref: str | None = Field(default=None, max_length=1000)
data_safety_acknowledged: bool = False
destroy_data: bool = False
status: Literal["planned", "applied", "blocked"] = "planned"
notes: str | None = Field(default=None, max_length=1000)
@@ -201,6 +202,63 @@ class ModuleInstallChecklistItem(BaseModel):
detail: str | None = None
class ModuleInstallTargetItem(BaseModel):
module_id: str
action: Literal["install", "update", "uninstall"]
source: Literal["manual", "catalog"]
current_version: str | None = None
target_version: str | None = None
python_package: str | None = None
python_ref: str | None = None
webui_package: str | None = None
webui_ref: str | None = None
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
migration_notes: str | None = None
current_version_min: str | None = None
current_version_max_exclusive: str | None = None
bridge_release: bool = False
bridge_notes: str | None = None
allow_downgrade: bool = False
allow_same_version: bool = False
recovery_tested: bool = False
recovery_notes: str | None = None
data_safety_acknowledged: bool = False
class ModuleMigrationPlanStep(BaseModel):
module_id: str
action: Literal["install", "update", "uninstall"]
phase: Literal["upgrade", "retirement"]
source: Literal["manifest", "catalog", "pending"]
has_migration_metadata: bool = False
metadata_pending: bool = False
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
current_version: str | None = None
target_version: str | None = None
reason: str | None = None
class ModuleMigrationTaskPlanItem(BaseModel):
module_id: str
task_id: str
phase: Literal["pre_migration_check", "pre_migration_prepare", "post_migration_backfill", "post_migration_verify"]
summary: str
task_version: str = "1"
safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
idempotent: bool = True
timeout_seconds: int | None = None
source: Literal["manifest", "catalog", "pending"] = "manifest"
has_executor: bool = False
metadata_pending: bool = False
class ModuleMigrationExecutionPlan(BaseModel):
enabled_modules: list[str] = Field(default_factory=list)
requires_database_migration: bool = False
steps: list[ModuleMigrationPlanStep] = Field(default_factory=list)
tasks: list[ModuleMigrationTaskPlanItem] = Field(default_factory=list)
class ModuleInstallPreflightResponse(BaseModel):
allowed: bool
maintenance_mode: bool
@@ -210,6 +268,8 @@ class ModuleInstallPreflightResponse(BaseModel):
rollback_commands: list[str] = Field(default_factory=list)
issues: list[ModuleInstallPreflightIssue] = Field(default_factory=list)
checklist: list[ModuleInstallChecklistItem] = Field(default_factory=list)
target_plan: list[ModuleInstallTargetItem] = Field(default_factory=list)
migration_plan: ModuleMigrationExecutionPlan = Field(default_factory=ModuleMigrationExecutionPlan)
class ModuleInstallPlanResponse(BaseModel):
@@ -328,6 +388,20 @@ class ModulePackageCatalogItem(BaseModel):
description: str | None = None
version: str | None = None
action: Literal["install", "update", "uninstall"] = "install"
dependencies: list[str] = Field(default_factory=list)
optional_dependencies: list[str] = Field(default_factory=list)
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
migration_notes: str | None = None
migration_after: list[str] = Field(default_factory=list)
migration_before: list[str] = Field(default_factory=list)
current_version_min: str | None = None
current_version_max_exclusive: str | None = None
bridge_release: bool = False
bridge_notes: str | None = None
allow_downgrade: bool = False
allow_same_version: bool = False
recovery_tested: bool = False
recovery_notes: str | None = None
python_package: str | None = None
python_ref: str | None = None
webui_package: str | None = None

View File

@@ -17,7 +17,7 @@ def _route_factory(context: ModuleContext):
manifest = ModuleManifest(
id="admin",
name="Admin",
version="0.1.6",
version="0.1.7",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
route_factory=_route_factory,
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),