Ajout système de synchronisation automatique Gitea → GitHub

- Ajout du script sync_gitea_to_github.py pour synchroniser tous les repos
- Ajout du script init_sync_job.py pour créer le cron job dans CronHub
- Ajout du champ 'category' au modèle Job pour classification
- Configuration du job pour exécution toutes les heures (0 * * * *)
- Documentation complète dans scripts/README_SYNC.md
- Ajout .gitignore pour exclure __pycache__ et fichiers temporaires

Co-authored-by: Cédric TOMAZ <percolouco@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-03-18 09:33:11 +00:00
co-authored by Cédric TOMAZ
parent 985e206426
commit efb428b4e7
6 changed files with 526 additions and 13 deletions
+41
View File
@@ -0,0 +1,41 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Database
*.db
*.db-shm
*.db-wal
data/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
+20 -6
View File
@@ -43,12 +43,22 @@ CronHub permet de créer, gérer et monitorer des tâches planifiées (cron jobs
---
## 📊 Jobs configurés (35 au total)
## 📊 Jobs configurés
| Catégorie | Nombre | Exemples |
|-----------|--------|---------|
| Système / MH / Plex / Location | 11 | Backups, monitoring, rapports Plex |
| Potager / Maison | 24 | Arrosage, alertes, automatisations maison |
| Sync | 1 | Synchronisation Gitea → GitHub |
### 🔄 Job de synchronisation Gitea → GitHub
Un job automatique synchronise tous les dépôts de Gitea (`http://192.168.1.29:3500/perco`) vers GitHub (`percolouco`) :
- **Fréquence** : Toutes les heures (`0 * * * *`)
- **Mode** : Mirror (toutes les branches et tags)
- **Scripts** : Voir `scripts/README_SYNC.md` pour la configuration complète
- **Tokens requis** : `GITHUB_TOKEN` et `GITEA_TOKEN` (configurés comme secrets)
---
@@ -119,12 +129,16 @@ curl https://cronhub.nas.percolouco.com/api/jobs/{id}/logs?limit=20
cronhub/
├── app/
│ ├── __init__.py
│ └── main.py # Application FastAPI (routes, scheduler, DB)
│ └── main.py # Application FastAPI (routes, scheduler, DB)
├── scripts/
│ ├── sync_gitea_to_github.py # Script de synchronisation Gitea → GitHub
│ ├── init_sync_job.py # Initialisation du job de sync dans CronHub
│ └── README_SYNC.md # Documentation du système de sync
├── templates/
│ ├── index.html # Dashboard (avec filtre par catégorie)
│ ├── job_detail.html # Détail d'un job + logs
│ └── job_form.html # Formulaire création/édition (champ category)
├── data/ # Volume persistant (SQLite)
│ ├── index.html # Dashboard (avec filtre par catégorie)
│ ├── job_detail.html # Détail d'un job + logs
│ └── job_form.html # Formulaire création/édition (champ category)
├── data/ # Volume persistant (SQLite)
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
+23 -7
View File
@@ -42,6 +42,7 @@ def init_db():
schedule TEXT NOT NULL,
command TEXT NOT NULL,
description TEXT DEFAULT '',
category TEXT DEFAULT '',
enabled INTEGER DEFAULT 1,
last_run TEXT,
last_status TEXT DEFAULT 'never',
@@ -59,7 +60,16 @@ def init_db():
);
""")
conn.commit()
conn.close()
# Migration: ajouter la colonne category si elle n'existe pas
try:
conn = get_db()
conn.execute("ALTER TABLE jobs ADD COLUMN category TEXT DEFAULT ''")
conn.commit()
conn.close()
except sqlite3.OperationalError:
# La colonne existe déjà
pass
def row_to_dict(row) -> dict:
@@ -220,6 +230,7 @@ class JobCreate(BaseModel):
schedule: str
command: str
description: Optional[str] = ""
category: Optional[str] = ""
enabled: Optional[bool] = True
@@ -228,6 +239,7 @@ class JobUpdate(BaseModel):
schedule: Optional[str] = None
command: Optional[str] = None
description: Optional[str] = None
category: Optional[str] = None
enabled: Optional[bool] = None
@@ -255,9 +267,9 @@ def api_create_job(payload: JobCreate):
now = datetime.now().isoformat()
conn = get_db()
conn.execute(
"INSERT INTO jobs (id,name,schedule,command,description,enabled,created_at) VALUES (?,?,?,?,?,?,?)",
"INSERT INTO jobs (id,name,schedule,command,description,category,enabled,created_at) VALUES (?,?,?,?,?,?,?,?)",
(job_id, payload.name, payload.schedule, payload.command,
payload.description or "", int(payload.enabled), now)
payload.description or "", payload.category or "", int(payload.enabled), now)
)
conn.commit()
conn.close()
@@ -295,6 +307,8 @@ def api_update_job(job_id: str, payload: JobUpdate):
updates["command"] = payload.command
if payload.description is not None:
updates["description"] = payload.description
if payload.category is not None:
updates["category"] = payload.category
if payload.enabled is not None:
updates["enabled"] = int(payload.enabled)
@@ -398,6 +412,7 @@ def ui_create_job(
schedule: str = Form(...),
command: str = Form(...),
description: str = Form(""),
category: str = Form(""),
enabled: str = Form("on"),
):
try:
@@ -418,8 +433,8 @@ def ui_create_job(
is_enabled = 1 if enabled == "on" else 0
conn = get_db()
conn.execute(
"INSERT INTO jobs (id,name,schedule,command,description,enabled,created_at) VALUES (?,?,?,?,?,?,?)",
(job_id, name, schedule, command, description, is_enabled, now)
"INSERT INTO jobs (id,name,schedule,command,description,category,enabled,created_at) VALUES (?,?,?,?,?,?,?,?)",
(job_id, name, schedule, command, description, category, is_enabled, now)
)
conn.commit()
conn.close()
@@ -465,6 +480,7 @@ def ui_update_job(
schedule: str = Form(...),
command: str = Form(...),
description: str = Form(""),
category: str = Form(""),
enabled: str = Form("off"),
):
job = get_job(job_id)
@@ -485,8 +501,8 @@ def ui_update_job(
is_enabled = 1 if enabled == "on" else 0
conn = get_db()
conn.execute(
"UPDATE jobs SET name=?,schedule=?,command=?,description=?,enabled=? WHERE id=?",
(name, schedule, command, description, is_enabled, job_id)
"UPDATE jobs SET name=?,schedule=?,command=?,description=?,category=?,enabled=? WHERE id=?",
(name, schedule, command, description, category, is_enabled, job_id)
)
conn.commit()
conn.close()
+128
View File
@@ -0,0 +1,128 @@
# Synchronisation Gitea → GitHub
Ce dossier contient les scripts pour synchroniser automatiquement tous les dépôts Gitea vers GitHub.
## 📋 Description
Le système synchronise tous les dépôts de l'utilisateur `perco` depuis le serveur Gitea (`http://192.168.1.29:3500`) vers le compte GitHub `percolouco`, en mode mirror (toutes les branches et tags).
## 🔑 Configuration des secrets
Les tokens d'accès doivent être configurés comme secrets dans le Cursor Dashboard (Cloud Agents > Secrets) :
1. **GITHUB_TOKEN** (requis)
- Token d'accès personnel GitHub avec les permissions `repo`
- Générer sur : https://github.com/settings/tokens
- Scopes nécessaires : `repo`, `workflow`
2. **GITEA_TOKEN** (optionnel mais recommandé)
- Token d'accès Gitea pour les dépôts privés
- Générer sur : http://192.168.1.29:3500/user/settings/applications
## 📂 Fichiers
- **`sync_gitea_to_github.py`** : Script principal de synchronisation
- **`init_sync_job.py`** : Script d'initialisation du cron job dans CronHub
- **`README_SYNC.md`** : Ce fichier
## 🚀 Installation
### 1. Configurer les secrets
Dans le Cursor Dashboard :
- Allez dans Cloud Agents > Secrets
- Ajoutez `GITHUB_TOKEN` avec votre token GitHub
- Ajoutez `GITEA_TOKEN` avec votre token Gitea (optionnel)
### 2. Initialiser le cron job
Exécutez le script d'initialisation pour créer le job dans CronHub :
```bash
python3 /opt/container/cronhub/scripts/init_sync_job.py
```
Le job sera configuré pour s'exécuter **toutes les heures** (schedule cron : `0 * * * *`).
### 3. Vérifier
Accédez à CronHub pour vérifier que le job a été créé :
- URL : https://cronhub.nas.percolouco.com
- Nom du job : "Sync Gitea → GitHub"
- Catégorie : "sync"
## 🔧 Fonctionnement
Le script effectue les opérations suivantes :
1. **Liste les dépôts Gitea** via l'API Gitea (`/api/v1/user/repos`)
2. Pour chaque dépôt :
- Vérifie si le dépôt existe sur GitHub
- Crée le dépôt sur GitHub s'il n'existe pas
- Clone le dépôt Gitea en mode `--mirror` (toutes les branches et tags)
- Push en mode `--mirror` vers GitHub
3. **Affiche un résumé** avec le nombre de réussites/échecs
## 📊 Logs et monitoring
Les logs d'exécution sont disponibles dans CronHub :
- Accédez au détail du job "Sync Gitea → GitHub"
- Consultez l'historique des exécutions
- Stdout/stderr sont capturés pour chaque exécution
## 🧪 Test manuel
Pour tester le script manuellement (hors cron) :
```bash
# Définir les tokens temporairement
export GITHUB_TOKEN="votre_token_github"
export GITEA_TOKEN="votre_token_gitea"
# Exécuter le script
python3 /opt/container/cronhub/scripts/sync_gitea_to_github.py
```
Ou via CronHub, en utilisant le bouton "Run Now" sur la page de détail du job.
## 📝 Notes importantes
- **Mode mirror** : Synchronise TOUTES les branches et tags (pas seulement `main`)
- **Dépôts privés** : Les dépôts privés sur Gitea restent privés sur GitHub
- **Timeout** : Chaque opération git a un timeout de 5 minutes (300s)
- **Exécution via nsenter** : Le script s'exécute dans le namespace de l'hôte Docker pour accès à git et python3
## 🔍 Dépannage
### Le script échoue avec "GITHUB_TOKEN non défini"
Vérifiez que le secret `GITHUB_TOKEN` est bien configuré dans le Cursor Dashboard et qu'il est injecté dans l'environnement du container CronHub.
### Erreur lors du clone Gitea
Vérifiez que :
- Le serveur Gitea est accessible (`http://192.168.1.29:3500`)
- Le token Gitea est valide (pour les dépôts privés)
- L'utilisateur `perco` existe et a des dépôts
### Erreur lors du push GitHub
Vérifiez que :
- Le token GitHub a les bonnes permissions (`repo`, `workflow`)
- Le compte GitHub `percolouco` existe
- Il n'y a pas de limite de taille de dépôt atteinte
## 📅 Modification du planning
Pour changer la fréquence de synchronisation :
1. Éditez le job dans CronHub (interface web ou API)
2. Modifiez le champ `schedule` avec une nouvelle expression cron :
- Toutes les heures : `0 * * * *` (actuel)
- Toutes les 2 heures : `0 */2 * * *`
- Tous les jours à 3h : `0 3 * * *`
- Toutes les 30 minutes : `*/30 * * * *`
## 📄 Licence
Projet privé — perco / NAS domestique
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Script d'initialisation du job de synchronisation Gitea → GitHub dans CronHub
Crée ou met à jour le cron job pour une exécution toutes les heures
"""
import json
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# Configuration
CRONHUB_URL = "https://cronhub.nas.percolouco.com"
JOB_NAME = "Sync Gitea → GitHub"
JOB_CATEGORY = "sync"
JOB_SCHEDULE = "0 * * * *" # Toutes les heures à la minute 0
JOB_COMMAND = "nsenter --target 1 --mount --uts --ipc --net --pid -- python3 /opt/container/cronhub/scripts/sync_gitea_to_github.py"
JOB_DESCRIPTION = "Synchronisation automatique des dépôts Gitea (http://192.168.1.29:3500/perco) vers GitHub (percolouco)"
def log(message):
"""Affiche un message"""
print(f"[INFO] {message}")
def api_request(url, method="GET", data=None):
"""Effectue une requête HTTP vers l'API CronHub"""
headers = {"Content-Type": "application/json"}
if data:
data = json.dumps(data).encode('utf-8')
req = Request(url, headers=headers, data=data, method=method)
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
error_msg = e.read().decode('utf-8')
log(f"Erreur HTTP {e.code} pour {url}: {error_msg}")
return None
except URLError as e:
log(f"Erreur URL pour {url}: {e.reason}")
return None
def find_existing_job():
"""Cherche si un job avec le même nom existe déjà"""
log(f"Recherche d'un job existant nommé '{JOB_NAME}'...")
url = f"{CRONHUB_URL}/api/jobs"
jobs = api_request(url)
if jobs is None:
log("Impossible de récupérer la liste des jobs")
return None
for job in jobs:
if job.get("name") == JOB_NAME:
log(f"Job existant trouvé: ID={job['id']}")
return job
log("Aucun job existant trouvé")
return None
def create_job():
"""Crée un nouveau job dans CronHub"""
log("Création d'un nouveau job...")
url = f"{CRONHUB_URL}/api/jobs"
data = {
"name": JOB_NAME,
"schedule": JOB_SCHEDULE,
"command": JOB_COMMAND,
"description": JOB_DESCRIPTION,
"category": JOB_CATEGORY,
"enabled": True
}
result = api_request(url, method="POST", data=data)
if result:
log(f"✓ Job créé avec succès: ID={result['id']}")
return True
else:
log("✗ Échec de la création du job")
return False
def update_job(job_id):
"""Met à jour un job existant dans CronHub"""
log(f"Mise à jour du job existant: ID={job_id}")
url = f"{CRONHUB_URL}/api/jobs/{job_id}"
data = {
"name": JOB_NAME,
"schedule": JOB_SCHEDULE,
"command": JOB_COMMAND,
"description": JOB_DESCRIPTION,
"category": JOB_CATEGORY,
"enabled": True
}
result = api_request(url, method="PUT", data=data)
if result:
log(f"✓ Job mis à jour avec succès")
return True
else:
log("✗ Échec de la mise à jour du job")
return False
def main():
"""Fonction principale"""
log("=== Initialisation du job de synchronisation Gitea → GitHub ===")
# Vérifier si le job existe déjà
existing_job = find_existing_job()
if existing_job:
# Mettre à jour le job existant
success = update_job(existing_job["id"])
else:
# Créer un nouveau job
success = create_job()
if success:
log("=== Configuration terminée avec succès ===")
log(f"Le job s'exécutera toutes les heures (schedule: {JOB_SCHEDULE})")
log(f"Vérifiez sur {CRONHUB_URL}")
sys.exit(0)
else:
log("=== Échec de la configuration ===")
sys.exit(1)
if __name__ == "__main__":
main()
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
Script de synchronisation des dépôts Gitea vers GitHub
Synchronise tous les dépôts depuis http://192.168.1.29:3500/perco vers GitHub (percolouco)
"""
import os
import sys
import json
import subprocess
import tempfile
import shutil
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# Configuration
GITEA_URL = "http://192.168.1.29:3500"
GITEA_USER = "perco"
GITHUB_USER = "percolouco"
# Les tokens doivent être configurés comme secrets dans Cursor Dashboard
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
def log(message):
"""Affiche un message avec timestamp"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}")
def api_request(url, token=None, method="GET", data=None):
"""Effectue une requête HTTP avec gestion du token"""
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
if data:
headers["Content-Type"] = "application/json"
data = json.dumps(data).encode('utf-8')
req = Request(url, headers=headers, data=data, method=method)
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
log(f"Erreur HTTP {e.code} pour {url}: {e.read().decode('utf-8')}")
return None
except URLError as e:
log(f"Erreur URL pour {url}: {e.reason}")
return None
def get_gitea_repos():
"""Liste tous les dépôts de l'utilisateur Gitea"""
log(f"Récupération des dépôts Gitea pour {GITEA_USER}...")
url = f"{GITEA_URL}/api/v1/user/repos"
repos = api_request(url, token=GITEA_TOKEN)
if repos is None:
log("Erreur lors de la récupération des dépôts Gitea")
return []
log(f"Trouvé {len(repos)} dépôts Gitea")
return repos
def github_repo_exists(repo_name):
"""Vérifie si un dépôt existe sur GitHub"""
url = f"https://api.github.com/repos/{GITHUB_USER}/{repo_name}"
result = api_request(url, token=GITHUB_TOKEN)
return result is not None
def create_github_repo(repo_name, description="", is_private=False):
"""Crée un dépôt sur GitHub"""
log(f"Création du dépôt GitHub: {repo_name}")
url = "https://api.github.com/user/repos"
data = {
"name": repo_name,
"description": description,
"private": is_private,
"auto_init": False
}
result = api_request(url, token=GITHUB_TOKEN, method="POST", data=data)
return result is not None
def sync_repo(gitea_repo):
"""Synchronise un dépôt Gitea vers GitHub en mode mirror"""
repo_name = gitea_repo["name"]
log(f"Début de la synchronisation: {repo_name}")
# Construire l'URL clone avec token
clone_url = gitea_repo["clone_url"]
if GITEA_TOKEN:
# Insérer le token dans l'URL
clone_url = clone_url.replace("http://", f"http://oauth2:{GITEA_TOKEN}@")
# URL GitHub avec token
github_url = f"https://{GITHUB_TOKEN}@github.com/{GITHUB_USER}/{repo_name}.git"
# Créer le dépôt GitHub s'il n'existe pas
if not github_repo_exists(repo_name):
description = gitea_repo.get("description", "")
is_private = gitea_repo.get("private", False)
if not create_github_repo(repo_name, description, is_private):
log(f"Échec de la création du dépôt GitHub: {repo_name}")
return False
# Créer un répertoire temporaire
temp_dir = tempfile.mkdtemp(prefix=f"gitea_sync_{repo_name}_")
try:
log(f"Clone mirror depuis Gitea: {repo_name}")
# Clone en mode mirror
result = subprocess.run(
["git", "clone", "--mirror", clone_url, temp_dir],
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
log(f"Erreur lors du clone: {result.stderr}")
return False
log(f"Push mirror vers GitHub: {repo_name}")
# Push en mode mirror vers GitHub
result = subprocess.run(
["git", "push", "--mirror", github_url],
cwd=temp_dir,
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
log(f"Erreur lors du push: {result.stderr}")
return False
log(f"✓ Synchronisation réussie: {repo_name}")
return True
except subprocess.TimeoutExpired:
log(f"Timeout lors de la synchronisation de {repo_name}")
return False
except Exception as e:
log(f"Erreur lors de la synchronisation de {repo_name}: {e}")
return False
finally:
# Nettoyer le répertoire temporaire
try:
shutil.rmtree(temp_dir)
except Exception as e:
log(f"Erreur lors du nettoyage de {temp_dir}: {e}")
def main():
"""Fonction principale"""
log("=== Début de la synchronisation Gitea → GitHub ===")
# Vérifier les tokens
if not GITEA_TOKEN:
log("ATTENTION: GITEA_TOKEN non défini, l'accès aux dépôts privés peut échouer")
if not GITHUB_TOKEN:
log("ERREUR: GITHUB_TOKEN non défini (requis)")
sys.exit(1)
# Récupérer la liste des dépôts
repos = get_gitea_repos()
if not repos:
log("Aucun dépôt à synchroniser")
return
# Synchroniser chaque dépôt
success_count = 0
fail_count = 0
for repo in repos:
if sync_repo(repo):
success_count += 1
else:
fail_count += 1
# Résumé
log("=== Synchronisation terminée ===")
log(f"Réussis: {success_count}/{len(repos)}")
log(f"Échecs: {fail_count}/{len(repos)}")
if fail_count > 0:
sys.exit(1)
if __name__ == "__main__":
main()