Block mismatched selective WebUI releases
This commit is contained in:
@@ -12,7 +12,7 @@ import sys
|
||||
from .git_state import collect_repository_snapshot, git_text
|
||||
from .model import RepositorySnapshot
|
||||
from .repository_push import command_text, compact_output
|
||||
from .version_alignment import repository_version_issues
|
||||
from .version_alignment import repository_version_issues, selected_release_webui_bundle_issues
|
||||
from .workspace import META_ROOT, load_repository_specs, resolve_repo_path, resolve_workspace_root
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ def tag_repositories(
|
||||
specs = {spec.name: spec for spec in load_repository_specs(include_website=False)}
|
||||
selected = tuple(dict.fromkeys(repos))
|
||||
results: list[dict[str, object]] = []
|
||||
bundle_issues_by_repo: dict[str, list[str]] = {}
|
||||
for issue in selected_release_webui_bundle_issues(
|
||||
repo_versions={repo: repo_versions.get(repo, "") for repo in selected},
|
||||
workspace=workspace,
|
||||
):
|
||||
bundle_issues_by_repo.setdefault(issue.repo, []).append(
|
||||
f"{issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
)
|
||||
|
||||
if apply:
|
||||
preflight = tag_repositories(
|
||||
@@ -147,6 +155,16 @@ def tag_repositories(
|
||||
)
|
||||
results.append({**row, "status": "blocked", "detail": f"version alignment gate failed: {detail}"})
|
||||
continue
|
||||
bundle_alignment = bundle_issues_by_repo.get(repo, ())
|
||||
if bundle_alignment:
|
||||
results.append(
|
||||
{
|
||||
**row,
|
||||
"status": "blocked",
|
||||
"detail": "release WebUI composition gate failed: " + "; ".join(bundle_alignment),
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
head_commit = git_text(path, "rev-parse", "HEAD")
|
||||
local_commit = ref_commit(path, f"refs/tags/{tag}")
|
||||
|
||||
@@ -28,7 +28,10 @@ from .source_provenance import (
|
||||
selected_source_provenance,
|
||||
source_tag_provenance_issues,
|
||||
)
|
||||
from .version_alignment import selected_repository_version_issues
|
||||
from .version_alignment import (
|
||||
selected_release_webui_bundle_issues,
|
||||
selected_repository_version_issues,
|
||||
)
|
||||
from .workspace import load_repository_specs, resolve_workspace_root, website_root
|
||||
|
||||
GITEA_BASE = "git+ssh://git@git.add-ideas.de/add-ideas"
|
||||
@@ -197,12 +200,19 @@ def build_selective_catalog_candidate(
|
||||
|
||||
|
||||
def enforce_selected_version_alignment(*, repo_versions: dict[str, str], workspace: Path) -> None:
|
||||
failures = [
|
||||
f"{issue.repo}: {issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
for issue in selected_repository_version_issues(
|
||||
issues = (
|
||||
*selected_repository_version_issues(
|
||||
repo_versions=repo_versions,
|
||||
workspace=workspace,
|
||||
)
|
||||
),
|
||||
*selected_release_webui_bundle_issues(
|
||||
repo_versions=repo_versions,
|
||||
workspace=workspace,
|
||||
),
|
||||
)
|
||||
failures = [
|
||||
f"{issue.repo}: {issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
for issue in issues
|
||||
]
|
||||
if failures:
|
||||
raise ValueError("Version alignment gate failed: " + "; ".join(failures))
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
|
||||
from .contracts import validate_contracts
|
||||
@@ -17,6 +19,7 @@ from .model import (
|
||||
SelectiveReleasePlan,
|
||||
ModuleContractSnapshot,
|
||||
)
|
||||
from .version_alignment import selected_release_webui_bundle_issues
|
||||
|
||||
|
||||
def build_selective_release_plan(
|
||||
@@ -34,6 +37,7 @@ def build_selective_release_plan(
|
||||
build_unit(repo, target_version=repo_versions.get(repo.spec.name) or target_version, contracts=contracts_by_repo.get(repo.spec.name))
|
||||
for repo in repositories
|
||||
)
|
||||
units = apply_release_webui_bundle_gate(units, workspace=Path(dashboard.workspace_root))
|
||||
compatibility = compatibility_issues(dashboard)
|
||||
steps = dry_run_steps(units=units, dashboard=dashboard, channel=channel)
|
||||
notes = release_notes(dashboard)
|
||||
@@ -49,6 +53,31 @@ def build_selective_release_plan(
|
||||
)
|
||||
|
||||
|
||||
def apply_release_webui_bundle_gate(
|
||||
units: tuple[ReleasePlanUnit, ...],
|
||||
*,
|
||||
workspace: Path,
|
||||
) -> tuple[ReleasePlanUnit, ...]:
|
||||
issues_by_repo: dict[str, list[str]] = {}
|
||||
for issue in selected_release_webui_bundle_issues(
|
||||
repo_versions={unit.repo: unit.target_version for unit in units},
|
||||
workspace=workspace,
|
||||
):
|
||||
issues_by_repo.setdefault(issue.repo, []).append(
|
||||
f"{issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
)
|
||||
return tuple(
|
||||
replace(
|
||||
unit,
|
||||
status="blocked",
|
||||
blockers=(*unit.blockers, *issues_by_repo[unit.repo]),
|
||||
)
|
||||
if unit.repo in issues_by_repo
|
||||
else unit
|
||||
for unit in units
|
||||
)
|
||||
|
||||
|
||||
def selected_repositories(dashboard: ReleaseDashboard, *, selected_repos: tuple[str, ...]) -> tuple[RepositorySnapshot, ...]:
|
||||
if selected_repos:
|
||||
wanted = set(selected_repos)
|
||||
|
||||
@@ -164,6 +164,121 @@ def selected_repository_version_issues(
|
||||
return tuple(issues)
|
||||
|
||||
|
||||
def selected_release_webui_bundle_issues(
|
||||
*,
|
||||
repo_versions: dict[str, str],
|
||||
workspace: Path,
|
||||
) -> tuple[VersionAlignmentIssue, ...]:
|
||||
"""Require selected WebUI modules to match Core's pinned release bundle.
|
||||
|
||||
Selective releases leave unselected module pins untouched. A selected module
|
||||
that exposes a WebUI package, however, must be the version that Core's
|
||||
immutable release package input and lockfile will actually install.
|
||||
"""
|
||||
|
||||
core_webui = workspace / "govoplan-core" / "webui"
|
||||
release_package_path = core_webui / "package.release.json"
|
||||
release_lock_path = core_webui / "package-lock.release.json"
|
||||
release_package = _json_object(release_package_path) if release_package_path.exists() else {}
|
||||
release_dependencies = release_package.get("dependencies")
|
||||
if not isinstance(release_dependencies, dict):
|
||||
release_dependencies = {}
|
||||
|
||||
release_lock = _json_object(release_lock_path) if release_lock_path.exists() else {}
|
||||
lock_packages = release_lock.get("packages")
|
||||
if not isinstance(lock_packages, dict):
|
||||
lock_packages = {}
|
||||
lock_root = lock_packages.get("")
|
||||
lock_root_dependencies = lock_root.get("dependencies") if isinstance(lock_root, dict) else None
|
||||
if not isinstance(lock_root_dependencies, dict):
|
||||
lock_root_dependencies = {}
|
||||
|
||||
issues: list[VersionAlignmentIssue] = []
|
||||
for repo, requested_version in sorted(repo_versions.items()):
|
||||
if repo == "govoplan-core":
|
||||
continue
|
||||
module_package_path = workspace / repo / "webui" / "package.json"
|
||||
if not module_package_path.exists():
|
||||
continue
|
||||
module_package = _json_object(module_package_path)
|
||||
package_name = module_package.get("name")
|
||||
if not isinstance(package_name, str) or not package_name:
|
||||
issues.append(
|
||||
VersionAlignmentIssue(
|
||||
repo=repo,
|
||||
source="webui/package.json:name",
|
||||
expected="WebUI package name",
|
||||
actual="",
|
||||
message="selected WebUI module must declare its package name",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
version = requested_version.removeprefix("v")
|
||||
release_ref = release_dependencies.get(package_name)
|
||||
release_match = _CATALOG_WEBUI_REF.search(release_ref) if isinstance(release_ref, str) else None
|
||||
actual_release = (
|
||||
f"{release_match.group('repo')}@v{release_match.group('version').removeprefix('v')}"
|
||||
if release_match is not None
|
||||
else str(release_ref or "")
|
||||
)
|
||||
expected_release = f"{repo}@v{version}"
|
||||
if (
|
||||
release_match is None
|
||||
or release_match.group("repo") != repo
|
||||
or release_match.group("version").removeprefix("v") != version
|
||||
):
|
||||
issues.append(
|
||||
VersionAlignmentIssue(
|
||||
repo=repo,
|
||||
source=f"govoplan-core/webui/package.release.json:dependencies.{package_name}",
|
||||
expected=expected_release,
|
||||
actual=actual_release,
|
||||
message="Core release WebUI dependency must match the selected module version",
|
||||
)
|
||||
)
|
||||
|
||||
locked_root_ref = lock_root_dependencies.get(package_name)
|
||||
if locked_root_ref != release_ref:
|
||||
issues.append(
|
||||
VersionAlignmentIssue(
|
||||
repo=repo,
|
||||
source=f"govoplan-core/webui/package-lock.release.json:dependencies.{package_name}",
|
||||
expected=str(release_ref or ""),
|
||||
actual=str(locked_root_ref or ""),
|
||||
message="Core release lock root dependency must match package.release.json",
|
||||
)
|
||||
)
|
||||
|
||||
locked_package = lock_packages.get(f"node_modules/{package_name}")
|
||||
locked_version = locked_package.get("version") if isinstance(locked_package, dict) else None
|
||||
if locked_version != version:
|
||||
issues.append(
|
||||
VersionAlignmentIssue(
|
||||
repo=repo,
|
||||
source=f"govoplan-core/webui/package-lock.release.json:{package_name}",
|
||||
expected=version,
|
||||
actual=str(locked_version or ""),
|
||||
message="Core release lock must resolve the selected module version",
|
||||
)
|
||||
)
|
||||
|
||||
expected_commit = _git_tag_commit(workspace / repo, f"v{version}")
|
||||
resolved = locked_package.get("resolved") if isinstance(locked_package, dict) else None
|
||||
resolved_commit = resolved.rsplit("#", 1)[-1] if isinstance(resolved, str) and "#" in resolved else None
|
||||
if expected_commit is not None and resolved_commit != expected_commit:
|
||||
issues.append(
|
||||
VersionAlignmentIssue(
|
||||
repo=repo,
|
||||
source=f"govoplan-core/webui/package-lock.release.json:{package_name}:resolved",
|
||||
expected=expected_commit,
|
||||
actual=resolved_commit or "",
|
||||
message="Core release lock commit must match the selected module tag",
|
||||
)
|
||||
)
|
||||
return tuple(issues)
|
||||
|
||||
|
||||
def release_composition_issues(meta_root: Path, *, core_root: Path) -> tuple[VersionAlignmentIssue, ...]:
|
||||
"""Require release references to agree and point at installable artifacts."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user