Files
ObsiGate/scripts/publish_release.py
T
bruno 5f981509ac
CI / lint (push) Successful in 30s
CI / security (push) Successful in 22s
CI / test (push) Successful in 35s
CI / build (push) Successful in 13s
CI / e2e (push) Successful in 6m7s
fix(publish): user-agent navigateur pour contourner le blocage Cloudflare 1010 (writes Gitea)
2026-08-25 21:23:04 -04:00

347 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
ObsiGate — Script de publication des binaires Desktop vers Gitea Releases.
Ce script recherche les binaires compilés localement (Windows .exe/.msi, Linux .AppImage/.deb),
génère le fichier checksums.txt (empreintes SHA256) et crée/met à jour la Release sur Gitea
via son API REST (https://git.dracodev.net/api/v1).
Usage:
python scripts/publish_release.py --tag v2.0.0
python scripts/publish_release.py --tag v2.0.0 --dry-run
python scripts/publish_release.py --tag v2.0.0 --token VOTRE_TOKEN_GITEA
Prérequis:
Variable d'environnement GITEA_TOKEN ou jeton défini dans .env ou via --token.
"""
import argparse
import hashlib
import json
import os
import pathlib
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
DEFAULT_GITEA_URL = "https://git.dracodev.net"
DEFAULT_REPO = "Projets/ObsiGate"
def load_env_token() -> str:
"""Recherche GITEA_TOKEN dans les variables d'environnement ou dans le fichier .env."""
token = os.environ.get("GITEA_TOKEN", "").strip()
if token:
return token
env_path = pathlib.Path(".env")
if env_path.exists():
with open(env_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("GITEA_TOKEN="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
return ""
def get_current_git_tag() -> str:
"""Tente de lire la version depuis tauri.conf.json ou pyproject.toml si pas de tag fourni."""
tauri_conf = pathlib.Path("desktop/tauri.conf.json")
if tauri_conf.exists():
try:
with open(tauri_conf, "r", encoding="utf-8") as f:
data = json.load(f)
ver = data.get("version")
if ver:
return f"v{ver}" if not ver.startswith("v") else ver
except Exception:
pass
return "v2.0.0"
def find_desktop_binaries(base_dir: pathlib.Path) -> list[pathlib.Path]:
"""Recherche les binaires Desktop générés par Tauri dans le répertoire desktop/."""
patterns = [
"target/**/release/bundle/nsis/*.exe",
"target/**/release/bundle/msi/*.msi",
"target/**/release/bundle/appimage/*.AppImage",
"target/**/release/bundle/deb/*.deb",
"release_assets/*",
]
found = []
desktop_path = base_dir / "desktop" if (base_dir / "desktop").exists() else base_dir
for pattern in patterns:
for p in desktop_path.glob(pattern):
if p.is_file() and not p.name.endswith(".pdb"):
found.append(p)
# Dédoublonner par nom de fichier (garder le plus récent si doublon)
unique_files: dict[str, pathlib.Path] = {}
for path in found:
name = path.name
if name not in unique_files or path.stat().st_mtime > unique_files[name].stat().st_mtime:
unique_files[name] = path
return sorted(list(unique_files.values()), key=lambda x: x.name)
def calculate_sha256(filepath: pathlib.Path) -> str:
"""Calcule l'empreinte SHA-256 d'un fichier."""
sha = hashlib.sha256()
with open(filepath, "rb") as f:
while chunk := f.read(65536):
sha.update(chunk)
return sha.hexdigest()
def generate_checksums_file(files: list[pathlib.Path], output_path: pathlib.Path) -> dict[str, str]:
"""Génère le fichier checksums.txt contenant SHA256 et noms de fichiers."""
checksums = {}
lines = []
for f in files:
digest = calculate_sha256(f)
checksums[f.name] = digest
lines.append(f"{digest} {f.name}")
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
return checksums
def make_gitea_request(
url: str,
method: str = "GET",
token: str = "",
data: bytes | None = None,
content_type: str | None = None,
) -> tuple[int, dict | str]:
"""Effectue une requête HTTP vers l'API REST Gitea."""
req = urllib.request.Request(url, method=method)
if token:
req.add_header("Authorization", f"token {token}")
if content_type:
req.add_header("Content-Type", content_type)
req.add_header("Accept", "application/json")
# Cloudflare sur git.dracodev.net bloque les User-Agents "Python-urllib/*"
# (HTTP 403 Error 1010) pour les écritures ; on présente un UA navigateur.
req.add_header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
)
try:
with urllib.request.urlopen(req, data=data) as resp:
body = resp.read().decode("utf-8")
status = resp.status
try:
return status, json.loads(body)
except Exception:
return status, body
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8")
try:
return e.code, json.loads(body)
except Exception:
return e.code, body
def upload_asset_to_gitea(
upload_url: str,
file_path: pathlib.Path,
token: str,
) -> bool:
"""Téléverse un fichier binaire vers l'API Gitea Release en Multipart Form."""
boundary = "----WebKitFormBoundary" + hashlib.md5(file_path.name.encode()).hexdigest()[:16]
with open(file_path, "rb") as f:
file_content = f.read()
filename = file_path.name
content_type = "application/octet-stream"
if filename.endswith(".exe"):
content_type = "application/x-msdownload"
elif filename.endswith(".msi"):
content_type = "application/x-ole-storage"
elif filename.endswith(".deb"):
content_type = "application/vnd.debian.binary-package"
elif filename.endswith(".txt"):
content_type = "text/plain"
body = []
body.append(f"--{boundary}".encode("utf-8"))
body.append(f'Content-Disposition: form-data; name="attachment"; filename="{filename}"'.encode("utf-8"))
body.append(f"Content-Type: {content_type}".encode("utf-8"))
body.append(b"")
body.append(file_content)
body.append(f"--{boundary}--".encode("utf-8"))
body.append(b"")
payload = b"\r\n".join(body)
status, resp = make_gitea_request(
url=upload_url,
method="POST",
token=token,
data=payload,
content_type=f"multipart/form-data; boundary={boundary}",
)
if status in (200, 201):
print(f" ✅ Uploadé : {filename} ({len(file_content) / (1024*1024):.2f} MB)")
return True
else:
print(f" ❌ Erreur upload pour {filename} (HTTP {status}) : {resp}")
return False
def main():
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
parser = argparse.ArgumentParser(description="Publier des binaires Desktop sur Gitea Releases.")
parser.add_argument("--tag", "-t", help="Tag de version (ex: v2.0.0)")
parser.add_argument("--token", help="Jeton d'accès API Gitea (GITEA_TOKEN)")
parser.add_argument("--gitea-url", default=DEFAULT_GITEA_URL, help=f"URL Gitea (défaut: {DEFAULT_GITEA_URL})")
parser.add_argument("--repo", default=DEFAULT_REPO, help=f"Dépôt Gitea (défaut: {DEFAULT_REPO})")
parser.add_argument("--notes", help="Notes de version (description textuelle)")
parser.add_argument("--dry-run", action="store_true", help="Simuler sans envoyer à Gitea")
parser.add_argument("--draft", action="store_true", help="Marquer la release comme brouillon")
parser.add_argument("--prerelease", action="store_true", help="Marquer comme pré-release")
args = parser.parse_args()
tag = args.tag or get_current_git_tag()
token = args.token or load_env_token()
gitea_url = args.gitea_url.rstrip("/")
repo = args.repo.strip("/")
print("==========================================================")
print(f" ObsiGate — Publication Gitea Release ({tag})")
print("==========================================================")
print(f"📍 Serveur Gitea : {gitea_url}")
print(f"📦 Dépôt : {repo}")
print(f"🏷️ Tag : {tag}")
print(f"🧪 Mode Dry-Run : {'OUI' if args.dry_run else 'NON'}")
# 1. Recherche des binaires
root_dir = pathlib.Path(__file__).resolve().parent.parent
binaries = find_desktop_binaries(root_dir)
if not binaries:
print("\n⚠️ Aucun binaire Desktop trouvé dans desktop/target/.")
print("💡 Pensez à exécuter au préalable :")
print(" - Windows : .\\desktop\\build-windows.bat")
print(" - Linux : ./desktop/build-linux.sh")
else:
print(f"\n🔍 {len(binaries)} binaire(s) trouvé(s) :")
for b in binaries:
size_mb = b.stat().st_size / (1024 * 1024)
print(f" • {b.name} ({size_mb:.2f} MB) -> {b}")
# 2. Génération de checksums.txt
checksums_path = root_dir / "desktop" / "checksums.txt"
all_files_to_upload = list(binaries)
if binaries:
print(f"\n🔐 Génération des empreintes SHA-256 ({checksums_path.name})...")
generate_checksums_file(binaries, checksums_path)
all_files_to_upload.append(checksums_path)
print(" ✅ checksums.txt généré avec succès")
if args.dry_run:
print("\n🧪 Mode Dry-Run terminé. Aucune action distante effectuée.")
return 0
if not token:
print("\n❌ ERREUR : Aucun jeton Gitea fourni.")
print("Veuillez définir la variable GITEA_TOKEN ou passer --token VOTRE_TOKEN.")
print("Pour générer un jeton sur Gitea : Mon Profil -> Paramètres -> Applications -> Générer un jeton")
return 1
if not all_files_to_upload:
print("\n❌ Aucun fichier à téléverser. Abandon.")
return 1
# 3. Récupérer ou Créer la Release sur Gitea
api_release_url = f"{gitea_url}/api/v1/repos/{repo}/releases"
print(f"\n📡 Vérification de la release '{tag}' sur Gitea...")
status, releases = make_gitea_request(api_release_url, token=token)
target_release = None
if status == 200 and isinstance(releases, list):
for rel in releases:
if rel.get("tag_name") == tag:
target_release = rel
break
release_body = args.notes or f"### ObsiGate {tag}\n\nPublication des binaires Desktop officiels (Windows & Linux)."
if target_release:
release_id = target_release["id"]
print(f"ℹ️ Release existante trouvée (ID: {release_id})")
else:
print(f"🆕 Création d'une nouvelle release pour le tag '{tag}'...")
payload = json.dumps({
"tag_name": tag,
"target_commitish": "main",
"name": f"ObsiGate {tag}",
"body": release_body,
"draft": args.draft,
"prerelease": args.prerelease,
}).encode("utf-8")
status, new_rel = make_gitea_request(
api_release_url,
method="POST",
token=token,
data=payload,
content_type="application/json",
)
if status in (200, 201) and isinstance(new_rel, dict):
target_release = new_rel
release_id = target_release["id"]
print(f"✅ Release créée avec succès (ID: {release_id})")
else:
print(f"❌ Impossible de créer la release (HTTP {status}) : {new_rel}")
return 1
# 4. Nettoyage des anciens assets portant le même nom si présents
existing_assets = target_release.get("assets", [])
upload_url = f"{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
print(f"\n📤 Téléversement de {len(all_files_to_upload)} fichier(s) vers la release Gitea...")
for file_path in all_files_to_upload:
# Supprimer si asset déjà existant
for asset in existing_assets:
if asset.get("name") == file_path.name:
asset_id = asset["id"]
del_url = f"{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}"
make_gitea_request(del_url, method="DELETE", token=token)
print(f" 🗑️ Ancien fichier {file_path.name} remplacé")
break
upload_asset_to_gitea(upload_url, file_path, token)
print("\n==========================================================")
print(f" 🎉 Publication terminée avec succès sur Gitea !")
print(f" 🔗 {gitea_url}/{repo}/releases/tag/{tag}")
print("==========================================================")
return 0
if __name__ == "__main__":
sys.exit(main())