|
| 1 | +"""Qualify whether the installed runtime proves a selected update source is active. |
| 2 | +
|
| 3 | +``loopx update check`` cannot call a release active just because a newer tag or |
| 4 | +branch exists: the installed archive has to carry source lineage that proves the |
| 5 | +selected update source reached the runtime. This module owns that decision so the |
| 6 | +question keeps one home instead of growing inside the self-update command surface. |
| 7 | +""" |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +import re |
| 12 | +from typing import Any |
| 13 | + |
| 14 | + |
| 15 | +RUNTIME_ACTIVATION_QUALIFICATION_SCHEMA_VERSION = ( |
| 16 | + "loopx_runtime_activation_qualification_v0" |
| 17 | +) |
| 18 | +_FULL_COMMIT_PATTERN = re.compile(r"[0-9a-fA-F]{40}") |
| 19 | + |
| 20 | + |
| 21 | +def _immutable_source_commit(source: dict[str, Any]) -> str | None: |
| 22 | + """Return the selected source commit when the ref is an immutable full SHA. |
| 23 | +
|
| 24 | + A full commit SHA is its own commit identity: ``scripts/install.sh`` installs |
| 25 | + it without a branch-resolution service, so the qualification can compare it |
| 26 | + against the installed manifest instead of reporting that the trusted target |
| 27 | + source lineage is unavailable. |
| 28 | + """ |
| 29 | + |
| 30 | + ref = str(source.get("ref") or "") |
| 31 | + return ref.lower() if _FULL_COMMIT_PATTERN.fullmatch(ref) else None |
| 32 | + |
| 33 | + |
| 34 | +def runtime_activation_qualification( |
| 35 | + *, |
| 36 | + install_freshness: dict[str, Any], |
| 37 | + source: dict[str, Any], |
| 38 | +) -> dict[str, Any]: |
| 39 | + """Return the typed activation qualification for an archive-snapshot install.""" |
| 40 | + |
| 41 | + installed_commit = install_freshness.get("manifest_source_git_commit") |
| 42 | + target_commit = install_freshness.get("freshness_source_git_commit") |
| 43 | + revision_relation = install_freshness.get("manifest_source_freshness_relation") |
| 44 | + selected_commit = _immutable_source_commit(source) |
| 45 | + if selected_commit and not target_commit: |
| 46 | + # An immutable ref names its own commit, so the qualification resolves it |
| 47 | + # the same way the installer does instead of reporting that the |
| 48 | + # installed-versus-target lineage is unavailable. |
| 49 | + target_commit = selected_commit |
| 50 | + if ( |
| 51 | + isinstance(installed_commit, str) |
| 52 | + and installed_commit.lower() == selected_commit |
| 53 | + ): |
| 54 | + revision_relation = "same" |
| 55 | + qualified_repo = install_freshness.get("manifest_source_repo") |
| 56 | + qualified_ref = install_freshness.get("manifest_source_ref") |
| 57 | + selected_repo = source.get("repo") |
| 58 | + selected_ref = source.get("ref") |
| 59 | + package_matches_runtime = install_freshness.get( |
| 60 | + "manifest_package_version_matches_runtime" |
| 61 | + ) |
| 62 | + requires_upgrade = install_freshness.get("requires_upgrade") |
| 63 | + has_commit_pair = all( |
| 64 | + isinstance(commit, str) and bool(commit) |
| 65 | + for commit in (installed_commit, target_commit) |
| 66 | + ) |
| 67 | + source_identity_matches = all( |
| 68 | + isinstance(value, str) and bool(value) |
| 69 | + for value in (qualified_repo, qualified_ref, selected_repo, selected_ref) |
| 70 | + ) and ( |
| 71 | + str(qualified_repo).removesuffix(".git").lower() |
| 72 | + == str(selected_repo).removesuffix(".git").lower() |
| 73 | + and str(qualified_ref).removeprefix("refs/heads/") |
| 74 | + == str(selected_ref).removeprefix("refs/heads/") |
| 75 | + ) |
| 76 | + |
| 77 | + if package_matches_runtime is False: |
| 78 | + decision = "release_or_install_successor_required" |
| 79 | + runtime_active: bool | None = False |
| 80 | + successor_kind = "release_or_install" |
| 81 | + reason = "release manifest package version does not match the active runtime" |
| 82 | + elif not source_identity_matches: |
| 83 | + decision = "activation_qualification_required" |
| 84 | + runtime_active = None |
| 85 | + successor_kind = "activation_qualification" |
| 86 | + reason = "trusted source lineage does not identify the selected update source" |
| 87 | + elif has_commit_pair and ( |
| 88 | + installed_commit == target_commit or revision_relation == "installed_ahead" |
| 89 | + ): |
| 90 | + decision = "runtime_active" |
| 91 | + runtime_active = True |
| 92 | + successor_kind = None |
| 93 | + reason = "installed source contains the trusted target source commit" |
| 94 | + elif has_commit_pair and revision_relation in {"installed_behind", "diverged"}: |
| 95 | + decision = "release_or_install_successor_required" |
| 96 | + runtime_active = False |
| 97 | + successor_kind = "release_or_install" |
| 98 | + reason = "installed source does not contain the trusted target source commit" |
| 99 | + elif has_commit_pair and selected_commit: |
| 100 | + decision = "activation_qualification_required" |
| 101 | + runtime_active = None |
| 102 | + successor_kind = "activation_qualification" |
| 103 | + reason = ( |
| 104 | + "installed source commit does not match the selected immutable source commit" |
| 105 | + ) |
| 106 | + else: |
| 107 | + decision = "activation_qualification_required" |
| 108 | + runtime_active = None |
| 109 | + successor_kind = "activation_qualification" |
| 110 | + reason = "trusted installed-versus-target source lineage is unavailable" |
| 111 | + |
| 112 | + return { |
| 113 | + "schema_version": RUNTIME_ACTIVATION_QUALIFICATION_SCHEMA_VERSION, |
| 114 | + "decision": decision, |
| 115 | + "runtime_active": runtime_active, |
| 116 | + "installed_release_id": install_freshness.get("release_id"), |
| 117 | + "installed_version": install_freshness.get("current_version"), |
| 118 | + "installed_source_commit": installed_commit, |
| 119 | + "target_source_label": install_freshness.get("freshness_source_label"), |
| 120 | + "target_source_commit": target_commit, |
| 121 | + "revision_relation": revision_relation, |
| 122 | + "qualified_source": { |
| 123 | + "repo": qualified_repo, |
| 124 | + "ref": qualified_ref, |
| 125 | + }, |
| 126 | + "source_identity_matches": source_identity_matches, |
| 127 | + "package_version_matches_runtime": package_matches_runtime, |
| 128 | + "requires_upgrade": requires_upgrade, |
| 129 | + "selected_source": { |
| 130 | + "repo": selected_repo, |
| 131 | + "ref": selected_ref, |
| 132 | + }, |
| 133 | + "successor": { |
| 134 | + "required": runtime_active is not True, |
| 135 | + "kind": successor_kind, |
| 136 | + }, |
| 137 | + "reason": reason, |
| 138 | + } |
0 commit comments