feat: ShootTracker SQLite+JWT+YOLOv8
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
deploy_shoottracker.py
|
||||
──────────────────────
|
||||
Déploie ShootTracker sur domench.fr via Gitea + Coolify.
|
||||
|
||||
Architecture : Docker Compose — un seul dépôt contient
|
||||
• l'app principale (frontend React + backend Node/Express + SQLite)
|
||||
• le microservice IA (FastAPI + YOLOv8)
|
||||
|
||||
Usage :
|
||||
python deploy_shoottracker.py
|
||||
|
||||
Prérequis (variables d'environnement ou saisie interactive) :
|
||||
GITEA_URL ex. https://git.domench.fr
|
||||
GITEA_USER ex. gael
|
||||
GITEA_TOKEN token API Gitea avec droits repo
|
||||
COOLIFY_URL ex. https://coolify.domench.fr
|
||||
COOLIFY_TOKEN token API Coolify (Settings → API)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import secrets
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import textwrap
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
|
||||
# ─── Couleurs terminal ────────────────────────────────────────────────────────
|
||||
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
RED = "\033[91m"
|
||||
CYAN = "\033[96m"
|
||||
BOLD = "\033[1m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
def ok(msg): print(f"{GREEN}✓ {msg}{RESET}")
|
||||
def warn(msg): print(f"{YELLOW}⚠ {msg}{RESET}")
|
||||
def err(msg): print(f"{RED}✗ {msg}{RESET}")
|
||||
def info(msg): print(f"{CYAN}→ {msg}{RESET}")
|
||||
def step(n, msg): print(f"\n{BOLD}── Étape {n} : {msg} ──{RESET}")
|
||||
|
||||
# ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def api_call(url, method="GET", data=None, headers=None, token=None, base_url=None):
|
||||
"""Appel HTTP JSON simple (sans dépendance externe)."""
|
||||
full_url = url if url.startswith("http") else f"{base_url}{url}"
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
if token:
|
||||
h["Authorization"] = f"Bearer {token}"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(full_url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
raw = resp.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode(errors="replace")
|
||||
raise RuntimeError(f"HTTP {e.code} {e.reason} — {body_text}") from e
|
||||
|
||||
# ─── Configuration ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_config():
|
||||
def ask(env, prompt, secret=False):
|
||||
val = os.environ.get(env, "").strip()
|
||||
if val:
|
||||
return val
|
||||
if secret:
|
||||
import getpass
|
||||
return getpass.getpass(f" {prompt} : ").strip()
|
||||
return input(f" {prompt} : ").strip()
|
||||
|
||||
print(f"\n{BOLD}=== Déploiement ShootTracker ==={RESET}")
|
||||
print("Renseignez les paramètres (ou définissez les variables d'environnement) :\n")
|
||||
|
||||
cfg = {
|
||||
"gitea_url": ask("GITEA_URL", "Gitea URL (ex: https://git.domench.fr)").rstrip("/"),
|
||||
"gitea_user": ask("GITEA_USER", "Gitea username"),
|
||||
"gitea_token": ask("GITEA_TOKEN", "Gitea API token", secret=True),
|
||||
"coolify_url": ask("COOLIFY_URL", "Coolify URL (ex: https://coolify.domench.fr)").rstrip("/"),
|
||||
"coolify_token": ask("COOLIFY_TOKEN","Coolify API token", secret=True),
|
||||
}
|
||||
|
||||
# Paramètres optionnels avec valeurs par défaut
|
||||
cfg["repo_name"] = os.environ.get("REPO_NAME", "shoottracker")
|
||||
cfg["app_domain"] = os.environ.get("APP_DOMAIN", "shoottracker.domench.fr")
|
||||
|
||||
# JWT_SECRET généré automatiquement (64 octets hex = 128 chars)
|
||||
cfg["jwt_secret"] = secrets.token_hex(64)
|
||||
|
||||
return cfg
|
||||
|
||||
# ─── Étape 1 : Créer le dépôt Gitea ─────────────────────────────────────────
|
||||
|
||||
def create_gitea_repo(cfg):
|
||||
step(1, "Création du dépôt Gitea")
|
||||
base = cfg["gitea_url"]
|
||||
tok = cfg["gitea_token"]
|
||||
user = cfg["gitea_user"]
|
||||
name = cfg["repo_name"]
|
||||
|
||||
# Vérifier si le dépôt existe déjà
|
||||
try:
|
||||
api_call(f"{base}/api/v1/repos/{user}/{name}", token=tok)
|
||||
warn(f"Le dépôt {user}/{name} existe déjà → on continue")
|
||||
return f"{base}/{user}/{name}.git"
|
||||
except RuntimeError:
|
||||
pass # N'existe pas encore
|
||||
|
||||
resp = api_call(
|
||||
f"{base}/api/v1/user/repos",
|
||||
method="POST",
|
||||
data={
|
||||
"name": name,
|
||||
"description": "ShootTracker — application de suivi des performances de tir sportif",
|
||||
"private": True,
|
||||
"auto_init": False,
|
||||
},
|
||||
token=tok,
|
||||
)
|
||||
ok(f"Dépôt créé : {resp['html_url']}")
|
||||
return resp["clone_url"]
|
||||
|
||||
# ─── Étape 2 : Push du code vers Gitea ───────────────────────────────────────
|
||||
|
||||
def push_code(cfg, clone_url):
|
||||
step(2, "Push du code vers Gitea")
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(script_dir)
|
||||
|
||||
# Construire l'URL authentifiée
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
parsed = urlparse(clone_url)
|
||||
auth_url = urlunparse(parsed._replace(
|
||||
netloc=f"{cfg['gitea_user']}:{cfg['gitea_token']}@{parsed.netloc}"
|
||||
))
|
||||
|
||||
def run(cmd, **kw):
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Commande échouée : {cmd}\n{result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
# Init git si nécessaire
|
||||
if not os.path.isdir(".git"):
|
||||
run("git init")
|
||||
run("git checkout -b main")
|
||||
ok("Dépôt git initialisé")
|
||||
|
||||
# .gitignore
|
||||
gitignore = textwrap.dedent("""\
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
data/
|
||||
*.db
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
""")
|
||||
with open(".gitignore", "w") as f:
|
||||
f.write(gitignore)
|
||||
|
||||
# Remote
|
||||
try:
|
||||
run("git remote remove origin")
|
||||
except RuntimeError:
|
||||
pass
|
||||
run(f"git remote add origin {auth_url}")
|
||||
|
||||
run("git add -A")
|
||||
try:
|
||||
run('git commit -m "feat: ShootTracker — SQLite + JWT + YOLOv8 AI"')
|
||||
except RuntimeError:
|
||||
warn("Rien à committer (arbre propre)")
|
||||
|
||||
run("git push -u origin main --force")
|
||||
ok("Code pushé sur Gitea")
|
||||
|
||||
# ─── Étape 3 : Créer l'application Coolify ───────────────────────────────────
|
||||
|
||||
def create_coolify_app(cfg, clone_url):
|
||||
step(3, "Création de l'application Coolify")
|
||||
base = cfg["coolify_url"]
|
||||
tok = cfg["coolify_token"]
|
||||
|
||||
# Récupérer le premier serveur disponible
|
||||
servers = api_call(f"{base}/api/v1/servers", token=tok)
|
||||
if not servers:
|
||||
raise RuntimeError("Aucun serveur trouvé dans Coolify")
|
||||
server_uuid = servers[0]["uuid"]
|
||||
info(f"Serveur Coolify : {servers[0].get('name', server_uuid)}")
|
||||
|
||||
# Variables d'environnement de l'application
|
||||
env_vars = "\n".join([
|
||||
f"NODE_ENV=production",
|
||||
f"PORT=3001",
|
||||
f"JWT_SECRET={cfg['jwt_secret']}",
|
||||
f"AI_SERVICE_URL=http://shoottracker-ai:8000",
|
||||
f"CORS_ORIGIN=https://{cfg['app_domain']}",
|
||||
f"UPLOADS_DIR=/app/data/uploads",
|
||||
])
|
||||
|
||||
payload = {
|
||||
"name": "shoottracker",
|
||||
"description": "ShootTracker — tir sportif IA",
|
||||
"server_uuid": server_uuid,
|
||||
"git_repository": clone_url,
|
||||
"git_branch": "main",
|
||||
"build_pack": "dockercompose",
|
||||
"docker_compose_location": "/docker-compose.yml",
|
||||
"fqdn": f"https://{cfg['app_domain']}",
|
||||
"ports_exposes": "3001",
|
||||
"environment_variables": env_vars,
|
||||
"instant_deploy": False,
|
||||
}
|
||||
|
||||
# Vérifier si l'app existe déjà
|
||||
try:
|
||||
apps = api_call(f"{base}/api/v1/applications", token=tok)
|
||||
existing = next((a for a in apps if a.get("name") == "shoottracker"), None)
|
||||
if existing:
|
||||
app_uuid = existing["uuid"]
|
||||
warn(f"Application 'shoottracker' existe déjà (uuid={app_uuid}) → mise à jour des variables")
|
||||
# Mettre à jour les env vars
|
||||
api_call(
|
||||
f"{base}/api/v1/applications/{app_uuid}",
|
||||
method="PATCH",
|
||||
data={"environment_variables": env_vars},
|
||||
token=tok,
|
||||
)
|
||||
ok("Variables d'environnement mises à jour")
|
||||
return app_uuid
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resp = api_call(
|
||||
f"{base}/api/v1/applications",
|
||||
method="POST",
|
||||
data=payload,
|
||||
token=tok,
|
||||
)
|
||||
app_uuid = resp.get("uuid") or resp.get("id")
|
||||
ok(f"Application créée (uuid={app_uuid})")
|
||||
return app_uuid
|
||||
|
||||
# ─── Étape 4 : Déployer ───────────────────────────────────────────────────────
|
||||
|
||||
def trigger_deploy(cfg, app_uuid):
|
||||
step(4, "Déclenchement du déploiement")
|
||||
base = cfg["coolify_url"]
|
||||
tok = cfg["coolify_token"]
|
||||
|
||||
try:
|
||||
resp = api_call(
|
||||
f"{base}/api/v1/applications/{app_uuid}/deploy",
|
||||
method="POST",
|
||||
token=tok,
|
||||
)
|
||||
deploy_id = resp.get("deployment_uuid") or resp.get("id", "—")
|
||||
ok(f"Déploiement déclenché (id={deploy_id})")
|
||||
info(f"Suivez l'avancement sur : {base}/project/shoottracker/deployments")
|
||||
except RuntimeError as e:
|
||||
warn(f"Impossible de déclencher via API : {e}")
|
||||
info("Déclenchez manuellement depuis l'interface Coolify")
|
||||
|
||||
# ─── Résumé final ─────────────────────────────────────────────────────────────
|
||||
|
||||
def print_summary(cfg):
|
||||
box = "═" * 60
|
||||
print(f"\n{GREEN}{BOLD}{box}")
|
||||
print(" ✓ ShootTracker déployé avec succès !")
|
||||
print(box + RESET)
|
||||
print(f"""
|
||||
🌐 App : https://{cfg['app_domain']}
|
||||
📦 Gitea : {cfg['gitea_url']}/{cfg['gitea_user']}/{cfg['repo_name']}
|
||||
🚀 Coolify : {cfg['coolify_url']}
|
||||
|
||||
{YELLOW}⚠ Conservez ce JWT_SECRET en lieu sûr (non récupérable) :{RESET}
|
||||
{BOLD}{cfg['jwt_secret']}{RESET}
|
||||
|
||||
Données persistées dans le volume Docker : shoottracker_data
|
||||
(SQLite DB + photos cibles + avatars)
|
||||
""")
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
try:
|
||||
cfg = get_config()
|
||||
clone_url = create_gitea_repo(cfg)
|
||||
push_code(cfg, clone_url)
|
||||
app_uuid = create_coolify_app(cfg, clone_url)
|
||||
trigger_deploy(cfg, app_uuid)
|
||||
print_summary(cfg)
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{YELLOW}Annulé.{RESET}")
|
||||
sys.exit(1)
|
||||
except RuntimeError as e:
|
||||
err(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user