🚀 Initialer Commit: Gitea Sync AI Service mit Docker + OpenWebUI-Vorbereitung
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
Dockerfile*
|
||||||
|
.docker/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Hier deine Werte eintragen
|
||||||
|
GITEE_URL=https://git.carabella.ch/
|
||||||
|
ACCESS_TOKEN=
|
||||||
|
COMMIT_NAME=
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Gitea Konfiguration – optional .env
|
||||||
|
GITEE_URL=https://git.carabella.ch/
|
||||||
|
ACCESS_TOKEN=dein_gitea_token_hier
|
||||||
|
COMMIT_NAME=KI-Sparringpartner
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Build stage (optional hier für jetzt reicht)
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Applikation kopieren
|
||||||
|
COPY app/ ./app
|
||||||
|
|
||||||
|
# Exponierter Port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Starte den Server
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# 🧠 Gitea Sync AI – Docker-Service
|
||||||
|
|
||||||
|
Ein lokaler Service, um Gitea-Repos abonnieren, mit KI bearbeiten und wieder pushen zu können.
|
||||||
|
Ideal für Nutzung mit **OpenWebUI** im Unraid!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Verwendung
|
||||||
|
|
||||||
|
### 1️⃣ Docker Image bauen (lokal oder via Gitea)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t gitea-sync-ai .
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder binde es als GitLab- oder GitHub-Action ein, um automatisiert Images zu publischen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ Docker Compose starten
|
||||||
|
|
||||||
|
Vorher `.env` anlegen:
|
||||||
|
|
||||||
|
**`.env`** (Beispiel):
|
||||||
|
```
|
||||||
|
GITEE_URL=https://git.carabella.ch/
|
||||||
|
ACCESS_TOKEN=hier_dein_gitea_pat
|
||||||
|
COMMIT_NAME=KI-Sparringpartner
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Unraid-Nutzung (Docker-Container direkt)
|
||||||
|
|
||||||
|
1. In Unraid → **Apps** -> + Add Container
|
||||||
|
2. Name: `gitea-sync-ai`
|
||||||
|
3. Image: lade es entweder lokal hoch (`localhost/gitea-sync-ai`) oder nutz ein Registry-Image
|
||||||
|
4. Port: `8000 → 8000`
|
||||||
|
5. Volume: `/home/user/projects:/projects`
|
||||||
|
6. Environment-Variablen setzen (ohne .env):
|
||||||
|
```
|
||||||
|
GITEE_URL=https://git.carabella.ch/
|
||||||
|
ACCESS_TOKEN=DEIN_TOKEN
|
||||||
|
COMMIT_NAME=KI-Sparringpartner
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ Fertig! Öffne http://[deine-ip]:8000 im Browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Sicherheitshinweis
|
||||||
|
|
||||||
|
- **`ACCESS_TOKEN`** muss Einzelhandhabung haben (keinsichere `.env` oder Secrets nutzen!)
|
||||||
|
- Docker-Container läuft in isolierter Umgebung – ideal fürheimnisches Umfeld
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Features (v1.0)
|
||||||
|
|
||||||
|
| Feature | Status |
|
||||||
|
|--------|--------|
|
||||||
|
| Repo-Liste von Gitea 🎯 | ✅ |
|
||||||
|
| Abonnieren & Klonen 💾 | ✅ |
|
||||||
|
| File-Watcher 🔍 | ✅ |
|
||||||
|
| Changelog-Generator 📋 | ✅ |
|
||||||
|
| Push mit AI-Tags ↩️ | ⏳ (kommt bald) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 TODO / Roadmap
|
||||||
|
|
||||||
|
- 🤖 OpenWebUI Integration für `analyze`, `improve`, `summarize`
|
||||||
|
- 🧠 Git-Hook zur KI-Begleitung bei Commits
|
||||||
|
- ☁️ Remote-Trigger via Knopfdruck (API-Webhook)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 Lizenz & Credits
|
||||||
|
|
||||||
|
Selbstgebaut mit ❤️ – für dich und deine Projekte!
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .main import app
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def generate_changelog(project_path: Path, output_file: str = "CHANGELOG.md") -> dict:
|
||||||
|
"""Generiert einen einfachen Changelog (manuell oder statisch)."""
|
||||||
|
changelog_path = project_path / output_file
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
content = f"""# 📋 Projekt-Changelog
|
||||||
|
|
||||||
|
## 🧠 Letzte Änderungen ({timestamp})
|
||||||
|
|
||||||
|
- Dateiüberwachung aktiviert
|
||||||
|
- AI-basierte Analysen möglich via OpenWebUI
|
||||||
|
|
||||||
|
💡 *Automatisch generiert vom Gitea-Sync-AI-Service*
|
||||||
|
"""
|
||||||
|
|
||||||
|
changelog_path.write_text(content)
|
||||||
|
return {"success": True, "path": str(changelog_path), "content": content}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import time
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class FileChangeHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, callback=None):
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
if not event.is_directory:
|
||||||
|
if self.callback:
|
||||||
|
self.callback(event.src_path)
|
||||||
|
print(f"[Watcher] Datei geändert: {event.src_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_watcher(path: Path, callback=None):
|
||||||
|
"""Startet den File-Watcher in einem Hintergrundthread."""
|
||||||
|
event_handler = FileChangeHandler(callback=callback)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(event_handler, str(path), recursive=True)
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
return observer
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def clone_repo(repo_url: str, target_dir: Path, token: str = None):
|
||||||
|
"""Klont ein Repository in den Zielordner."""
|
||||||
|
if not target_dir.exists():
|
||||||
|
target_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
# URL mit Token (falls nötig)
|
||||||
|
if token and "@" not in repo_url:
|
||||||
|
repo_url = repo_url.replace("https://", f"https://{token}@")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "clone", repo_url, str(target_dir)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": result.returncode == 0, "output": result.stdout, "error": result.stderr}
|
||||||
|
|
||||||
|
|
||||||
|
def push_changes(repo_path: Path, message: str = "Update 🧠"):
|
||||||
|
"""Pusht lokale Änderungen ins Repository."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "add", "."], cwd=repo_path, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {"success": False, "error": f"git add fehlgeschlagen: {result.stderr}"}
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "commit", "-m", message], cwd=repo_path, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0 and "nothing to commit" not in result.stderr.lower():
|
||||||
|
return {"success": False, "error": f"Commit fehlgeschlagen: {result.stderr}"}
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "push"], cwd=repo_path, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": result.returncode == 0, "output": result.stdout}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
# Umgebungsvariablen mit Fallback
|
||||||
|
GITEE_URL = os.getenv("GITEE_URL", "https://git.carabella.ch/")
|
||||||
|
ACCESS_TOKEN = os.getenv("ACCESS_TOKEN", "") # Leerlassen, falls nicht gesetzt
|
||||||
|
COMMIT_NAME = os.getenv("COMMIT_NAME", "KI-Sparringpartner")
|
||||||
|
|
||||||
|
# Projekt-Ordner (gemountet im Docker)
|
||||||
|
PROJECTS_DIR = Path("/projects")
|
||||||
|
|
||||||
|
|
||||||
|
def get_token():
|
||||||
|
"""Gibt Token zurück – aus .env oder leer."""
|
||||||
|
return ACCESS_TOKEN or ""
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="Gitea Sync AI", version="1.0")
|
||||||
|
|
||||||
|
# Frontend ausliefern
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def dashboard(request: Request):
|
||||||
|
with open("/home/user/projects/gitea-cloner/app/templates/index.html") as f:
|
||||||
|
return f.read()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import httpx
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from .git_manager import clone_repo, push_changes
|
||||||
|
from .changelog_gen import generate_changelog
|
||||||
|
from .main import get_token
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/repos")
|
||||||
|
async def list_repos():
|
||||||
|
"""Listet Repositories von Gitea auf."""
|
||||||
|
GITEE_URL = "https://git.carabella.ch/api/v1/user/repos"
|
||||||
|
token = get_token()
|
||||||
|
|
||||||
|
headers = {"Authorization": f"token {token}"} if token else {}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.get(GITEE_URL, headers=headers)
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/subscribe")
|
||||||
|
async def subscribe_repo(repo_url: str, target_name: str, token_provided: str = None):
|
||||||
|
"""Abonniert und klont ein Repository."""
|
||||||
|
# Wenn kein Token übergibst → nutz den DEFAULT aus .env oder Umgebung
|
||||||
|
effective_token = token_provided or get_token()
|
||||||
|
|
||||||
|
target_dir = Path("/projects") / (target_name or "imported-repo")
|
||||||
|
|
||||||
|
result = clone_repo(repo_url, target_dir, token=effective_token)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
return {"error": result.get("error"), "success": False}
|
||||||
|
|
||||||
|
# Optional: File-Watcher starten & Changelog erzeugen
|
||||||
|
generate_changelog(target_dir)
|
||||||
|
|
||||||
|
return {"message": f"✅ {target_name} geklont!", "path": str(target_dir), "success": True}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Gitea Sync AI 🧠</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #333; }
|
||||||
|
.repo-card { background: white; margin: 10px 0; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,.1); }
|
||||||
|
button { padding: 8px 16px; cursor: pointer; border: none; border-radius: 4px; }
|
||||||
|
.btn-subscribe { background: #007bff; color: white; }
|
||||||
|
.btn-push { background: #28a745; color: white; }
|
||||||
|
input, button { margin-top: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>🧠 Gitea Sync AI – Dashboard</h1>
|
||||||
|
|
||||||
|
<h3>Verfügbare Repositories:</h3>
|
||||||
|
<div id="repos"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function loadRepos() {
|
||||||
|
const res = await fetch("/api/repos");
|
||||||
|
const repos = await res.json();
|
||||||
|
const container = document.getElementById("repos");
|
||||||
|
container.innerHTML = "";
|
||||||
|
repos.forEach(repo => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "repo-card";
|
||||||
|
card.innerHTML = `
|
||||||
|
<strong>${repo.name}</strong>
|
||||||
|
<p>${repo.full_name || repo.id} - ${repo.owner?.login || "unbekannt"}</p>
|
||||||
|
<button class="btn-subscribe" onclick="subscribe(${JSON.stringify(repo).replace(/"/g, '"')})">➕ Abonnieren & Klonen</button>
|
||||||
|
`;
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribe(repo) {
|
||||||
|
const target = prompt("Ziel-Name (optional):") || repo.name;
|
||||||
|
const token = prompt("Gitea Token:");
|
||||||
|
|
||||||
|
await fetch("/api/subscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({repo_url: repo.clone_url, target_name: target, token})
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(`📦 "${target}" wurde geklont!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRepos();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gitea-sync-ai:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./projects:/projects # Lokale Projekte sichtbar im Container
|
||||||
|
- ./auth:/app/auth # Optional für Token-Speicherung
|
||||||
|
env_file:
|
||||||
|
- .env # Lädt GITEE_URL, ACCESS_TOKEN, usw.
|
||||||
|
environment:
|
||||||
|
- GITEE_URL=${GITEE_URL} # Fallback falls .env fehlt
|
||||||
|
- ACCESS_TOKEN=${ACCESS_TOKEN}
|
||||||
|
- COMMIT_NAME=${COMMIT_NAME}
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fastapi==0.115.12
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
httpx==0.27.2
|
||||||
|
watchdog==4.0.2
|
||||||
Reference in New Issue
Block a user