115 lines
5.6 KiB
Python
115 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Actual-asset update transaction in an owned image; never publishes the fixture version."""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from verify_prepared_release import InstalledDaemon, execute, install_assets, minimal_os, response
|
|
|
|
|
|
def prepare_update(image, manifest, key, helper):
|
|
# Reuse real payloads under a newly signed, acceptance-only catalog identity.
|
|
# This tests the update owner without claiming a second public release exists.
|
|
next_manifest = dict(manifest)
|
|
major, minor, patch = map(int, manifest["version"].split("."))
|
|
version = f"{major}.{minor}.{patch + 1}"
|
|
next_manifest["version"] = version
|
|
machine = image / "opt/dever"
|
|
package = machine / "cache/downloads" / version
|
|
package.mkdir()
|
|
installed = machine / "versions" / manifest["version"]
|
|
for artifact in manifest["artifacts"]:
|
|
destination = package / artifact["path"]
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
os.link(installed / artifact["path"], destination)
|
|
catalog = package / "manifest.json"
|
|
catalog.write_text(json.dumps(next_manifest, separators=(",", ":")))
|
|
execute([helper, "--sign-catalog", catalog, key])
|
|
(machine / "cache/downloads/latest").write_text(version)
|
|
return version, hashlib.sha256(catalog.read_bytes()).hexdigest()
|
|
|
|
|
|
def accept(args):
|
|
manifest = json.loads((args.release / "manifest.json").read_bytes())
|
|
result = {"version": manifest["version"], "checks": [],
|
|
"transport": "local actual assets; acceptance-only next-version catalog",
|
|
"online_update": "not_run"}
|
|
|
|
def passed(name):
|
|
result["checks"].append(name)
|
|
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
|
print(f"PASS {name}", flush=True)
|
|
|
|
try:
|
|
args.image.mkdir()
|
|
minimal_os(args.image)
|
|
install_assets(args.assets, args.image, manifest)
|
|
machine = args.image / "opt/dever"
|
|
extension = f"runtime-pip-{manifest['platform']}"
|
|
execute([args.extension_helper, machine, args.assets, manifest["version"], extension])
|
|
records = args.report.parent / f"{args.report.stem}-process"
|
|
with InstalledDaemon(args.image, records, args.init) as daemon:
|
|
daemon.command("new", "/project")
|
|
response(daemon.command("run", "/project", "--", "hello.greeting.greet", '{"name":"Dever"}'))
|
|
passed("actual_release_and_selected_extension_before_update")
|
|
version, catalog = prepare_update(args.image, manifest, args.key, args.extension_helper)
|
|
result["fixture_version"] = version
|
|
previous_active = (machine / "state/active-version").read_bytes()
|
|
empty_assets = args.image / "missing-extension-assets"
|
|
empty_assets.mkdir()
|
|
try:
|
|
execute([args.extension_helper, machine, empty_assets, version, "--update"])
|
|
except RuntimeError as error:
|
|
if "cannot open actual extension asset" not in str(error):
|
|
raise
|
|
else:
|
|
raise RuntimeError("update with a missing extension should fail")
|
|
if (machine / "state/active-version").read_bytes() != previous_active:
|
|
raise RuntimeError("failed update changed active version")
|
|
if daemon.command("version") != manifest["version"]:
|
|
raise RuntimeError("failed update changed the public launcher selection")
|
|
response(daemon.command("run", "/project", "--", "hello.greeting.greet", '{"name":"Dever"}'))
|
|
passed("failed_extension_update_keeps_old_active_and_application_working")
|
|
execute([args.extension_helper, machine, args.assets, version, "--update"])
|
|
if daemon.command("version") != version:
|
|
raise RuntimeError("successful update did not activate the new version")
|
|
if daemon.command("skill", "path") != f"/opt/dever/versions/{version}/skills/dever-language":
|
|
raise RuntimeError("skill did not follow the updated core")
|
|
extensions = machine / "cache/extensions" / version / catalog
|
|
if {path.name for path in extensions.iterdir()} != {extension}:
|
|
raise RuntimeError("update did not preserve exactly the installed extension selection")
|
|
passed("successful_update_activates_core_skill_and_only_installed_extensions")
|
|
# Only the catalog version changes in this transaction fixture.
|
|
# The managed compiler correctly requires its real build version;
|
|
# execute it again only after selecting the actual release.
|
|
daemon.command("use", manifest["version"])
|
|
response(daemon.command("run", "/project", "--", "hello.greeting.greet", '{"name":"Dever"}'))
|
|
passed("actual_release_remains_usable_after_update_and_explicit_switch_back")
|
|
result["status"] = "passed"
|
|
except BaseException as error:
|
|
result["status"] = "failed"
|
|
result["error"] = str(error)
|
|
raise
|
|
finally:
|
|
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
for name in ("release", "assets", "image", "report", "init", "extension-helper", "key"):
|
|
parser.add_argument(f"--{name}", required=True, type=Path)
|
|
args = parser.parse_args()
|
|
if os.geteuid() != 0:
|
|
parser.error("requires root for an owned image; never installs host services")
|
|
for name, value in vars(args).items():
|
|
setattr(args, name, value.absolute())
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
accept(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|