Cursor: Synchronisation automatique Gitea → GitHub #1

Merged
perco merged 1 commits from cursor-sync-gitea-github into main 2026-03-18 11:23:22 +01:00
6 changed files with 526 additions and 13 deletions
Showing only changes of commit efb428b4e7 - Show all commits
+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 | | Catégorie | Nombre | Exemples |
|-----------|--------|---------| |-----------|--------|---------|
| Système / MH / Plex / Location | 11 | Backups, monitoring, rapports Plex | | Système / MH / Plex / Location | 11 | Backups, monitoring, rapports Plex |
| Potager / Maison | 24 | Arrosage, alertes, automatisations maison | | 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/ cronhub/
├── app/ ├── app/
│ ├── __init__.py │ ├── __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/ ├── templates/
│ ├── index.html # Dashboard (avec filtre par catégorie) │ ├── index.html # Dashboard (avec filtre par catégorie)
│ ├── job_detail.html # Détail d'un job + logs │ ├── job_detail.html # Détail d'un job + logs
│ └── job_form.html # Formulaire création/édition (champ category) │ └── job_form.html # Formulaire création/édition (champ category)
├── data/ # Volume persistant (SQLite) ├── data/ # Volume persistant (SQLite)
├── Dockerfile ├── Dockerfile
├── docker-compose.yml ├── docker-compose.yml
└── requirements.txt └── requirements.txt
+23 -7
View File
@@ -42,6 +42,7 @@ def init_db():
schedule TEXT NOT NULL, schedule TEXT NOT NULL,
command TEXT NOT NULL, command TEXT NOT NULL,
description TEXT DEFAULT '', description TEXT DEFAULT '',
category TEXT DEFAULT '',
enabled INTEGER DEFAULT 1, enabled INTEGER DEFAULT 1,
last_run TEXT, last_run TEXT,
last_status TEXT DEFAULT 'never', last_status TEXT DEFAULT 'never',
@@ -59,7 +60,16 @@ def init_db():
); );
""") """)
conn.commit() 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: def row_to_dict(row) -> dict:
@@ -220,6 +230,7 @@ class JobCreate(BaseModel):
schedule: str schedule: str
command: str command: str
description: Optional[str] = "" description: Optional[str] = ""
category: Optional[str] = ""
enabled: Optional[bool] = True enabled: Optional[bool] = True
@@ -228,6 +239,7 @@ class JobUpdate(BaseModel):
schedule: Optional[str] = None schedule: Optional[str] = None
command: Optional[str] = None command: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
category: Optional[str] = None
enabled: Optional[bool] = None enabled: Optional[bool] = None
@@ -255,9 +267,9 @@ def api_create_job(payload: JobCreate):
now = datetime.now().isoformat() now = datetime.now().isoformat()
conn = get_db() conn = get_db()
conn.execute( 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, (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.commit()
conn.close() conn.close()
@@ -295,6 +307,8 @@ def api_update_job(job_id: str, payload: JobUpdate):
updates["command"] = payload.command updates["command"] = payload.command
if payload.description is not None: if payload.description is not None:
updates["description"] = payload.description updates["description"] = payload.description
if payload.category is not None:
updates["category"] = payload.category
if payload.enabled is not None: if payload.enabled is not None:
updates["enabled"] = int(payload.enabled) updates["enabled"] = int(payload.enabled)
@@ -398,6 +412,7 @@ def ui_create_job(
schedule: str = Form(...), schedule: str = Form(...),
command: str = Form(...), command: str = Form(...),
description: str = Form(""), description: str = Form(""),
category: str = Form(""),
enabled: str = Form("on"), enabled: str = Form("on"),
): ):
try: try:
@@ -418,8 +433,8 @@ def ui_create_job(
is_enabled = 1 if enabled == "on" else 0 is_enabled = 1 if enabled == "on" else 0
conn = get_db() conn = get_db()
conn.execute( 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, name, schedule, command, description, is_enabled, now) (job_id, name, schedule, command, description, category, is_enabled, now)
) )
conn.commit() conn.commit()
conn.close() conn.close()
@@ -465,6 +480,7 @@ def ui_update_job(
schedule: str = Form(...), schedule: str = Form(...),
command: str = Form(...), command: str = Form(...),
description: str = Form(""), description: str = Form(""),
category: str = Form(""),
enabled: str = Form("off"), enabled: str = Form("off"),
): ):
job = get_job(job_id) job = get_job(job_id)
@@ -485,8 +501,8 @@ def ui_update_job(
is_enabled = 1 if enabled == "on" else 0 is_enabled = 1 if enabled == "on" else 0
conn = get_db() conn = get_db()
conn.execute( conn.execute(
"UPDATE jobs SET name=?,schedule=?,command=?,description=?,enabled=? WHERE id=?", "UPDATE jobs SET name=?,schedule=?,command=?,description=?,category=?,enabled=? WHERE id=?",
(name, schedule, command, description, is_enabled, job_id) (name, schedule, command, description, category, is_enabled, job_id)
) )
conn.commit() conn.commit()
conn.close() 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()