A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
296 lines
10 KiB
Python
296 lines
10 KiB
Python
"""
|
|
Test Registry - CI/CD Integration Endpoints
|
|
|
|
Endpoints for receiving results from CI/CD pipelines.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Dict
|
|
|
|
from fastapi import APIRouter, BackgroundTasks
|
|
|
|
from ...database import get_db_session
|
|
from ...repository import TestRepository
|
|
from ..api_models import CIResultRequest
|
|
from ..config import (
|
|
get_test_runs,
|
|
get_persisted_results,
|
|
is_postgres_available,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/ci-result")
|
|
async def receive_ci_result(result: CIResultRequest, background_tasks: BackgroundTasks):
|
|
"""
|
|
Empfaengt Test-Ergebnisse von der CI/CD-Pipeline.
|
|
|
|
Wird vom report-test-results Step in .woodpecker/main.yml aufgerufen.
|
|
|
|
Flow:
|
|
1. Pipeline fuehrt Tests aus und sammelt JSON-Ergebnisse
|
|
2. Pipeline sendet detaillierte Ergebnisse pro Service hierher
|
|
3. Dieser Endpoint speichert in PostgreSQL
|
|
4. Dashboard zeigt die Daten an
|
|
|
|
test_results Format:
|
|
{
|
|
"service": "consent-service",
|
|
"framework": "go",
|
|
"total": 57,
|
|
"passed": 57,
|
|
"failed": 0,
|
|
"skipped": 0,
|
|
"coverage": 75.5
|
|
}
|
|
"""
|
|
test_runs = get_test_runs()
|
|
persisted_results = get_persisted_results()
|
|
|
|
# Extrahiere Service-spezifische Daten aus test_results
|
|
tr = result.test_results or {}
|
|
service_name = tr.get("service", "ci-pipeline")
|
|
framework = tr.get("framework", "unknown")
|
|
total = tr.get("total", 0)
|
|
passed = tr.get("passed", 0)
|
|
failed = tr.get("failed", 0)
|
|
skipped = tr.get("skipped", 0)
|
|
coverage = tr.get("coverage", 0)
|
|
|
|
# Log zur Debugging
|
|
print(f"[CI-RESULT] Pipeline {result.pipeline_id} - Service: {service_name}")
|
|
print(f"[CI-RESULT] Tests: {passed}/{total} passed, {failed} failed, {skipped} skipped")
|
|
print(f"[CI-RESULT] Coverage: {coverage}%, Commit: {result.commit[:8]}")
|
|
|
|
# Speichere in PostgreSQL wenn verfuegbar
|
|
if is_postgres_available():
|
|
try:
|
|
with get_db_session() as db:
|
|
repo = TestRepository(db)
|
|
|
|
# Erstelle eindeutige Run-ID pro Service
|
|
run_id = f"ci-{result.pipeline_id}-{service_name}"
|
|
|
|
# Erstelle Test-Run Eintrag
|
|
run = repo.create_run(
|
|
run_id=run_id,
|
|
service=service_name,
|
|
framework=framework,
|
|
triggered_by="ci",
|
|
git_commit=result.commit[:8] if result.commit else None,
|
|
git_branch=result.branch
|
|
)
|
|
|
|
# Markiere als abgeschlossen mit detaillierten Zahlen
|
|
status = "passed" if failed == 0 else "failed"
|
|
repo.complete_run(
|
|
run_id=run_id,
|
|
status=status,
|
|
total_tests=total,
|
|
passed_tests=passed,
|
|
failed_tests=failed,
|
|
skipped_tests=skipped,
|
|
duration_seconds=0
|
|
)
|
|
print(f"[CI-RESULT] Stored as run_id: {run_id}, status: {status}")
|
|
|
|
# WICHTIG: Aktualisiere den In-Memory Cache fuer sofortige Frontend-Updates
|
|
persisted_results[service_name] = {
|
|
"total": total,
|
|
"passed": passed,
|
|
"failed": failed,
|
|
"last_run": datetime.utcnow().isoformat(),
|
|
"status": status,
|
|
"failed_test_ids": []
|
|
}
|
|
print(f"[CI-RESULT] Updated cache for {service_name}: {passed}/{total} passed")
|
|
|
|
# Bei fehlgeschlagenen Tests: Backlog-Eintrag erstellen
|
|
if failed > 0:
|
|
background_tasks.add_task(
|
|
_create_backlog_entry,
|
|
service_name,
|
|
framework,
|
|
failed,
|
|
result.pipeline_id,
|
|
result.commit,
|
|
result.branch
|
|
)
|
|
else:
|
|
# Alle Tests bestanden: Schließe offene Backlog-Einträge
|
|
background_tasks.add_task(
|
|
_close_backlog_entry,
|
|
service_name,
|
|
result.pipeline_id,
|
|
result.commit
|
|
)
|
|
|
|
return {
|
|
"received": True,
|
|
"run_id": run_id,
|
|
"service": service_name,
|
|
"pipeline_id": result.pipeline_id,
|
|
"status": status,
|
|
"tests": {"total": total, "passed": passed, "failed": failed},
|
|
"stored_in": "postgres"
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"[CI-RESULT] PostgreSQL Error: {e}")
|
|
# Fallback auf Memory-Storage
|
|
pass
|
|
|
|
# Memory-Fallback
|
|
ci_run = {
|
|
"id": f"ci-{result.pipeline_id}",
|
|
"pipeline_id": result.pipeline_id,
|
|
"commit": result.commit,
|
|
"branch": result.branch,
|
|
"status": result.status,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"test_results": result.test_results
|
|
}
|
|
test_runs.append(ci_run)
|
|
|
|
return {
|
|
"received": True,
|
|
"pipeline_id": result.pipeline_id,
|
|
"status": result.status,
|
|
"stored_in": "memory"
|
|
}
|
|
|
|
|
|
async def _create_backlog_entry(
|
|
service_name: str,
|
|
framework: str,
|
|
failed_count: int,
|
|
pipeline_id: str,
|
|
commit: str,
|
|
branch: str
|
|
):
|
|
"""
|
|
Background-Task: Erstellt Backlog-Eintraege fuer fehlgeschlagene Tests.
|
|
|
|
Wird asynchron aufgerufen wenn Tests fehlgeschlagen sind.
|
|
"""
|
|
from ...db_models import FailedTestBacklogDB
|
|
|
|
print(f"[CI-RESULT] Creating backlog entry for {service_name}: {failed_count} failed tests")
|
|
|
|
if is_postgres_available():
|
|
try:
|
|
with get_db_session() as db:
|
|
now = datetime.utcnow()
|
|
|
|
# Pruefe ob schon ein offener Backlog-Eintrag fuer diesen Service existiert
|
|
existing = db.query(FailedTestBacklogDB).filter(
|
|
FailedTestBacklogDB.service == service_name,
|
|
FailedTestBacklogDB.status == "open"
|
|
).first()
|
|
|
|
if existing:
|
|
# Aktualisiere existierenden Eintrag
|
|
existing.last_failed_at = now
|
|
existing.failure_count += 1
|
|
existing.error_message = f"{failed_count} Tests fehlgeschlagen in Pipeline {pipeline_id} (Branch: {branch})"
|
|
db.commit()
|
|
print(f"[CI-RESULT] Updated existing backlog entry (ID: {existing.id})")
|
|
else:
|
|
# Neuen Eintrag erstellen
|
|
backlog = FailedTestBacklogDB(
|
|
test_name=f"{service_name} Tests",
|
|
test_file=f"{service_name}/",
|
|
service=service_name,
|
|
framework=framework,
|
|
error_message=f"{failed_count} Tests fehlgeschlagen in Pipeline {pipeline_id} (Branch: {branch})",
|
|
error_type="TEST_FAILURE",
|
|
first_failed_at=now,
|
|
last_failed_at=now,
|
|
failure_count=1,
|
|
status="open",
|
|
priority="high" if failed_count > 5 else "medium"
|
|
)
|
|
db.add(backlog)
|
|
db.commit()
|
|
print(f"[CI-RESULT] Created new backlog entry (ID: {backlog.id})")
|
|
|
|
except Exception as e:
|
|
print(f"[CI-RESULT] Error creating backlog entry: {e}")
|
|
|
|
|
|
async def _close_backlog_entry(
|
|
service_name: str,
|
|
pipeline_id: str,
|
|
commit: str
|
|
):
|
|
"""
|
|
Background-Task: Schließt Backlog-Einträge wenn alle Tests bestanden.
|
|
|
|
Wird asynchron aufgerufen wenn Tests erfolgreich waren.
|
|
"""
|
|
from ...db_models import FailedTestBacklogDB
|
|
|
|
print(f"[CI-RESULT] Checking for open backlog entries to close for {service_name}")
|
|
|
|
if is_postgres_available():
|
|
try:
|
|
with get_db_session() as db:
|
|
now = datetime.utcnow()
|
|
|
|
# Finde offene Backlog-Einträge für diesen Service
|
|
open_entries = db.query(FailedTestBacklogDB).filter(
|
|
FailedTestBacklogDB.service == service_name,
|
|
FailedTestBacklogDB.status == "open"
|
|
).all()
|
|
|
|
for entry in open_entries:
|
|
entry.status = "resolved"
|
|
entry.resolved_at = now
|
|
entry.resolution_commit = commit[:8] if commit else None
|
|
entry.resolution_notes = f"Automatisch geschlossen - alle Tests in Pipeline {pipeline_id} bestanden"
|
|
print(f"[CI-RESULT] Auto-closed backlog entry (ID: {entry.id}) for {service_name}")
|
|
|
|
if open_entries:
|
|
db.commit()
|
|
print(f"[CI-RESULT] Closed {len(open_entries)} backlog entries for {service_name}")
|
|
else:
|
|
print(f"[CI-RESULT] No open backlog entries for {service_name}")
|
|
|
|
except Exception as e:
|
|
print(f"[CI-RESULT] Error closing backlog entries: {e}")
|
|
|
|
|
|
async def _fetch_and_store_failed_tests(pipeline_id: str, commit: str, branch: str):
|
|
"""
|
|
Legacy Background-Task fuer generische Pipeline-Fehler.
|
|
"""
|
|
from ...db_models import FailedTestBacklogDB
|
|
|
|
print(f"[CI-RESULT] Fetching failed test details for pipeline {pipeline_id}")
|
|
|
|
if is_postgres_available():
|
|
try:
|
|
with get_db_session() as db:
|
|
now = datetime.utcnow()
|
|
|
|
backlog = FailedTestBacklogDB(
|
|
test_name=f"CI Pipeline {pipeline_id}",
|
|
test_file=".woodpecker/main.yml",
|
|
service="ci-pipeline",
|
|
framework="woodpecker",
|
|
error_message=f"Pipeline {pipeline_id} fehlgeschlagen auf Branch {branch}",
|
|
error_type="CI_FAILURE",
|
|
first_failed_at=now,
|
|
last_failed_at=now,
|
|
failure_count=1,
|
|
status="open",
|
|
priority="high"
|
|
)
|
|
db.add(backlog)
|
|
db.commit()
|
|
print(f"[CI-RESULT] Added pipeline failure to backlog (ID: {backlog.id})")
|
|
|
|
except Exception as e:
|
|
print(f"[CI-RESULT] Error adding to backlog: {e}")
|