From ff77d8f89ff7416aef619f51bf68956c9cb39f97 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Thu, 22 Oct 2026 22:06:57 +0300 Subject: [PATCH] [skip ci] fix ots --- .gitea/workflows/android-release.yml | 2 +- backend/app/services/android_updates.py | 50 ++++++++++++++++++-- backend/tests/test_android_updates.py | 61 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/android-release.yml b/.gitea/workflows/android-release.yml index 9fdbffb..49c7dc1 100644 --- a/.gitea/workflows/android-release.yml +++ b/.gitea/workflows/android-release.yml @@ -149,7 +149,7 @@ jobs: APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner | sort | tail -1)" SIGNER_SHA256="$("$APKSIGNER" verify --print-certs "$APK" \ - | sed -n 's/^Signer #1 certificate SHA-256 digest: //p' | head -1)" + | sed -n 's/.*certificate SHA-256 digest: //p' | head -1)" printf '{\n "version_code": %s,\n "version_name": "%s",\n "commit": "%s",\n "built_at": "%s",\n "size_bytes": %s,\n "sha256": "%s",\n "signer_sha256": "%s"\n}\n' \ "${{ gitea.run_number }}" \ diff --git a/backend/app/services/android_updates.py b/backend/app/services/android_updates.py index 7904a30..b4e7c9c 100644 --- a/backend/app/services/android_updates.py +++ b/backend/app/services/android_updates.py @@ -19,7 +19,7 @@ import tempfile import threading import time import zipfile -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path from typing import Any @@ -139,8 +139,45 @@ def _artifact(raw: Any) -> Artifact | None: ) +def _run(client: httpx.Client, settings: EnvSettings, run_id: int) -> dict[str, Any]: + try: + response = client.get(_api(settings, f"/actions/runs/{run_id}")) + response.raise_for_status() + payload = response.json() + except (httpx.HTTPError, ValueError) as exc: + raise UpdateUnavailable(f"Could not read Gitea run {run_id}: {exc}") from exc + return payload if isinstance(payload, dict) else {} + + +def _with_run( + client: httpx.Client, + settings: EnvSettings, + artifact: Artifact, + runs: dict[int, dict[str, Any]], +) -> Artifact: + """Gitea's artifact list can omit the run's branch, number and conclusion.""" + if artifact.run_number and artifact.branch and artifact.conclusion: + return artifact + if not artifact.run_id: + return artifact + run = runs.get(artifact.run_id) + if run is None: + run = _run(client, settings, artifact.run_id) + runs[artifact.run_id] = run + return replace( + artifact, + run_number=int(run.get("run_number") or artifact.run_number), + branch=str(run.get("head_branch") or artifact.branch), + commit=str(run.get("head_sha") or artifact.commit), + conclusion=str(run.get("conclusion") or artifact.conclusion), + ) + + def _list_artifacts( - client: httpx.Client, settings: EnvSettings, name: str + client: httpx.Client, + settings: EnvSettings, + name: str, + runs: dict[int, dict[str, Any]] | None = None, ) -> list[Artifact]: try: response = client.get(_api(settings, "/actions/artifacts"), params={"name": name}) @@ -152,7 +189,9 @@ def _list_artifacts( entries = payload.get("artifacts") if isinstance(payload, dict) else None if not isinstance(entries, list): return [] - return [artifact for raw in entries if (artifact := _artifact(raw)) is not None] + artifacts = [artifact for raw in entries if (artifact := _artifact(raw)) is not None] + run_cache = runs if runs is not None else {} + return [_with_run(client, settings, artifact, run_cache) for artifact in artifacts] def _successful(artifacts: list[Artifact], branch: str) -> list[Artifact]: @@ -268,12 +307,13 @@ def _resolve(settings: EnvSettings) -> ResolvedUpdate: if cached is not None and now - cached[0] < _METADATA_TTL_SECONDS: return cached[1] + runs: dict[int, dict[str, Any]] = {} with _client(settings) as client: metadata_artifacts = _successful( - _list_artifacts(client, settings, METADATA_ARTIFACT), + _list_artifacts(client, settings, METADATA_ARTIFACT, runs), settings.gitea_update_branch, ) - apk_artifacts = _list_artifacts(client, settings, APK_ARTIFACT) + apk_artifacts = _list_artifacts(client, settings, APK_ARTIFACT, runs) usable_apk = _successful(apk_artifacts, settings.gitea_update_branch) resolved: ResolvedUpdate | None = None diff --git a/backend/tests/test_android_updates.py b/backend/tests/test_android_updates.py index 20b22b0..e850c60 100644 --- a/backend/tests/test_android_updates.py +++ b/backend/tests/test_android_updates.py @@ -8,6 +8,7 @@ import zipfile import httpx import pytest + from app.config import env_settings from app.services import android_updates @@ -200,3 +201,63 @@ def test_the_route_reports_503_when_updates_are_not_configured(client, monkeypat assert response.status_code == 503 assert "GITEA_TOKEN" in response.json()["detail"] + + +def test_sparse_artifact_run_metadata_is_fetched_from_the_run(monkeypatch): + _configure(monkeypatch) + metadata = _artifact(31, run_id=401, run_number=0, branch="", conclusion="") + metadata["workflow_run"] = {"id": 401, "head_sha": "sha-88"} + apk = _artifact( + 32, + run_id=401, + run_number=0, + name=android_updates.APK_ARTIFACT, + branch="", + conclusion="", + ) + apk["workflow_run"] = {"id": 401, "head_sha": "sha-88"} + zips = { + 31: _zip({"update.json": _metadata_body(88, "0.1.0-master.88")}), + 32: _zip({"app-release.apk": b"sparse apk"}), + } + run_requests = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal run_requests + if request.url.path.endswith("/actions/artifacts"): + name = request.url.params.get("name") + return httpx.Response( + 200, + json={ + "total_count": 1, + "artifacts": [metadata] + if name == android_updates.METADATA_ARTIFACT + else [apk], + }, + ) + if request.url.path.endswith("/actions/runs/401"): + run_requests += 1 + return httpx.Response( + 200, + json={ + "id": 401, + "run_number": 88, + "head_branch": "master", + "head_sha": "sha-88", + "conclusion": "success", + }, + ) + artifact_id = int(request.url.path.rstrip("/").split("/")[-2]) + return httpx.Response(200, content=zips[artifact_id]) + + _install_transport(monkeypatch, handler) + + metadata = android_updates.latest_android() + assert metadata["version_code"] == 88 + assert metadata["version_name"] == "0.1.0-master.88" + assert metadata["commit"] == "sha-88" + assert run_requests == 1 + + path, same = android_updates.android_apk() + assert path.read_bytes() == b"sparse apk" + assert same == metadata