From 26099297801bd71edf9b2443910cff963c445c36 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 19 Feb 2026 14:06:53 +0100 Subject: [PATCH] FDroid automation in pipeline. --- .gitlab-ci.yml | 13 +++- update_fdroid_index.py | 170 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 update_fdroid_index.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dec0b88d..46a1754f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,7 +26,7 @@ buildAndDeployApkStable: expire_in: 30 days paths: - app/build/outputs/apk/stable/release/*.apk - + buildAndDeployPlaystore: stage: buildAndDeployPlaystore script: @@ -44,3 +44,14 @@ buildAndDeployPlaystore: expire_in: 30 days paths: - app/build/outputs/bundle/playstoreRelease/*.aab + +updateFdroidRepo: + stage: updateFdroidRepo + only: + - tags + when: on_success + needs: + - job: buildAndDeployApkStable + artifacts: true + script: + - python3 update_fdroid_index.py \ No newline at end of file diff --git a/update_fdroid_index.py b/update_fdroid_index.py new file mode 100644 index 00000000..7763214e --- /dev/null +++ b/update_fdroid_index.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import glob +import hashlib +import datetime +import os +import re +import shutil +import subprocess +import sys +import tempfile +from typing import Optional + +APK_URL = "https://releases.grayjay.app/app-universal-release.apk" + +FDROID_REPO_SSH = "git@gitlab.futo.org:fdroid/repo-v2.git" +FDROID_INDEX_PATH = "apps/Grayjay/index.yml" +UNIVERSAL_APK_GLOB = "app/build/outputs/apk/stable/release/*universal*.apk" + +GIT_USER_NAME = "koen" +GIT_USER_EMAIL = "koen@futo.org" + +class Fatal(Exception): + pass + +def run(cmd: list[str], *, cwd: Optional[str] = None) -> str: + p = subprocess.run(cmd, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if p.returncode != 0: + raise Fatal(f"Command failed ({p.returncode}): {' '.join(cmd)}\n{p.stdout}") + return p.stdout.strip() + +def sha256_of_file(path: str) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + +def pick_universal_apk() -> str: + matches = sorted(glob.glob(UNIVERSAL_APK_GLOB)) + if not matches: + raise Fatal(f"No universal APK found via glob: {UNIVERSAL_APK_GLOB}") + + for m in matches: + base = os.path.basename(m) + if "app-stable-universal" in base: + return m + + return matches[-1] + +def get_release_date_today() -> str: + return datetime.datetime.now(datetime.timezone.utc).date().isoformat() + +def get_version_code_from_tag() -> int: + tag = os.environ.get("CI_COMMIT_TAG", "").strip() + if not tag: + tag = run(["git", "describe", "--tags"]).strip() + + m = re.search(r"(\d+)", tag) + if not m: + raise Fatal(f"Could not parse an integer versionCode from tag '{tag}'") + + return int(m.group(1)) + +def update_index_yml(path: str, sha256sum: str, date_str: str, version_code: int) -> None: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + + url_line_idx = None + url_line_indent = "" + for i, line in enumerate(lines): + stripped = line.lstrip() + if stripped.startswith("- url:") and APK_URL in stripped: + url_line_idx = i + url_line_indent = line[: len(line) - len(stripped)] + break + if url_line_idx is None: + raise Fatal(f"Did not find an apk entry with url {APK_URL} in {path}") + + def is_url_line_same_level(s: str) -> bool: + st = s.lstrip() + indent = s[: len(s) - len(st)] + return st.startswith("- url:") and indent == url_line_indent + + end = len(lines) + for j in range(url_line_idx + 1, len(lines)): + if is_url_line_same_level(lines[j]): + end = j + break + + child_indent = url_line_indent + " " + found_sha = found_date = found_vc = False + + for j in range(url_line_idx + 1, end): + st = lines[j].lstrip() + indent = lines[j][: len(lines[j]) - len(st)] + if st.startswith("sha256sum:"): + lines[j] = f"{indent}sha256sum: {sha256sum}\n" + found_sha = True + elif st.startswith("date:"): + lines[j] = f"{indent}date: {date_str}\n" + found_date = True + elif st.startswith("version-code:"): + lines[j] = f"{indent}version-code: {version_code}\n" + found_vc = True + + insert_pos = url_line_idx + 1 + to_insert: list[str] = [] + if not found_sha: + to_insert.append(f"{child_indent}sha256sum: {sha256sum}\n") + if not found_date: + to_insert.append(f"{child_indent}date: {date_str}\n") + if not found_vc: + to_insert.append(f"{child_indent}version-code: {version_code}\n") + if to_insert: + lines[insert_pos:insert_pos] = to_insert + + with open(path, "w", encoding="utf-8") as f: + f.writelines(lines) + + +def main() -> int: + version_code = get_version_code_from_tag() + date_str = get_release_date_today() + + apk_path = pick_universal_apk() + print(f"Computing sha256 for {apk_path} ...") + sha = sha256_of_file(apk_path) + print(f"sha256: {sha}") + print(f"date: {date_str}") + print(f"version-code: {version_code}") + + tmpdir = tempfile.mkdtemp(prefix="fdroid-repo-") + try: + print(f"Cloning {FDROID_REPO_SSH} ...") + run(["git", "clone", "--depth", "1", FDROID_REPO_SSH, tmpdir]) + + run(["git", "config", "user.name", GIT_USER_NAME], cwd=tmpdir) + run(["git", "config", "user.email", GIT_USER_EMAIL], cwd=tmpdir) + + index_path = os.path.join(tmpdir, FDROID_INDEX_PATH) + if not os.path.exists(index_path): + raise Fatal(f"Missing {FDROID_INDEX_PATH} in repo-v2") + + update_index_yml(index_path, sha, date_str, version_code) + + run(["git", "add", FDROID_INDEX_PATH], cwd=tmpdir) + + diff_rc = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=tmpdir).returncode + if diff_rc == 0: + print("No changes to commit.") + return 0 + + msg = f"Grayjay: update sha/date/version-code to {version_code} ({date_str})" + run(["git", "commit", "-m", msg], cwd=tmpdir) + run(["git", "push"], cwd=tmpdir) + + print("Pushed update to fdroid/repo-v2.") + return 0 + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Fatal as e: + print(f"ERROR: {e}", file=sys.stderr) + raise SystemExit(2)