Add Woodpecker CI/CD pipeline
- Lint: ruff (Python services), next lint (Node.js apps) - Tests: pytest for voice-service, jest for website - Build: Docker images for all 6 services - Security: SBOM generation + vulnerability scanning - Deploy: manual docker compose deployment
This commit is contained in:
337
.woodpecker/main.yml
Normal file
337
.woodpecker/main.yml
Normal file
@@ -0,0 +1,337 @@
|
||||
# Woodpecker CI Main Pipeline
|
||||
# BreakPilot Lehrer - CI/CD Pipeline
|
||||
#
|
||||
# Plattform: ARM64 (Apple Silicon Mac Mini)
|
||||
#
|
||||
# Services:
|
||||
# Python: voice-service, klausur-service, backend-lehrer
|
||||
# Node.js: website, admin-lehrer, studio-v2
|
||||
#
|
||||
# Strategie:
|
||||
# - Lint bei PRs
|
||||
# - 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:
|
||||
- &python_image python:3.12-slim
|
||||
- &nodejs_image node:20-alpine
|
||||
- &docker_image docker:27-cli
|
||||
|
||||
steps:
|
||||
# ========================================
|
||||
# STAGE 1: Lint (nur bei PRs)
|
||||
# ========================================
|
||||
|
||||
python-lint:
|
||||
image: *python_image
|
||||
commands:
|
||||
- pip install --quiet ruff
|
||||
- |
|
||||
for svc in voice-service klausur-service backend-lehrer; do
|
||||
if [ -d "$svc" ]; then
|
||||
echo "=== Linting $svc ==="
|
||||
ruff check "$svc/" --output-format=github || true
|
||||
fi
|
||||
done
|
||||
when:
|
||||
event: pull_request
|
||||
|
||||
nodejs-lint:
|
||||
image: *nodejs_image
|
||||
commands:
|
||||
- |
|
||||
for svc in website admin-lehrer studio-v2; do
|
||||
if [ -d "$svc" ]; then
|
||||
echo "=== Linting $svc ==="
|
||||
cd "$svc"
|
||||
npm ci --silent 2>/dev/null || npm install --silent
|
||||
npx next lint || true
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
when:
|
||||
event: pull_request
|
||||
|
||||
# ========================================
|
||||
# STAGE 2: Unit Tests mit JSON-Ausgabe
|
||||
# Ergebnisse werden im Workspace gespeichert (.ci-results/)
|
||||
# ========================================
|
||||
|
||||
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-nodejs-website:
|
||||
image: *nodejs_image
|
||||
commands:
|
||||
- |
|
||||
set -uo pipefail
|
||||
mkdir -p .ci-results
|
||||
|
||||
if [ ! -d "website" ]; then
|
||||
echo '{"service":"website","framework":"jest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-website.json
|
||||
echo "WARNUNG: website Verzeichnis nicht gefunden"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd website
|
||||
npm ci --silent 2>/dev/null || npm install --silent
|
||||
|
||||
set +e
|
||||
npx jest --json --outputFile=../.ci-results/test-website.json 2>&1
|
||||
TEST_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ -f ../.ci-results/test-website.json ]; then
|
||||
TOTAL=$(node -e "const d=require('../.ci-results/test-website.json'); console.log(d.numTotalTests || 0)" 2>/dev/null || echo "0")
|
||||
PASSED=$(node -e "const d=require('../.ci-results/test-website.json'); console.log(d.numPassedTests || 0)" 2>/dev/null || echo "0")
|
||||
FAILED=$(node -e "const d=require('../.ci-results/test-website.json'); console.log(d.numFailedTests || 0)" 2>/dev/null || echo "0")
|
||||
SKIPPED=$(node -e "const d=require('../.ci-results/test-website.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\":\"website\",\"framework\":\"jest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-website.json
|
||||
cat ../.ci-results/results-website.json
|
||||
|
||||
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
|
||||
|
||||
# ========================================
|
||||
# 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}\",
|
||||
\"repo\": \"breakpilot-lehrer\",
|
||||
\"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-python-voice
|
||||
- test-nodejs-website
|
||||
|
||||
# ========================================
|
||||
# STAGE 4: Build & Security (nur Tags/manuell)
|
||||
# ========================================
|
||||
|
||||
build-admin-lehrer:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- |
|
||||
if [ -d ./admin-lehrer ]; then
|
||||
docker build -t breakpilot/admin-lehrer:${CI_COMMIT_SHA:0:8} ./admin-lehrer
|
||||
docker tag breakpilot/admin-lehrer:${CI_COMMIT_SHA:0:8} breakpilot/admin-lehrer:latest
|
||||
echo "Built breakpilot/admin-lehrer:${CI_COMMIT_SHA:0:8}"
|
||||
else
|
||||
echo "admin-lehrer Verzeichnis nicht gefunden - ueberspringe"
|
||||
fi
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
|
||||
build-studio-v2:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- |
|
||||
if [ -d ./studio-v2 ]; then
|
||||
docker build -t breakpilot/studio-v2:${CI_COMMIT_SHA:0:8} ./studio-v2
|
||||
docker tag breakpilot/studio-v2:${CI_COMMIT_SHA:0:8} breakpilot/studio-v2:latest
|
||||
echo "Built breakpilot/studio-v2:${CI_COMMIT_SHA:0:8}"
|
||||
else
|
||||
echo "studio-v2 Verzeichnis nicht gefunden - ueberspringe"
|
||||
fi
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
|
||||
build-website:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- |
|
||||
if [ -d ./website ]; then
|
||||
docker build -t breakpilot/website:${CI_COMMIT_SHA:0:8} ./website
|
||||
docker tag breakpilot/website:${CI_COMMIT_SHA:0:8} breakpilot/website:latest
|
||||
echo "Built breakpilot/website:${CI_COMMIT_SHA:0:8}"
|
||||
else
|
||||
echo "website Verzeichnis nicht gefunden - ueberspringe"
|
||||
fi
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
|
||||
build-backend-lehrer:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- |
|
||||
if [ -d ./backend-lehrer ]; then
|
||||
docker build -t breakpilot/backend-lehrer:${CI_COMMIT_SHA:0:8} ./backend-lehrer
|
||||
docker tag breakpilot/backend-lehrer:${CI_COMMIT_SHA:0:8} breakpilot/backend-lehrer:latest
|
||||
echo "Built breakpilot/backend-lehrer:${CI_COMMIT_SHA:0:8}"
|
||||
else
|
||||
echo "backend-lehrer Verzeichnis nicht gefunden - ueberspringe"
|
||||
fi
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
|
||||
build-klausur-service:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- |
|
||||
if [ -d ./klausur-service ]; then
|
||||
docker build -t breakpilot/klausur-service:${CI_COMMIT_SHA:0:8} ./klausur-service
|
||||
docker tag breakpilot/klausur-service:${CI_COMMIT_SHA:0:8} breakpilot/klausur-service:latest
|
||||
echo "Built breakpilot/klausur-service:${CI_COMMIT_SHA:0:8}"
|
||||
else
|
||||
echo "klausur-service Verzeichnis nicht gefunden - ueberspringe"
|
||||
fi
|
||||
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: python:3.12-slim
|
||||
commands:
|
||||
- |
|
||||
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
|
||||
if [ -d "./$svc" ]; then
|
||||
syft dir:./$svc -o cyclonedx-json > sbom-$svc.json
|
||||
echo "SBOM generated for $svc"
|
||||
fi
|
||||
done
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
|
||||
vulnerability-scan:
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- |
|
||||
echo "Installing grype for ARM64..."
|
||||
apt-get update -qq && apt-get install -y -qq wget > /dev/null
|
||||
wget -qO- https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
for f in sbom-*.json; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "=== Scanning $f ==="
|
||||
grype sbom:"$f" -o table --fail-on critical || true
|
||||
done
|
||||
when:
|
||||
- event: tag
|
||||
- event: manual
|
||||
depends_on:
|
||||
- generate-sbom
|
||||
|
||||
# ========================================
|
||||
# STAGE 5: Deploy (nur manuell)
|
||||
# ========================================
|
||||
|
||||
deploy-production:
|
||||
image: *docker_image
|
||||
commands:
|
||||
- echo "Deploying breakpilot-lehrer 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-admin-lehrer
|
||||
- build-studio-v2
|
||||
- build-website
|
||||
- build-backend-lehrer
|
||||
- build-klausur-service
|
||||
- build-voice-service
|
||||
Reference in New Issue
Block a user