Add channel-aware release tooling and updater recovery

This commit is contained in:
2026-09-10 00:39:37 +02:00
parent cc48bf45d2
commit 0a89323533
42 changed files with 7840 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Build one signed channel view from authenticated release history metadata."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from ptlinux.self_update import LauncherUpdateError, parse_update_manifest # noqa: E402
from ptlinux.update_resolver import ( # noqa: E402
ReleaseModelError,
ReleaseRecord,
UpdateResolver,
validate_channel,
)
from scripts.release_lib import ReleaseToolError # noqa: E402
def prepare_channel_metadata(
history: object,
channel: str,
manifest_bytes: bytes,
) -> tuple[dict[str, object], dict[str, object]]:
"""Return the selected history record and a status-aware manifest copy."""
try:
validate_channel(channel)
except ReleaseModelError as exc:
raise ReleaseToolError(str(exc)) from exc
if not isinstance(history, dict) or history.get("schema") != 1:
raise ReleaseToolError("release history must use schema 1")
releases = history.get("releases")
if not isinstance(releases, list):
raise ReleaseToolError("release history releases must be an array")
records: list[tuple[dict[str, object], ReleaseRecord]] = []
for value in releases:
if not isinstance(value, dict) or value.get("channel") != channel:
continue
try:
record = ReleaseRecord(
version=_string(value, "version"),
channel=_string(value, "channel"),
status=_string(value, "status"),
tag=_string(value, "tag"),
manifest_url=_string(value, "manifest_url"),
).validate()
except ReleaseModelError as exc:
raise ReleaseToolError(f"invalid release history record: {exc}") from exc
records.append((value, record))
if not records:
raise ReleaseToolError(f"release history has no {channel} releases")
try:
resolver = UpdateResolver(record for _value, record in records)
selected_record = resolver.latest_eligible(channel)
if selected_record is None:
selected_record = resolver.eligible_releases(channel, include_inactive=True)[0]
except ReleaseModelError as exc:
raise ReleaseToolError(f"invalid release history: {exc}") from exc
selected = next(
value for value, record in records if record.version == selected_record.version
)
try:
manifest = parse_update_manifest(manifest_bytes)
except LauncherUpdateError as exc:
raise ReleaseToolError(f"selected channel manifest is invalid: {exc}") from exc
for field, actual, expected in (
("version", manifest.version, selected_record.version),
("channel", manifest.channel, selected_record.channel),
("tag", manifest.tag, selected_record.tag),
):
if actual != expected:
raise ReleaseToolError(
f"selected manifest {field} {actual!r} does not match history {expected!r}"
)
try:
manifest_value = json.loads(manifest_bytes.decode("utf-8"))
except (UnicodeError, json.JSONDecodeError) as exc:
raise ReleaseToolError(f"selected channel manifest is invalid JSON: {exc}") from exc
if not isinstance(manifest_value, dict):
raise ReleaseToolError("selected channel manifest must be an object")
manifest_value["status"] = selected_record.status
history_compatibility = selected.get("compatibility")
if not isinstance(history_compatibility, dict):
raise ReleaseToolError("selected release compatibility must be an object")
config_schema = _positive_integer(history_compatibility, "config_schema")
minimum_config_schema = _positive_integer(
history_compatibility,
"minimum_config_schema",
)
if minimum_config_schema > config_schema:
raise ReleaseToolError(
"selected release minimum_config_schema exceeds config_schema"
)
manifest_compatibility = manifest_value.get("compatibility")
if not isinstance(manifest_compatibility, dict):
raise ReleaseToolError("selected channel manifest compatibility is invalid")
manifest_compatibility["config_schema"] = config_schema
manifest_compatibility["minimum_config_schema"] = minimum_config_schema
return selected, manifest_value
def _string(value: dict[str, object], key: str) -> str:
item = value.get(key)
if not isinstance(item, str) or not item:
raise ReleaseToolError(f"release history field {key!r} must be a string")
return item
def _positive_integer(value: dict[str, object], key: str) -> int:
item = value.get(key)
if isinstance(item, bool) or not isinstance(item, int) or item <= 0:
raise ReleaseToolError(
f"selected release compatibility {key!r} must be positive"
)
return item
def _write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
try:
with temporary.open("x", encoding="utf-8", newline="\n") as handle:
json.dump(value, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
temporary.chmod(0o644)
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("history", type=Path)
parser.add_argument("channel", choices=("stable", "prerelease"))
parser.add_argument("manifest", type=Path)
parser.add_argument("output", type=Path)
arguments = parser.parse_args()
try:
history = json.loads(arguments.history.read_text(encoding="utf-8"))
selected, manifest = prepare_channel_metadata(
history,
arguments.channel,
arguments.manifest.read_bytes(),
)
_write_json(arguments.output, manifest)
print(_string(selected, "version"))
except (OSError, UnicodeError, json.JSONDecodeError, ReleaseToolError) as exc:
print(f"Channel metadata preparation failed: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())