From 526a0eed71d07a23456c2ea85d4c62d76e49b055 Mon Sep 17 00:00:00 2001 From: Benjamin Boenisch Date: Sun, 15 Feb 2026 12:34:53 +0100 Subject: [PATCH] Complete pipeline: add school-service, klausur-service, BQAS, geo-service, agent-core tests Added missing test steps: - test-go-school (Go tests for school-service) - test-bqas-golden (BQAS golden/regression/synthetic) - test-bqas-rag (BQAS rag/notifier) - test-python-klausur (klausur-service backend tests) - test-python-geo (geo-service tests) - test-python-agent-core (agent-core tests) Added missing lint targets and build steps for school-service and geo-service --- .woodpecker/main.yml | 300 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 296 insertions(+), 4 deletions(-) diff --git a/.woodpecker/main.yml b/.woodpecker/main.yml index d59b557..c065736 100644 --- a/.woodpecker/main.yml +++ b/.woodpecker/main.yml @@ -4,7 +4,8 @@ # Plattform: ARM64 (Apple Silicon Mac Mini) # # Services: -# Python: voice-service, klausur-service, backend-lehrer +# Go: school-service +# Python: voice-service (+ BQAS), klausur-service, backend-lehrer, geo-service, agent-core # Node.js: website, admin-lehrer, studio-v2 # # Strategie: @@ -27,7 +28,9 @@ clone: - 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 - &nodejs_image node:20-alpine - &docker_image docker:27-cli @@ -36,17 +39,31 @@ steps: # STAGE 1: Lint (nur bei PRs) # ======================================== + go-lint: + image: golangci/golangci-lint:v1.55-alpine + commands: + - | + if [ -d "school-service" ]; then + cd school-service && golangci-lint run --timeout 5m ./... + fi + when: + event: pull_request + python-lint: image: *python_image commands: - pip install --quiet ruff - | - for svc in voice-service klausur-service backend-lehrer; do + for svc in voice-service backend-lehrer geo-service agent-core; do if [ -d "$svc" ]; then echo "=== Linting $svc ===" ruff check "$svc/" --output-format=github || true fi done + if [ -d "klausur-service/backend" ]; then + echo "=== Linting klausur-service ===" + ruff check klausur-service/backend/ --output-format=github || true + fi when: event: pull_request @@ -71,6 +88,49 @@ steps: # Ergebnisse werden im Workspace gespeichert (.ci-results/) # ======================================== + 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_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 + + if [ "$FAILED" -gt "0" ]; then + echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben" + fi + test-python-voice: image: *python_image environment: @@ -92,7 +152,7 @@ steps: 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 + python -m pytest tests/ -v --tb=short --ignore=tests/bqas --json-report --json-report-file=../.ci-results/test-voice.json TEST_EXIT=$? set -e @@ -110,6 +170,200 @@ steps: 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 + 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-python-geo: + image: *python_image + environment: + CI: "true" + commands: + - | + set -uo pipefail + mkdir -p .ci-results + + if [ ! -d "geo-service" ]; then + echo '{"service":"geo-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-geo.json + echo "WARNUNG: geo-service Verzeichnis nicht gefunden" + exit 0 + fi + + cd geo-service + 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-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-geo.json + TEST_EXIT=$? + set -e + + if [ -f ../.ci-results/test-geo.json ]; then + TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-geo.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-geo.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-geo.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-geo.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\":\"geo-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-geo.json + cat ../.ci-results/results-geo.json + + if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi + + test-python-agent-core: + image: *python_image + environment: + CI: "true" + commands: + - | + set -uo pipefail + mkdir -p .ci-results + + if [ ! -d "agent-core" ]; then + echo '{"service":"agent-core","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-agent-core.json + echo "WARNUNG: agent-core Verzeichnis nicht gefunden" + exit 0 + fi + + cd agent-core + export PYTHONPATH="$(pwd):${PYTHONPATH:-}" + pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || pip install --quiet --no-cache-dir 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-agent-core.json + TEST_EXIT=$? + set -e + + if [ -f ../.ci-results/test-agent-core.json ]; then + TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-agent-core.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-agent-core.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-agent-core.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-agent-core.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\":\"agent-core\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-agent-core.json + cat ../.ci-results/results-agent-core.json + + if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi + test-nodejs-website: image: *nodejs_image commands: @@ -184,7 +438,13 @@ steps: when: status: [success, failure] depends_on: + - test-go-school - test-python-voice + - test-bqas-golden + - test-bqas-rag + - test-python-klausur + - test-python-geo + - test-python-agent-core - test-nodejs-website # ======================================== @@ -281,6 +541,36 @@ steps: - event: tag - event: manual + build-school-service: + image: *docker_image + commands: + - | + if [ -d ./school-service ]; then + docker build -t breakpilot/school-service:${CI_COMMIT_SHA:0:8} ./school-service + docker tag breakpilot/school-service:${CI_COMMIT_SHA:0:8} breakpilot/school-service:latest + echo "Built breakpilot/school-service:${CI_COMMIT_SHA:0:8}" + else + echo "school-service Verzeichnis nicht gefunden - ueberspringe" + fi + when: + - event: tag + - event: manual + + build-geo-service: + image: *docker_image + commands: + - | + if [ -d ./geo-service ]; then + docker build -t breakpilot/geo-service:${CI_COMMIT_SHA:0:8} ./geo-service + docker tag breakpilot/geo-service:${CI_COMMIT_SHA:0:8} breakpilot/geo-service:latest + echo "Built breakpilot/geo-service:${CI_COMMIT_SHA:0:8}" + else + echo "geo-service Verzeichnis nicht gefunden - ueberspringe" + fi + when: + - event: tag + - event: manual + generate-sbom: image: python:3.12-slim commands: @@ -288,7 +578,7 @@ steps: echo "Installing syft for ARM64..." apt-get update -qq && apt-get install -y -qq wget > /dev/null wget -qO- https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin - for svc in voice-service klausur-service backend-lehrer website; do + for svc in voice-service klausur-service backend-lehrer website school-service geo-service agent-core; do if [ -d "./$svc" ]; then syft dir:./$svc -o cyclonedx-json > sbom-$svc.json echo "SBOM generated for $svc" @@ -335,3 +625,5 @@ steps: - build-backend-lehrer - build-klausur-service - build-voice-service + - build-school-service + - build-geo-service