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>
670 lines
29 KiB
YAML
670 lines
29 KiB
YAML
# Woodpecker CI Main Pipeline
|
|
# BreakPilot PWA - CI/CD Pipeline
|
|
#
|
|
# Plattform: ARM64 (Apple Silicon Mac Mini)
|
|
#
|
|
# Strategie:
|
|
# - Tests laufen bei JEDEM Push/PR
|
|
# - Test-Ergebnisse werden an Dashboard gesendet
|
|
# - Builds/Scans laufen nur bei Tags oder manuell
|
|
# - Deployment nur manuell (Sicherheit)
|
|
|
|
when:
|
|
- event: [push, pull_request, manual, tag]
|
|
branch: [main, develop]
|
|
|
|
clone:
|
|
git:
|
|
image: woodpeckerci/plugin-git
|
|
settings:
|
|
depth: 1
|
|
extra_hosts:
|
|
- macmini:192.168.178.100
|
|
|
|
variables:
|
|
- &golang_image golang:1.23-alpine
|
|
- &python_image python:3.12-slim
|
|
- &python_ci_image breakpilot/python-ci:3.12 # Custom image with WeasyPrint
|
|
- &nodejs_image node:20-alpine
|
|
- &docker_image docker:27-cli
|
|
|
|
steps:
|
|
# ========================================
|
|
# STAGE 1: Lint (nur bei PRs)
|
|
# ========================================
|
|
|
|
go-lint:
|
|
image: golangci/golangci-lint:v1.55-alpine
|
|
commands:
|
|
- cd consent-service && golangci-lint run --timeout 5m ./...
|
|
- cd ../billing-service && golangci-lint run --timeout 5m ./...
|
|
- cd ../school-service && golangci-lint run --timeout 5m ./...
|
|
when:
|
|
event: pull_request
|
|
|
|
python-lint:
|
|
image: *python_image
|
|
commands:
|
|
- pip install --quiet ruff black
|
|
- ruff check backend/ --output-format=github || true
|
|
- black --check backend/ || true
|
|
when:
|
|
event: pull_request
|
|
|
|
# ========================================
|
|
# STAGE 2: Unit Tests mit JSON-Ausgabe
|
|
# Ergebnisse werden im Workspace gespeichert (.ci-results/)
|
|
# ========================================
|
|
|
|
test-go-consent:
|
|
image: *golang_image
|
|
environment:
|
|
CGO_ENABLED: "0"
|
|
commands:
|
|
- |
|
|
set -euo pipefail
|
|
apk add --no-cache jq bash
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "consent-service" ]; then
|
|
echo '{"service":"consent-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-consent.json
|
|
echo "WARNUNG: consent-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd consent-service
|
|
set +e
|
|
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-consent.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
# JSON-Zeilen extrahieren und mit jq zählen
|
|
JSON_FILE="../.ci-results/test-consent.json"
|
|
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
|
|
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
|
|
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
|
|
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
|
|
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
|
|
else
|
|
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
|
|
[ -z "$COVERAGE" ] && COVERAGE=0
|
|
|
|
echo "{\"service\":\"consent-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-consent.json
|
|
cat ../.ci-results/results-consent.json
|
|
|
|
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
|
|
if [ "$FAILED" -gt "0" ]; then
|
|
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
|
|
fi
|
|
|
|
test-go-billing:
|
|
image: *golang_image
|
|
environment:
|
|
CGO_ENABLED: "0"
|
|
commands:
|
|
- |
|
|
set -euo pipefail
|
|
apk add --no-cache jq bash
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "billing-service" ]; then
|
|
echo '{"service":"billing-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-billing.json
|
|
echo "WARNUNG: billing-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd billing-service
|
|
set +e
|
|
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-billing.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
# JSON-Zeilen extrahieren und mit jq zählen
|
|
JSON_FILE="../.ci-results/test-billing.json"
|
|
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
|
|
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
|
|
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
|
|
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
|
|
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
|
|
else
|
|
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
|
|
[ -z "$COVERAGE" ] && COVERAGE=0
|
|
|
|
echo "{\"service\":\"billing-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-billing.json
|
|
cat ../.ci-results/results-billing.json
|
|
|
|
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
|
|
if [ "$FAILED" -gt "0" ]; then
|
|
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
|
|
fi
|
|
|
|
test-go-school:
|
|
image: *golang_image
|
|
environment:
|
|
CGO_ENABLED: "0"
|
|
commands:
|
|
- |
|
|
set -euo pipefail
|
|
apk add --no-cache jq bash
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "school-service" ]; then
|
|
echo '{"service":"school-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-school.json
|
|
echo "WARNUNG: school-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd school-service
|
|
set +e
|
|
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-school.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
# JSON-Zeilen extrahieren und mit jq zählen
|
|
JSON_FILE="../.ci-results/test-school.json"
|
|
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
|
|
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
|
|
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
|
|
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
|
|
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
|
|
else
|
|
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
|
|
[ -z "$COVERAGE" ] && COVERAGE=0
|
|
|
|
echo "{\"service\":\"school-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-school.json
|
|
cat ../.ci-results/results-school.json
|
|
|
|
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
|
|
if [ "$FAILED" -gt "0" ]; then
|
|
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
|
|
fi
|
|
|
|
test-go-edu-search:
|
|
image: *golang_image
|
|
environment:
|
|
CGO_ENABLED: "0"
|
|
commands:
|
|
- |
|
|
set -euo pipefail
|
|
apk add --no-cache jq bash
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "edu-search-service" ]; then
|
|
echo '{"service":"edu-search-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-edu-search.json
|
|
echo "WARNUNG: edu-search-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd edu-search-service
|
|
set +e
|
|
go test -v -json -coverprofile=coverage.out ./internal/... 2>&1 | tee ../.ci-results/test-edu-search.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
# JSON-Zeilen extrahieren und mit jq zählen
|
|
JSON_FILE="../.ci-results/test-edu-search.json"
|
|
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
|
|
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
|
|
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
|
|
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
|
|
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
|
|
else
|
|
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
|
|
[ -z "$COVERAGE" ] && COVERAGE=0
|
|
|
|
echo "{\"service\":\"edu-search-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-edu-search.json
|
|
cat ../.ci-results/results-edu-search.json
|
|
|
|
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
|
|
if [ "$FAILED" -gt "0" ]; then
|
|
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
|
|
fi
|
|
|
|
test-go-ai-compliance:
|
|
image: *golang_image
|
|
environment:
|
|
CGO_ENABLED: "0"
|
|
commands:
|
|
- |
|
|
set -euo pipefail
|
|
apk add --no-cache jq bash
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "ai-compliance-sdk" ]; then
|
|
echo '{"service":"ai-compliance-sdk","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-ai-compliance.json
|
|
echo "WARNUNG: ai-compliance-sdk Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd ai-compliance-sdk
|
|
set +e
|
|
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-ai-compliance.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
# JSON-Zeilen extrahieren und mit jq zählen
|
|
JSON_FILE="../.ci-results/test-ai-compliance.json"
|
|
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
|
|
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
|
|
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
|
|
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
|
|
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
|
|
else
|
|
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
|
|
[ -z "$COVERAGE" ] && COVERAGE=0
|
|
|
|
echo "{\"service\":\"ai-compliance-sdk\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-ai-compliance.json
|
|
cat ../.ci-results/results-ai-compliance.json
|
|
|
|
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
|
|
if [ "$FAILED" -gt "0" ]; then
|
|
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
|
|
fi
|
|
|
|
test-python-backend:
|
|
image: *python_ci_image
|
|
environment:
|
|
CI: "true"
|
|
DATABASE_URL: "postgresql://test:test@localhost:5432/test_db"
|
|
SKIP_DB_TESTS: "true"
|
|
SKIP_WEASYPRINT_TESTS: "false"
|
|
SKIP_INTEGRATION_TESTS: "true"
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "backend" ]; then
|
|
echo '{"service":"backend","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-backend.json
|
|
echo "WARNUNG: backend Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd backend
|
|
# Set PYTHONPATH to current directory (backend) so local packages like classroom_engine, alerts_agent are found
|
|
# IMPORTANT: Use absolute path and export before pip install to ensure modules are available
|
|
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
|
|
|
|
# Test tools are pre-installed in breakpilot/python-ci image
|
|
# Only install project-specific dependencies
|
|
pip install --quiet --no-cache-dir -r requirements.txt
|
|
|
|
# NOTE: PostgreSQL service removed - tests that require DB are skipped via SKIP_DB_TESTS=true
|
|
# For full integration tests, use: docker compose -f docker-compose.test.yml up -d
|
|
|
|
set +e
|
|
# Use python -m pytest to ensure PYTHONPATH is properly applied before pytest starts
|
|
python -m pytest tests/ -v --tb=short --cov=. --cov-report=term-missing --json-report --json-report-file=../.ci-results/test-backend.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../.ci-results/test-backend.json ]; then
|
|
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
|
|
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
|
|
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
|
|
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
echo "{\"service\":\"backend\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-backend.json
|
|
cat ../.ci-results/results-backend.json
|
|
|
|
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
|
|
|
|
test-python-voice:
|
|
image: *python_image
|
|
environment:
|
|
CI: "true"
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "voice-service" ]; then
|
|
echo '{"service":"voice-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-voice.json
|
|
echo "WARNUNG: voice-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd voice-service
|
|
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
|
|
pip install --quiet --no-cache-dir -r requirements.txt
|
|
pip install --quiet --no-cache-dir pytest-json-report
|
|
|
|
set +e
|
|
python -m pytest tests/ -v --tb=short --json-report --json-report-file=../.ci-results/test-voice.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../.ci-results/test-voice.json ]; then
|
|
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
|
|
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
|
|
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
|
|
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
echo "{\"service\":\"voice-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-voice.json
|
|
cat ../.ci-results/results-voice.json
|
|
|
|
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
|
|
|
|
test-bqas-golden:
|
|
image: *python_image
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "voice-service/tests/bqas" ]; then
|
|
echo '{"service":"bqas-golden","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-golden.json
|
|
echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd voice-service
|
|
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
|
|
pip install --quiet --no-cache-dir -r requirements.txt
|
|
pip install --quiet --no-cache-dir pytest-json-report pytest-asyncio
|
|
|
|
set +e
|
|
python -m pytest tests/bqas/test_golden.py tests/bqas/test_regression.py tests/bqas/test_synthetic.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-golden.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../.ci-results/test-bqas-golden.json ]; then
|
|
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
|
|
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
|
|
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
|
|
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
echo "{\"service\":\"bqas-golden\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-golden.json
|
|
cat ../.ci-results/results-bqas-golden.json
|
|
|
|
# BQAS tests may skip if Ollama not available - don't fail pipeline
|
|
if [ "$FAILED" -gt "0" ]; then exit 1; fi
|
|
|
|
test-bqas-rag:
|
|
image: *python_image
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "voice-service/tests/bqas" ]; then
|
|
echo '{"service":"bqas-rag","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-rag.json
|
|
echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd voice-service
|
|
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
|
|
pip install --quiet --no-cache-dir -r requirements.txt
|
|
pip install --quiet --no-cache-dir pytest-json-report pytest-asyncio
|
|
|
|
set +e
|
|
python -m pytest tests/bqas/test_rag.py tests/bqas/test_notifier.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-rag.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../.ci-results/test-bqas-rag.json ]; then
|
|
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
|
|
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
|
|
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
|
|
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
echo "{\"service\":\"bqas-rag\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-rag.json
|
|
cat ../.ci-results/results-bqas-rag.json
|
|
|
|
# BQAS tests may skip if Ollama not available - don't fail pipeline
|
|
if [ "$FAILED" -gt "0" ]; then exit 1; fi
|
|
|
|
test-python-klausur:
|
|
image: *python_image
|
|
environment:
|
|
CI: "true"
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "klausur-service/backend" ]; then
|
|
echo '{"service":"klausur-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-klausur.json
|
|
echo "WARNUNG: klausur-service/backend Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd klausur-service/backend
|
|
# Set PYTHONPATH to current directory so local modules like hyde, hybrid_search, etc. are found
|
|
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
|
|
|
|
pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || pip install --quiet --no-cache-dir fastapi uvicorn pytest pytest-asyncio pytest-json-report
|
|
pip install --quiet --no-cache-dir pytest-json-report
|
|
|
|
set +e
|
|
python -m pytest tests/ -v --tb=short --json-report --json-report-file=../../.ci-results/test-klausur.json
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../../.ci-results/test-klausur.json ]; then
|
|
TOTAL=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
|
|
PASSED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
|
|
FAILED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
|
|
SKIPPED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
echo "{\"service\":\"klausur-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../../.ci-results/results-klausur.json
|
|
cat ../../.ci-results/results-klausur.json
|
|
|
|
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
|
|
|
|
test-nodejs-h5p:
|
|
image: *nodejs_image
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
mkdir -p .ci-results
|
|
|
|
if [ ! -d "h5p-service" ]; then
|
|
echo '{"service":"h5p-service","framework":"jest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-h5p.json
|
|
echo "WARNUNG: h5p-service Verzeichnis nicht gefunden"
|
|
exit 0
|
|
fi
|
|
|
|
cd h5p-service
|
|
npm ci --silent 2>/dev/null || npm install --silent
|
|
|
|
set +e
|
|
npm run test:ci -- --json --outputFile=../.ci-results/test-h5p.json 2>&1
|
|
TEST_EXIT=$?
|
|
set -e
|
|
|
|
if [ -f ../.ci-results/test-h5p.json ]; then
|
|
TOTAL=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numTotalTests || 0)" 2>/dev/null || echo "0")
|
|
PASSED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numPassedTests || 0)" 2>/dev/null || echo "0")
|
|
FAILED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numFailedTests || 0)" 2>/dev/null || echo "0")
|
|
SKIPPED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numPendingTests || 0)" 2>/dev/null || echo "0")
|
|
else
|
|
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
|
|
fi
|
|
|
|
[ -z "$TOTAL" ] && TOTAL=0
|
|
[ -z "$PASSED" ] && PASSED=0
|
|
[ -z "$FAILED" ] && FAILED=0
|
|
[ -z "$SKIPPED" ] && SKIPPED=0
|
|
|
|
echo "{\"service\":\"h5p-service\",\"framework\":\"jest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-h5p.json
|
|
cat ../.ci-results/results-h5p.json
|
|
|
|
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
|
|
|
|
# ========================================
|
|
# STAGE 2.5: Integration Tests
|
|
# ========================================
|
|
# Integration Tests laufen in separater Pipeline:
|
|
# .woodpecker/integration.yml
|
|
# (benötigt Pipeline-Level Services für PostgreSQL und Valkey)
|
|
|
|
# ========================================
|
|
# STAGE 3: Test-Ergebnisse an Dashboard senden
|
|
# ========================================
|
|
|
|
report-test-results:
|
|
image: curlimages/curl:8.10.1
|
|
commands:
|
|
- |
|
|
set -uo pipefail
|
|
echo "=== Sende Test-Ergebnisse an Dashboard ==="
|
|
echo "Pipeline Status: ${CI_PIPELINE_STATUS:-unknown}"
|
|
ls -la .ci-results/ || echo "Verzeichnis nicht gefunden"
|
|
|
|
PIPELINE_STATUS="${CI_PIPELINE_STATUS:-unknown}"
|
|
|
|
for f in .ci-results/results-*.json; do
|
|
[ -f "$f" ] || continue
|
|
echo "Sending: $f"
|
|
curl -f -sS -X POST "http://backend:8000/api/tests/ci-result" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"pipeline_id\": \"${CI_PIPELINE_NUMBER}\",
|
|
\"commit\": \"${CI_COMMIT_SHA}\",
|
|
\"branch\": \"${CI_COMMIT_BRANCH}\",
|
|
\"status\": \"${PIPELINE_STATUS}\",
|
|
\"test_results\": $(cat "$f")
|
|
}" || echo "WARNUNG: Konnte $f nicht senden"
|
|
done
|
|
|
|
echo "=== Test-Ergebnisse gesendet ==="
|
|
when:
|
|
status: [success, failure]
|
|
depends_on:
|
|
- test-go-consent
|
|
- test-go-billing
|
|
- test-go-school
|
|
- test-go-edu-search
|
|
- test-go-ai-compliance
|
|
- test-python-backend
|
|
- test-python-voice
|
|
- test-bqas-golden
|
|
- test-bqas-rag
|
|
- test-python-klausur
|
|
- test-nodejs-h5p
|
|
|
|
# ========================================
|
|
# STAGE 4: Build & Security (nur Tags/manuell)
|
|
# ========================================
|
|
|
|
build-consent-service:
|
|
image: *docker_image
|
|
commands:
|
|
- docker build -t breakpilot/consent-service:${CI_COMMIT_SHA:0:8} ./consent-service
|
|
- docker tag breakpilot/consent-service:${CI_COMMIT_SHA:0:8} breakpilot/consent-service:latest
|
|
- echo "Built breakpilot/consent-service:${CI_COMMIT_SHA:0:8}"
|
|
when:
|
|
- event: tag
|
|
- event: manual
|
|
|
|
build-backend:
|
|
image: *docker_image
|
|
commands:
|
|
- docker build -t breakpilot/backend:${CI_COMMIT_SHA:0:8} ./backend
|
|
- docker tag breakpilot/backend:${CI_COMMIT_SHA:0:8} breakpilot/backend:latest
|
|
- echo "Built breakpilot/backend:${CI_COMMIT_SHA:0:8}"
|
|
when:
|
|
- event: tag
|
|
- event: manual
|
|
|
|
build-voice-service:
|
|
image: *docker_image
|
|
commands:
|
|
- |
|
|
if [ -d ./voice-service ]; then
|
|
docker build -t breakpilot/voice-service:${CI_COMMIT_SHA:0:8} ./voice-service
|
|
docker tag breakpilot/voice-service:${CI_COMMIT_SHA:0:8} breakpilot/voice-service:latest
|
|
echo "Built breakpilot/voice-service:${CI_COMMIT_SHA:0:8}"
|
|
else
|
|
echo "voice-service Verzeichnis nicht gefunden - ueberspringe"
|
|
fi
|
|
when:
|
|
- event: tag
|
|
- event: manual
|
|
|
|
generate-sbom:
|
|
image: *golang_image
|
|
commands:
|
|
- |
|
|
echo "Installing syft for ARM64..."
|
|
wget -qO- https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
|
syft dir:./consent-service -o cyclonedx-json > sbom-consent.json
|
|
syft dir:./backend -o cyclonedx-json > sbom-backend.json
|
|
if [ -d ./voice-service ]; then
|
|
syft dir:./voice-service -o cyclonedx-json > sbom-voice.json
|
|
fi
|
|
echo "SBOMs generated successfully"
|
|
when:
|
|
- event: tag
|
|
- event: manual
|
|
|
|
vulnerability-scan:
|
|
image: *golang_image
|
|
commands:
|
|
- |
|
|
echo "Installing grype for ARM64..."
|
|
wget -qO- https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
|
|
grype sbom:sbom-consent.json -o table --fail-on critical || true
|
|
grype sbom:sbom-backend.json -o table --fail-on critical || true
|
|
if [ -f sbom-voice.json ]; then
|
|
grype sbom:sbom-voice.json -o table --fail-on critical || true
|
|
fi
|
|
when:
|
|
- event: tag
|
|
- event: manual
|
|
depends_on:
|
|
- generate-sbom
|
|
|
|
# ========================================
|
|
# STAGE 5: Deploy (nur manuell)
|
|
# ========================================
|
|
|
|
deploy-production:
|
|
image: *docker_image
|
|
commands:
|
|
- echo "Deploying to production..."
|
|
- docker compose -f docker-compose.yml pull || true
|
|
- docker compose -f docker-compose.yml up -d --remove-orphans || true
|
|
when:
|
|
event: manual
|
|
depends_on:
|
|
- build-consent-service
|
|
- build-backend
|