#!/usr/bin/env python3 """Publie/met à jour une release Gitea : corps = section CHANGELOG de la version, APK en pièce jointe (optionnel). Réutilisable à chaque release. Usage : python3 scripts/gitea-release.py v1.2.3 [chemin/vers/app.apk] - le corps est extrait de docs/CHANGELOG.md (section « ## [X.Y.Z] », SANS « v » — le tag git porte le « v », la CHANGELOG non : piège corrigé, cf §16.bis) ; - si la release existe déjà, son corps est mis à jour (PATCH) ; - si un chemin d'APK est fourni, il est attaché (l'asset existant du même nom est remplacé). Auth : le token Gitea est lu dans le trousseau macOS (security find-internet-password -s gitea.cloudyfy.fr -w) — scope requis : write:repository (suffit pour releases + assets, pas pour créer un repo). """ import json import re import subprocess import sys import urllib.error import urllib.request GITEA = "https://gitea.cloudyfy.fr" OWNER, REPO = "Siphonight", "HormoneTrack" CHANGELOG = "docs/CHANGELOG.md" def gitea_token() -> str: out = subprocess.run( ["security", "find-internet-password", "-s", "gitea.cloudyfy.fr", "-w"], capture_output=True, text=True ).stdout.strip() if not out: sys.exit("Token Gitea introuvable dans le trousseau macOS") return out def request(url: str, method: str = "GET", data=None, token: str = "", files=None): req = urllib.request.Request(url, method=method) req.add_header("Authorization", f"token {token}") body = None if files is not None: boundary = "----HormoneTrackBoundary" body = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; " f"filename=\"{files['name']}\"\r\n" f"Content-Type: application/vnd.android.package-archive\r\n\r\n").encode() \ + files["data"] + f"\r\n--{boundary}--\r\n".encode() req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}") elif data is not None: req.add_header("Content-Type", "application/json") body = json.dumps(data).encode() if body is not None: req.data = body with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode()) def changelog_section(version: str) -> str: """Extrait la section « ## [X.Y.Z] » de la CHANGELOG (version SANS le « v»).""" num = version.removeprefix("v") text = open(CHANGELOG, encoding="utf-8").read() m = re.search(rf"## \[{re.escape(num)}\][^\n]*\n(.*?)(?=\n## \[|\Z)", text, re.S) if not m: sys.exit(f"Section [{num}] introuvable dans {CHANGELOG}") return m.group(1).strip() def main(): tag = sys.argv[1] if len(sys.argv) > 1 else sys.exit("Usage: gitea-release.py vX.Y.Z [app.apk]") apk_path = sys.argv[2] if len(sys.argv) > 2 else None token = gitea_token() base = f"{GITEA}/api/v1/repos/{OWNER}/{REPO}" body = changelog_section(tag) # Récupère la release du tag (crée-la si absente) try: release = request(f"{base}/releases/tags/{tag}", token=token) release_id = release["id"] created = False except urllib.error.HTTPError as e: if e.code != 404: raise release = request(f"{base}/releases", "POST", token=token, data={ "tag_name": tag, "name": tag, "body": body, "draft": False, "prerelease": False, }) release_id = release["id"] created = True # Corps = section CHANGELOG (PATCH si la release existait déjà) if not created: request(f"{base}/releases/{release_id}", "PATCH", token=token, data={"tag_name": tag, "name": tag, "body": body, "draft": False, "prerelease": False}) # APK en pièce jointe (remplace l'asset existant du même nom) if apk_path: asset_name = f"HormoneTrack-{tag}.apk" for asset in release.get("assets", []): if asset["name"] == asset_name: request(f"{base}/releases/{release_id}/assets/{asset['id']}", "DELETE", token=token) with open(apk_path, "rb") as f: asset = request( f"{base}/releases/{release_id}/assets?name={asset_name}", "POST", token=token, files={"name": asset_name, "data": f.read()}, ) link = asset.get("browser_download_url", "?") else: link = "—" action = "créée" if created else "mise à jour" print(f"Release {tag} {action} (id {release_id}) — corps {len(body)} caractères, APK : {link}") if __name__ == "__main__": main()