Compare commits
4
Commits
985e206426
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d19fc0bbf8 | ||
|
|
bb9fe626ea | ||
|
|
e0c822d093 | ||
|
|
efb428b4e7 |
+41
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -120,6 +130,10 @@ 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
|
||||||
|
|||||||
+38
-8
@@ -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()
|
||||||
|
|
||||||
|
# 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()
|
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)
|
||||||
|
|
||||||
@@ -366,16 +380,30 @@ def api_get_logs(job_id: str, limit: int = 50):
|
|||||||
# UI Routes
|
# UI Routes
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def ui_index(request: Request):
|
def ui_index(request: Request, filter: Optional[str] = None):
|
||||||
jobs = get_all_jobs()
|
jobs = get_all_jobs()
|
||||||
total = len(jobs)
|
total = len(jobs)
|
||||||
active = sum(1 for j in jobs if j["enabled"])
|
active = sum(1 for j in jobs if j["enabled"])
|
||||||
failed = sum(1 for j in jobs if j["last_status"] == "failed")
|
failed = sum(1 for j in jobs if j["last_status"] == "failed")
|
||||||
success = sum(1 for j in jobs if j["last_status"] == "success")
|
success = sum(1 for j in jobs if j["last_status"] == "success")
|
||||||
|
|
||||||
|
VALID_FILTERS = {"total", "active", "success", "failed"}
|
||||||
|
active_filter = filter if filter in VALID_FILTERS else None
|
||||||
|
|
||||||
|
if active_filter == "active":
|
||||||
|
filtered_jobs = [j for j in jobs if j["enabled"]]
|
||||||
|
elif active_filter == "success":
|
||||||
|
filtered_jobs = [j for j in jobs if j["last_status"] == "success"]
|
||||||
|
elif active_filter == "failed":
|
||||||
|
filtered_jobs = [j for j in jobs if j["last_status"] == "failed"]
|
||||||
|
else:
|
||||||
|
filtered_jobs = jobs
|
||||||
|
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"jobs": jobs,
|
"jobs": filtered_jobs,
|
||||||
"page": "dashboard",
|
"page": "dashboard",
|
||||||
|
"active_filter": active_filter,
|
||||||
"stats": {"total": total, "active": active, "failed": failed, "success": success},
|
"stats": {"total": total, "active": active, "failed": failed, "success": success},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -398,6 +426,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 +447,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 +494,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 +515,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()
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+126
@@ -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()
|
||||||
Executable
+188
@@ -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()
|
||||||
+31
-8
@@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<div class="grid grid-cols-4 gap-4 mb-8">
|
<div class="grid grid-cols-4 gap-4 mb-8">
|
||||||
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<a href="{{ '/' if active_filter == 'total' else '/?filter=total' }}"
|
||||||
|
class="bg-white rounded-xl p-5 shadow-sm border transition hover:shadow-md
|
||||||
|
{{ 'border-blue-400 ring-2 ring-blue-200' if active_filter == 'total' else 'border-slate-100 hover:border-blue-200' }}">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
<i class="fas fa-list text-blue-600"></i>
|
<i class="fas fa-list text-blue-600"></i>
|
||||||
@@ -25,8 +27,10 @@
|
|||||||
<p class="text-xs text-slate-500">Total</p>
|
<p class="text-xs text-slate-500">Total</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<a href="{{ '/' if active_filter == 'active' else '/?filter=active' }}"
|
||||||
|
class="bg-white rounded-xl p-5 shadow-sm border transition hover:shadow-md
|
||||||
|
{{ 'border-green-400 ring-2 ring-green-200' if active_filter == 'active' else 'border-slate-100 hover:border-green-200' }}">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||||
<i class="fas fa-circle-check text-green-600"></i>
|
<i class="fas fa-circle-check text-green-600"></i>
|
||||||
@@ -36,8 +40,10 @@
|
|||||||
<p class="text-xs text-slate-500">Actifs</p>
|
<p class="text-xs text-slate-500">Actifs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<a href="{{ '/' if active_filter == 'success' else '/?filter=success' }}"
|
||||||
|
class="bg-white rounded-xl p-5 shadow-sm border transition hover:shadow-md
|
||||||
|
{{ 'border-emerald-400 ring-2 ring-emerald-200' if active_filter == 'success' else 'border-slate-100 hover:border-emerald-200' }}">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
<div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||||
<i class="fas fa-check text-emerald-600"></i>
|
<i class="fas fa-check text-emerald-600"></i>
|
||||||
@@ -47,8 +53,10 @@
|
|||||||
<p class="text-xs text-slate-500">Succès</p>
|
<p class="text-xs text-slate-500">Succès</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<a href="{{ '/' if active_filter == 'failed' else '/?filter=failed' }}"
|
||||||
|
class="bg-white rounded-xl p-5 shadow-sm border transition hover:shadow-md
|
||||||
|
{{ 'border-red-400 ring-2 ring-red-200' if active_filter == 'failed' else 'border-slate-100 hover:border-red-200' }}">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
||||||
<i class="fas fa-triangle-exclamation text-red-600"></i>
|
<i class="fas fa-triangle-exclamation text-red-600"></i>
|
||||||
@@ -58,13 +66,28 @@
|
|||||||
<p class="text-xs text-slate-500">Échecs</p>
|
<p class="text-xs text-slate-500">Échecs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Jobs table -->
|
<!-- Jobs table -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<h3 class="font-semibold text-slate-700">Jobs planifiés</h3>
|
<h3 class="font-semibold text-slate-700">Jobs planifiés</h3>
|
||||||
|
{% if active_filter %}
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold
|
||||||
|
{% if active_filter == 'failed' %}bg-red-100 text-red-700
|
||||||
|
{% elif active_filter == 'success' %}bg-emerald-100 text-emerald-700
|
||||||
|
{% elif active_filter == 'active' %}bg-green-100 text-green-700
|
||||||
|
{% else %}bg-blue-100 text-blue-700{% endif %}">
|
||||||
|
<i class="fas fa-filter text-xs"></i>
|
||||||
|
{{ {'total': 'Tous', 'active': 'Actifs', 'success': 'Succès', 'failed': 'Échecs'}[active_filter] }}
|
||||||
|
</span>
|
||||||
|
<a href="/" class="text-xs text-slate-400 hover:text-slate-600 transition" title="Effacer le filtre">
|
||||||
|
<i class="fas fa-xmark"></i> Effacer
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="text-xs text-slate-400">Rafraîchissement auto toutes les 30s</span>
|
<span class="text-xs text-slate-400">Rafraîchissement auto toutes les 30s</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user