Files
espresso_frame/server/app/gitea_releases.py
T
tfaour 94d67f7767
Build and push server image / build-and-push (push) Successful in 39s
Firmware CI releases + Gitea auto-update in the server
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.
2026-07-21 19:25:08 -04:00

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