Build and push server image / build-and-push (push) Successful in 39s
New Gitea Actions workflow builds both board variants and publishes them as release assets whenever firmware/version.txt is bumped. The server can now poll that repo's releases (next to the existing manual upload) and either surface an "Update frame" button or, with "Automatically apply updates" checked, stage the new build itself -- the frame still only updates on its own next wake either way.
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""Checks a Gitea repo's releases for newer frame firmware, and can pull a
|
|
new release's binary into the local firmware.bin slot -- from the device's
|
|
point of view an auto-applied update is indistinguishable from a manual
|
|
`POST /api/firmware` upload, it self-updates on its next wake either way
|
|
(see firmware/main/ota_update.c).
|
|
|
|
The release-side counterpart is `.gitea/workflows/firmware-release-build.yml`,
|
|
which builds one binary per supported board and publishes them as release
|
|
assets named `firmware-<board_variant>.bin` -- asset_name_for_board() below
|
|
must keep matching that naming exactly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
UPDATE_CHECK_INTERVAL_S = 15 * 60 # don't hit the Gitea API more often than this
|
|
HTTP_TIMEOUT_S = 15.0
|
|
DOWNLOAD_TIMEOUT_S = 60.0
|
|
|
|
|
|
def asset_name_for_board(board_variant: str) -> str:
|
|
return f"firmware-{board_variant}.bin"
|
|
|
|
|
|
def _api_base(repo_url: str) -> str:
|
|
""""https://git.example.com/owner/repo" (optionally with a trailing
|
|
slash or ".git") -> that repo's Gitea REST API base."""
|
|
url = repo_url.strip().rstrip("/")
|
|
if url.endswith(".git"):
|
|
url = url[: -len(".git")]
|
|
scheme, _, rest = url.partition("://")
|
|
host, _, path = rest.partition("/")
|
|
owner, _, repo = path.rpartition("/")
|
|
return f"{scheme}://{host}/api/v1/repos/{owner}/{repo}"
|
|
|
|
|
|
def _headers(token: str) -> dict:
|
|
return {"Authorization": f"token {token}"} if token else {}
|
|
|
|
|
|
def fetch_latest_release(repo_url: str, token: str = "") -> dict | None:
|
|
"""Returns {"version": "1.2.3", "assets": {name: download_url}} for the
|
|
repo's latest release, or None if it has no releases yet."""
|
|
resp = httpx.get(
|
|
f"{_api_base(repo_url)}/releases/latest", headers=_headers(token), timeout=HTTP_TIMEOUT_S
|
|
)
|
|
if resp.status_code == 404:
|
|
return None
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
tag = data["tag_name"]
|
|
version = tag[1:] if tag.startswith("v") else tag
|
|
assets = {a["name"]: a["browser_download_url"] for a in data.get("assets", [])}
|
|
return {"version": version, "assets": assets}
|
|
|
|
|
|
def download_asset(url: str, token: str = "") -> bytes:
|
|
resp = httpx.get(url, headers=_headers(token), timeout=DOWNLOAD_TIMEOUT_S, follow_redirects=True)
|
|
resp.raise_for_status()
|
|
return resp.content
|