feat(release): enforce aligned trusted publications
This commit is contained in:
@@ -13,7 +13,9 @@ from govoplan_core.core.module_package_catalog import validate_module_package_ca
|
||||
|
||||
from .catalog import canonical_hash
|
||||
from .model import CatalogPublishResult, CatalogPublishStep
|
||||
from .module_directory import write_module_directory
|
||||
from .selective_catalog import trusted_keys_from_keyring
|
||||
from .version_alignment import candidate_catalog_version_issues
|
||||
from .workspace import DEFAULT_WORKSPACE_ROOT, resolve_workspace_root, website_root
|
||||
|
||||
|
||||
@@ -41,7 +43,6 @@ def publish_catalog_candidate(
|
||||
resolved_web_root = Path(web_root).expanduser().resolve() if web_root is not None else website_root(workspace)
|
||||
candidate_catalog = candidate_root / "channels" / f"{channel}.json"
|
||||
candidate_keyring = candidate_root / "keyring.json"
|
||||
candidate_modules = candidate_root / "modules"
|
||||
target_catalog = resolved_web_root / "public" / "catalogs" / "v1" / "channels" / f"{channel}.json"
|
||||
target_keyring = resolved_web_root / "public" / "catalogs" / "v1" / "keyring.json"
|
||||
target_modules = resolved_web_root / "public" / "catalogs" / "v1" / "modules"
|
||||
@@ -72,11 +73,36 @@ def publish_catalog_candidate(
|
||||
validation_warnings: tuple[str, ...] = ()
|
||||
|
||||
if candidate_payload is not None and keyring_payload is not None:
|
||||
version_issues = candidate_catalog_version_issues(candidate_payload)
|
||||
blockers.extend(
|
||||
f"candidate version alignment: {issue.source}: {issue.actual!r}; "
|
||||
f"expected {issue.expected!r} ({issue.message})"
|
||||
for issue in version_issues
|
||||
)
|
||||
candidate_keys = trusted_keys_from_keyring(keyring_payload if isinstance(keyring_payload, dict) else {})
|
||||
target_keyring_payload = read_json(target_keyring) if target_keyring.exists() else None
|
||||
publication_trust = trusted_keys_from_keyring(
|
||||
target_keyring_payload if isinstance(target_keyring_payload, dict) else {}
|
||||
)
|
||||
if not publication_trust:
|
||||
blockers.append(
|
||||
"publication trust anchor is missing; bootstrap a trusted website keyring through a separate reviewed process"
|
||||
)
|
||||
for key_id in sorted(set(candidate_keys) & set(publication_trust)):
|
||||
if candidate_keys[key_id] != publication_trust[key_id]:
|
||||
blockers.append(f"candidate keyring changes the public key for trusted key id {key_id!r}")
|
||||
if candidate_keyring_hash != target_keyring_hash_before:
|
||||
release = candidate_payload.get("release") if isinstance(candidate_payload, dict) else None
|
||||
signed_keyring_hash = release.get("keyring_sha256") if isinstance(release, dict) else None
|
||||
if signed_keyring_hash != candidate_keyring_hash:
|
||||
blockers.append(
|
||||
"candidate keyring differs from the publication trust anchor without a matching signed keyring_sha256"
|
||||
)
|
||||
validation = validate_module_package_catalog(
|
||||
candidate_catalog,
|
||||
require_trusted=True,
|
||||
approved_channels=(channel,),
|
||||
trusted_keys=trusted_keys_from_keyring(keyring_payload if isinstance(keyring_payload, dict) else {}),
|
||||
trusted_keys=publication_trust,
|
||||
)
|
||||
validation_valid = validation.get("valid") is True
|
||||
validation_error = str(validation["error"]) if validation.get("error") else None
|
||||
@@ -115,14 +141,12 @@ def publish_catalog_candidate(
|
||||
steps.append(
|
||||
CatalogPublishStep(
|
||||
id="copy",
|
||||
title="Copy candidate catalog, keyring, and module directory",
|
||||
title="Copy candidate catalog/keyring and derive the module directory",
|
||||
detail=f"{candidate_root} -> {resolved_web_root / 'public' / 'catalogs' / 'v1'}",
|
||||
mutating=True,
|
||||
status="planned" if not apply else "blocked" if blockers else "done",
|
||||
)
|
||||
)
|
||||
if not candidate_modules.exists():
|
||||
notes.append("Candidate has no module-directory tree; only catalog and keyring will be copied.")
|
||||
if build_web:
|
||||
steps.append(
|
||||
CatalogPublishStep(
|
||||
@@ -224,10 +248,15 @@ def publish_catalog_candidate(
|
||||
target_catalog.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(candidate_catalog, target_catalog)
|
||||
shutil.copy2(candidate_keyring, target_keyring)
|
||||
if candidate_modules.exists():
|
||||
if target_modules.exists():
|
||||
shutil.rmtree(target_modules)
|
||||
shutil.copytree(candidate_modules, target_modules)
|
||||
if target_modules.exists():
|
||||
shutil.rmtree(target_modules)
|
||||
write_module_directory(
|
||||
catalog_payload=candidate_payload,
|
||||
keyring_payload=keyring_payload,
|
||||
output_root=target_catalog.parent.parent,
|
||||
channel=channel,
|
||||
public_base_url=catalog_public_base_url(candidate_payload),
|
||||
)
|
||||
|
||||
completed_steps = [replace_status(step, "done") if step.id == "copy" else step for step in steps]
|
||||
if build_web:
|
||||
@@ -282,6 +311,14 @@ def publish_catalog_candidate(
|
||||
)
|
||||
|
||||
|
||||
def catalog_public_base_url(payload: dict[str, object]) -> str:
|
||||
release = payload.get("release")
|
||||
catalog_url = release.get("catalog_url") if isinstance(release, dict) else None
|
||||
if isinstance(catalog_url, str) and "/catalogs/" in catalog_url:
|
||||
return catalog_url.split("/catalogs/", 1)[0]
|
||||
return "https://govoplan.add-ideas.de"
|
||||
|
||||
|
||||
def result(
|
||||
*,
|
||||
status: str,
|
||||
|
||||
Reference in New Issue
Block a user