chore: Woodpecker CI entfernt — nur noch Gitea Actions
All checks were successful
CI / test-bqas (push) Successful in 27s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 28s
All checks were successful
CI / test-bqas (push) Successful in 27s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 28s
Woodpecker wird nicht mehr verwendet. Wir migrieren vollstaendig auf Gitea Actions (gitea.meghsakha.com). Entfernt: - woodpecker-server + woodpecker-agent Container (docker-compose.yml) - woodpecker_data Volume - backend-core/woodpecker_proxy_api.py (SQLite-DB Proxy) - admin-core/app/api/admin/infrastructure/woodpecker/route.ts - admin-core/app/api/webhooks/woodpecker/route.ts - .woodpecker/main.yml (alte CI-Pipeline-Konfiguration) Bereinigt: - ci-cd/page.tsx: Woodpecker-Tab + Status-Karte + State entfernt - types/infrastructure-modules.ts: Woodpecker-Typen + API-Endpunkte - DevOpsPipelineSidebar.tsx: Textbeschreibungen auf Gitea Actions - dashboard/page.tsx: Woodpecker aus Service-Health-Liste - sbom/page.tsx: Woodpecker aus SBOM-Liste - navigation.ts: Beschreibung aktualisiert - .env.example: WOODPECKER_* Variablen entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,11 +46,6 @@ ERPNEXT_DB_ROOT_PASSWORD=erpnext_root
|
|||||||
ERPNEXT_DB_PASSWORD=erpnext_secret
|
ERPNEXT_DB_PASSWORD=erpnext_secret
|
||||||
ERPNEXT_ADMIN_PASSWORD=admin
|
ERPNEXT_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
# Woodpecker CI
|
|
||||||
WOODPECKER_HOST=http://macmini:8090
|
|
||||||
WOODPECKER_ADMIN=pilotadmin
|
|
||||||
WOODPECKER_AGENT_SECRET=woodpecker-secret
|
|
||||||
|
|
||||||
# Gitea Runner
|
# Gitea Runner
|
||||||
GITEA_RUNNER_TOKEN=
|
GITEA_RUNNER_TOKEN=
|
||||||
|
|
||||||
|
|||||||
@@ -1,422 +0,0 @@
|
|||||||
# Woodpecker CI Main Pipeline
|
|
||||||
# BreakPilot Core - CI/CD Pipeline
|
|
||||||
#
|
|
||||||
# Plattform: ARM64 (Apple Silicon Mac Mini)
|
|
||||||
#
|
|
||||||
# Services:
|
|
||||||
# Go: consent-service
|
|
||||||
# Python: backend-core, voice-service (+ BQAS), embedding-service, night-scheduler
|
|
||||||
# Node.js: admin-core
|
|
||||||
#
|
|
||||||
# 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:
|
|
||||||
- &golang_image golang:1.23-alpine
|
|
||||||
- &python_image python:3.12-slim
|
|
||||||
- &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 ./...
|
|
||||||
when:
|
|
||||||
event: pull_request
|
|
||||||
|
|
||||||
python-lint:
|
|
||||||
image: *python_image
|
|
||||||
commands:
|
|
||||||
- pip install --quiet ruff
|
|
||||||
- |
|
|
||||||
for svc in backend-core voice-service night-scheduler embedding-service; 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:
|
|
||||||
- |
|
|
||||||
if [ -d "admin-core" ]; then
|
|
||||||
cd admin-core
|
|
||||||
npm ci --silent 2>/dev/null || npm install --silent
|
|
||||||
npx next lint || true
|
|
||||||
fi
|
|
||||||
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_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-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 2>/dev/null || true
|
|
||||||
pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-json-report
|
|
||||||
|
|
||||||
set +e
|
|
||||||
python -m pytest tests/ -v --tb=short --ignore=tests/bqas --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 2>/dev/null || true
|
|
||||||
pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest 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 2>/dev/null || true
|
|
||||||
pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest 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
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# 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-core\",
|
|
||||||
\"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-python-voice
|
|
||||||
- test-bqas-golden
|
|
||||||
- test-bqas-rag
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 4: Build & Security (nur Tags/manuell)
|
|
||||||
# ========================================
|
|
||||||
|
|
||||||
build-consent-service:
|
|
||||||
image: *docker_image
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -d ./consent-service ]; then
|
|
||||||
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}"
|
|
||||||
else
|
|
||||||
echo "consent-service Verzeichnis nicht gefunden - ueberspringe"
|
|
||||||
fi
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
- event: manual
|
|
||||||
|
|
||||||
build-backend-core:
|
|
||||||
image: *docker_image
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -d ./backend-core ]; then
|
|
||||||
docker build -t breakpilot/backend-core:${CI_COMMIT_SHA:0:8} ./backend-core
|
|
||||||
docker tag breakpilot/backend-core:${CI_COMMIT_SHA:0:8} breakpilot/backend-core:latest
|
|
||||||
echo "Built breakpilot/backend-core:${CI_COMMIT_SHA:0:8}"
|
|
||||||
else
|
|
||||||
echo "backend-core Verzeichnis nicht gefunden - ueberspringe"
|
|
||||||
fi
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
- event: manual
|
|
||||||
|
|
||||||
build-admin-core:
|
|
||||||
image: *docker_image
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -d ./admin-core ]; then
|
|
||||||
docker build -t breakpilot/admin-core:${CI_COMMIT_SHA:0:8} ./admin-core
|
|
||||||
docker tag breakpilot/admin-core:${CI_COMMIT_SHA:0:8} breakpilot/admin-core:latest
|
|
||||||
echo "Built breakpilot/admin-core:${CI_COMMIT_SHA:0:8}"
|
|
||||||
else
|
|
||||||
echo "admin-core 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
|
|
||||||
|
|
||||||
build-embedding-service:
|
|
||||||
image: *docker_image
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -d ./embedding-service ]; then
|
|
||||||
docker build -t breakpilot/embedding-service:${CI_COMMIT_SHA:0:8} ./embedding-service
|
|
||||||
docker tag breakpilot/embedding-service:${CI_COMMIT_SHA:0:8} breakpilot/embedding-service:latest
|
|
||||||
echo "Built breakpilot/embedding-service:${CI_COMMIT_SHA:0:8}"
|
|
||||||
else
|
|
||||||
echo "embedding-service Verzeichnis nicht gefunden - ueberspringe"
|
|
||||||
fi
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
- event: manual
|
|
||||||
|
|
||||||
build-night-scheduler:
|
|
||||||
image: *docker_image
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -d ./night-scheduler ]; then
|
|
||||||
docker build -t breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8} ./night-scheduler
|
|
||||||
docker tag breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8} breakpilot/night-scheduler:latest
|
|
||||||
echo "Built breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8}"
|
|
||||||
else
|
|
||||||
echo "night-scheduler 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
|
|
||||||
for svc in consent-service backend-core voice-service embedding-service night-scheduler; 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: *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
|
|
||||||
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-core 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-core
|
|
||||||
- build-admin-core
|
|
||||||
- build-voice-service
|
|
||||||
- build-embedding-service
|
|
||||||
- build-night-scheduler
|
|
||||||
@@ -27,7 +27,6 @@ export default function DashboardPage() {
|
|||||||
{ name: 'Jitsi Meet', status: 'unknown' },
|
{ name: 'Jitsi Meet', status: 'unknown' },
|
||||||
{ name: 'Mailpit', status: 'unknown' },
|
{ name: 'Mailpit', status: 'unknown' },
|
||||||
{ name: 'Gitea', status: 'unknown' },
|
{ name: 'Gitea', status: 'unknown' },
|
||||||
{ name: 'Woodpecker CI', status: 'unknown' },
|
|
||||||
{ name: 'Backend Core', status: 'unknown' },
|
{ name: 'Backend Core', status: 'unknown' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -85,38 +85,7 @@ interface DockerStats {
|
|||||||
stopped_containers: number
|
stopped_containers: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabType = 'overview' | 'woodpecker' | 'pipelines' | 'deployments' | 'setup' | 'scheduler'
|
type TabType = 'overview' | 'pipelines' | 'deployments' | 'setup' | 'scheduler'
|
||||||
|
|
||||||
// Woodpecker Types
|
|
||||||
interface WoodpeckerStep {
|
|
||||||
name: string
|
|
||||||
state: 'pending' | 'running' | 'success' | 'failure' | 'skipped'
|
|
||||||
exit_code: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WoodpeckerPipeline {
|
|
||||||
id: number
|
|
||||||
number: number
|
|
||||||
status: 'pending' | 'running' | 'success' | 'failure' | 'error'
|
|
||||||
event: string
|
|
||||||
branch: string
|
|
||||||
commit: string
|
|
||||||
message: string
|
|
||||||
author: string
|
|
||||||
created: number
|
|
||||||
started: number
|
|
||||||
finished: number
|
|
||||||
steps: WoodpeckerStep[]
|
|
||||||
errors?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WoodpeckerStatus {
|
|
||||||
status: 'online' | 'offline'
|
|
||||||
pipelines: WoodpeckerPipeline[]
|
|
||||||
lastUpdate: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Components
|
// Helper Components
|
||||||
@@ -168,10 +137,6 @@ export default function CICDPage() {
|
|||||||
const [containerFilter, setContainerFilter] = useState<'all' | 'running' | 'stopped'>('all')
|
const [containerFilter, setContainerFilter] = useState<'all' | 'running' | 'stopped'>('all')
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||||
|
|
||||||
// Woodpecker State
|
|
||||||
const [woodpeckerStatus, setWoodpeckerStatus] = useState<WoodpeckerStatus | null>(null)
|
|
||||||
const [triggeringWoodpecker, setTriggeringWoodpecker] = useState(false)
|
|
||||||
|
|
||||||
// General State
|
// General State
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -214,54 +179,12 @@ export default function CICDPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadWoodpeckerData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/infrastructure/woodpecker?limit=10')
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setWoodpeckerStatus(data)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load Woodpecker data:', err)
|
|
||||||
setWoodpeckerStatus({
|
|
||||||
status: 'offline',
|
|
||||||
pipelines: [],
|
|
||||||
lastUpdate: new Date().toISOString(),
|
|
||||||
error: 'Verbindung fehlgeschlagen'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const triggerWoodpeckerPipeline = async () => {
|
|
||||||
setTriggeringWoodpecker(true)
|
|
||||||
setMessage(null)
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/infrastructure/woodpecker', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ branch: 'main' })
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json()
|
|
||||||
setMessage(`Woodpecker Pipeline #${result.pipeline?.number || '?'} gestartet!`)
|
|
||||||
setTimeout(loadWoodpeckerData, 2000)
|
|
||||||
setTimeout(loadWoodpeckerData, 5000)
|
|
||||||
} else {
|
|
||||||
setError('Pipeline-Start fehlgeschlagen')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Pipeline konnte nicht gestartet werden')
|
|
||||||
} finally {
|
|
||||||
setTriggeringWoodpecker(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadAllData = useCallback(async () => {
|
const loadAllData = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
await Promise.all([loadPipelineData(), loadContainerData(), loadWoodpeckerData()])
|
await Promise.all([loadPipelineData(), loadContainerData()])
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [loadPipelineData, loadContainerData, loadWoodpeckerData])
|
}, [loadPipelineData, loadContainerData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAllData()
|
loadAllData()
|
||||||
@@ -402,11 +325,6 @@ export default function CICDPage() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
)},
|
)},
|
||||||
{ id: 'woodpecker', name: 'Woodpecker CI', icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
)},
|
|
||||||
{ id: 'pipelines', name: 'Gitea Pipelines', icon: (
|
{ id: 'pipelines', name: 'Gitea Pipelines', icon: (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
@@ -458,95 +376,6 @@ export default function CICDPage() {
|
|||||||
{/* ================================================================ */}
|
{/* ================================================================ */}
|
||||||
{activeTab === 'overview' && (
|
{activeTab === 'overview' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Woodpecker CI Status - Prominent */}
|
|
||||||
<div className={`p-4 rounded-xl border-2 ${
|
|
||||||
woodpeckerStatus?.status === 'online'
|
|
||||||
? woodpeckerStatus.pipelines?.[0]?.status === 'success'
|
|
||||||
? 'border-green-300 bg-green-50'
|
|
||||||
: woodpeckerStatus.pipelines?.[0]?.status === 'failure' || woodpeckerStatus.pipelines?.[0]?.status === 'error'
|
|
||||||
? 'border-red-300 bg-red-50'
|
|
||||||
: woodpeckerStatus.pipelines?.[0]?.status === 'running'
|
|
||||||
? 'border-blue-300 bg-blue-50'
|
|
||||||
: 'border-slate-300 bg-slate-50'
|
|
||||||
: 'border-red-300 bg-red-50'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className={`p-3 rounded-lg ${
|
|
||||||
woodpeckerStatus?.status === 'online'
|
|
||||||
? woodpeckerStatus.pipelines?.[0]?.status === 'success'
|
|
||||||
? 'bg-green-100'
|
|
||||||
: woodpeckerStatus.pipelines?.[0]?.status === 'failure' || woodpeckerStatus.pipelines?.[0]?.status === 'error'
|
|
||||||
? 'bg-red-100'
|
|
||||||
: 'bg-blue-100'
|
|
||||||
: 'bg-red-100'
|
|
||||||
}`}>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-slate-900">Woodpecker CI</h3>
|
|
||||||
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
|
||||||
woodpeckerStatus?.status === 'online' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
||||||
}`}>
|
|
||||||
{woodpeckerStatus?.status === 'online' ? 'Online' : 'Offline'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{woodpeckerStatus?.pipelines?.[0] && (
|
|
||||||
<p className="text-sm text-slate-600 mt-1">
|
|
||||||
Pipeline #{woodpeckerStatus.pipelines[0].number}: {' '}
|
|
||||||
<span className={`font-medium ${
|
|
||||||
woodpeckerStatus.pipelines[0].status === 'success' ? 'text-green-600' :
|
|
||||||
woodpeckerStatus.pipelines[0].status === 'failure' || woodpeckerStatus.pipelines[0].status === 'error' ? 'text-red-600' :
|
|
||||||
woodpeckerStatus.pipelines[0].status === 'running' ? 'text-blue-600' : 'text-slate-600'
|
|
||||||
}`}>
|
|
||||||
{woodpeckerStatus.pipelines[0].status}
|
|
||||||
</span>
|
|
||||||
{' '}auf {woodpeckerStatus.pipelines[0].branch}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('woodpecker')}
|
|
||||||
className="px-3 py-1.5 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-white"
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={triggerWoodpeckerPipeline}
|
|
||||||
disabled={triggeringWoodpecker}
|
|
||||||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{triggeringWoodpecker ? (
|
|
||||||
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white" />
|
|
||||||
) : (
|
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
Starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Failed steps preview */}
|
|
||||||
{woodpeckerStatus?.pipelines?.[0]?.steps?.some(s => s.state === 'failure') && (
|
|
||||||
<div className="mt-3 pt-3 border-t border-red-200">
|
|
||||||
<p className="text-xs font-medium text-red-700 mb-2">Fehlgeschlagene Steps:</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{woodpeckerStatus.pipelines[0].steps.filter(s => s.state === 'failure').map((step, i) => (
|
|
||||||
<span key={i} className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">
|
|
||||||
{step.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Cards */}
|
{/* Status Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div className={`p-4 rounded-lg ${pipelineStatus?.gitea_connected ? 'bg-green-50' : 'bg-yellow-50'}`}>
|
<div className={`p-4 rounded-lg ${pipelineStatus?.gitea_connected ? 'bg-green-50' : 'bg-yellow-50'}`}>
|
||||||
@@ -679,299 +508,6 @@ export default function CICDPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ================================================================ */}
|
|
||||||
{/* Woodpecker Tab */}
|
|
||||||
{/* ================================================================ */}
|
|
||||||
{activeTab === 'woodpecker' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Woodpecker Status Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h3 className="text-lg font-semibold text-slate-800">Woodpecker CI Pipeline</h3>
|
|
||||||
<span className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${
|
|
||||||
woodpeckerStatus?.status === 'online'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: 'bg-red-100 text-red-800'
|
|
||||||
}`}>
|
|
||||||
<span className={`w-2 h-2 rounded-full ${
|
|
||||||
woodpeckerStatus?.status === 'online' ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`} />
|
|
||||||
{woodpeckerStatus?.status === 'online' ? 'Online' : 'Offline'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
href="http://macmini:8090"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="px-3 py-2 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
Woodpecker UI
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={triggerWoodpeckerPipeline}
|
|
||||||
disabled={triggeringWoodpecker}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{triggeringWoodpecker ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
|
||||||
Startet...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
Pipeline starten
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pipeline Stats */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Gesamt</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-blue-700">{woodpeckerStatus?.pipelines?.length || 0}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 p-4 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Erfolgreich</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-green-700">
|
|
||||||
{woodpeckerStatus?.pipelines?.filter(p => p.status === 'success').length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-50 p-4 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Fehlgeschlagen</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-red-700">
|
|
||||||
{woodpeckerStatus?.pipelines?.filter(p => p.status === 'failure' || p.status === 'error').length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Laufend</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-yellow-700">
|
|
||||||
{woodpeckerStatus?.pipelines?.filter(p => p.status === 'running' || p.status === 'pending').length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pipeline List */}
|
|
||||||
{woodpeckerStatus?.pipelines && woodpeckerStatus.pipelines.length > 0 ? (
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium text-slate-800 mb-4">Pipeline Historie</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{woodpeckerStatus.pipelines.map((pipeline) => (
|
|
||||||
<div
|
|
||||||
key={pipeline.id}
|
|
||||||
className={`border rounded-xl p-4 transition-colors ${
|
|
||||||
pipeline.status === 'success'
|
|
||||||
? 'border-green-200 bg-green-50/30'
|
|
||||||
: pipeline.status === 'failure' || pipeline.status === 'error'
|
|
||||||
? 'border-red-200 bg-red-50/30'
|
|
||||||
: pipeline.status === 'running'
|
|
||||||
? 'border-blue-200 bg-blue-50/30'
|
|
||||||
: 'border-slate-200 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className={`w-3 h-3 rounded-full ${
|
|
||||||
pipeline.status === 'success' ? 'bg-green-500' :
|
|
||||||
pipeline.status === 'failure' || pipeline.status === 'error' ? 'bg-red-500' :
|
|
||||||
pipeline.status === 'running' ? 'bg-blue-500 animate-pulse' : 'bg-slate-400'
|
|
||||||
}`} />
|
|
||||||
<span className="font-semibold text-slate-900">Pipeline #{pipeline.number}</span>
|
|
||||||
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
|
||||||
pipeline.status === 'success' ? 'bg-green-100 text-green-800' :
|
|
||||||
pipeline.status === 'failure' || pipeline.status === 'error' ? 'bg-red-100 text-red-800' :
|
|
||||||
pipeline.status === 'running' ? 'bg-blue-100 text-blue-800' :
|
|
||||||
'bg-slate-100 text-slate-600'
|
|
||||||
}`}>
|
|
||||||
{pipeline.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-slate-600 mb-2">
|
|
||||||
<span className="font-mono">{pipeline.branch}</span>
|
|
||||||
<span className="mx-2 text-slate-400">•</span>
|
|
||||||
<span className="font-mono text-slate-500">{pipeline.commit}</span>
|
|
||||||
<span className="mx-2 text-slate-400">•</span>
|
|
||||||
<span>{pipeline.event}</span>
|
|
||||||
</div>
|
|
||||||
{pipeline.message && (
|
|
||||||
<p className="text-sm text-slate-500 mb-2 truncate max-w-xl">{pipeline.message}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Steps Progress */}
|
|
||||||
{pipeline.steps && pipeline.steps.length > 0 && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<div className="flex gap-1 mb-2">
|
|
||||||
{pipeline.steps.map((step, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`h-2 flex-1 rounded-full ${
|
|
||||||
step.state === 'success' ? 'bg-green-500' :
|
|
||||||
step.state === 'failure' ? 'bg-red-500' :
|
|
||||||
step.state === 'running' ? 'bg-blue-500 animate-pulse' :
|
|
||||||
step.state === 'skipped' ? 'bg-slate-200' : 'bg-slate-300'
|
|
||||||
}`}
|
|
||||||
title={`${step.name}: ${step.state}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 text-xs">
|
|
||||||
{pipeline.steps.map((step, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className={`px-2 py-1 rounded ${
|
|
||||||
step.state === 'success' ? 'bg-green-100 text-green-700' :
|
|
||||||
step.state === 'failure' ? 'bg-red-100 text-red-700' :
|
|
||||||
step.state === 'running' ? 'bg-blue-100 text-blue-700' :
|
|
||||||
'bg-slate-100 text-slate-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{step.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Errors */}
|
|
||||||
{pipeline.errors && pipeline.errors.length > 0 && (
|
|
||||||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
||||||
<h5 className="text-sm font-medium text-red-800 mb-1">Fehler:</h5>
|
|
||||||
<ul className="text-xs text-red-700 space-y-1">
|
|
||||||
{pipeline.errors.map((err, i) => (
|
|
||||||
<li key={i} className="font-mono">{err}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-right text-sm text-slate-500">
|
|
||||||
<p>{new Date(pipeline.created * 1000).toLocaleDateString('de-DE')}</p>
|
|
||||||
<p className="text-xs">{new Date(pipeline.created * 1000).toLocaleTimeString('de-DE')}</p>
|
|
||||||
{pipeline.started && pipeline.finished && (
|
|
||||||
<p className="text-xs mt-1">
|
|
||||||
Dauer: {Math.round((pipeline.finished - pipeline.started) / 60)}m
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-slate-50 rounded-lg p-8 text-center">
|
|
||||||
<svg className="w-12 h-12 text-slate-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
||||||
</svg>
|
|
||||||
<p className="text-slate-500">Keine Pipelines gefunden</p>
|
|
||||||
<p className="text-sm text-slate-400 mt-1">Starte eine neue Pipeline oder pruefe die Woodpecker-Konfiguration</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pipeline Configuration Info */}
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium text-slate-800 mb-3">Pipeline Konfiguration</h4>
|
|
||||||
<pre className="bg-slate-800 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm">
|
|
||||||
{`Woodpecker CI Pipeline (.woodpecker/main.yml)
|
|
||||||
│
|
|
||||||
├── 1. go-lint → Go Linting (PR only)
|
|
||||||
├── 2. python-lint → Python Linting (PR only)
|
|
||||||
├── 3. secrets-scan → GitLeaks Secrets Scan
|
|
||||||
│
|
|
||||||
├── 4. test-go-consent → Go Unit Tests
|
|
||||||
├── 5. test-go-billing → Billing Service Tests
|
|
||||||
├── 6. test-go-school → School Service Tests
|
|
||||||
├── 7. test-python → Python Backend Tests
|
|
||||||
│
|
|
||||||
├── 8. build-images → Docker Image Build
|
|
||||||
├── 9. generate-sbom → SBOM Generation (Syft)
|
|
||||||
├── 10. vuln-scan → Vulnerability Scan (Grype)
|
|
||||||
├── 11. container-scan → Container Scan (Trivy)
|
|
||||||
│
|
|
||||||
├── 12. sign-images → Cosign Image Signing
|
|
||||||
├── 13. attest-sbom → SBOM Attestation
|
|
||||||
├── 14. provenance → SLSA Provenance
|
|
||||||
│
|
|
||||||
└── 15. deploy-prod → Production Deployment`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Workflow Anleitung */}
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium text-blue-800 mb-3 flex items-center gap-2">
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
Workflow-Anleitung
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<h5 className="font-medium text-blue-700 mb-2">🤖 Automatisch (bei jedem Push/PR):</h5>
|
|
||||||
<ul className="space-y-1 text-blue-600">
|
|
||||||
<li>• <strong>Linting</strong> - Code-Qualitaet pruefen (nur PRs)</li>
|
|
||||||
<li>• <strong>Unit Tests</strong> - Go & Python Tests</li>
|
|
||||||
<li>• <strong>Test-Dashboard</strong> - Ergebnisse werden gesendet</li>
|
|
||||||
<li>• <strong>Backlog</strong> - Fehlgeschlagene Tests werden erfasst</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-medium text-blue-700 mb-2">👆 Manuell (Button oder Tag):</h5>
|
|
||||||
<ul className="space-y-1 text-blue-600">
|
|
||||||
<li>• <strong>Docker Builds</strong> - Container erstellen</li>
|
|
||||||
<li>• <strong>SBOM/Scans</strong> - Sicherheitsanalyse</li>
|
|
||||||
<li>• <strong>Deployment</strong> - In Produktion deployen</li>
|
|
||||||
<li>• <strong>Pipeline starten</strong> - Diesen Button verwenden</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 pt-3 border-t border-blue-200">
|
|
||||||
<h5 className="font-medium text-blue-700 mb-2">⚙️ Setup: API Token konfigurieren</h5>
|
|
||||||
<p className="text-blue-600 text-sm">
|
|
||||||
Um Pipelines ueber das Dashboard zu starten, muss ein <strong>WOODPECKER_TOKEN</strong> konfiguriert werden:
|
|
||||||
</p>
|
|
||||||
<ol className="mt-2 space-y-1 text-blue-600 text-sm list-decimal list-inside">
|
|
||||||
<li>Woodpecker UI oeffnen: <a href="http://macmini:8090" target="_blank" rel="noopener noreferrer" className="underline hover:text-blue-800">http://macmini:8090</a></li>
|
|
||||||
<li>Mit Gitea-Account einloggen</li>
|
|
||||||
<li>Klick auf Profil → <strong>User Settings</strong> → <strong>Personal Access Tokens</strong></li>
|
|
||||||
<li>Neues Token erstellen und in <code className="bg-blue-100 px-1 rounded">.env</code> eintragen: <code className="bg-blue-100 px-1 rounded">WOODPECKER_TOKEN=...</code></li>
|
|
||||||
<li>Container neu starten: <code className="bg-blue-100 px-1 rounded">docker compose up -d admin-v2</code></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ================================================================ */}
|
{/* ================================================================ */}
|
||||||
{/* Pipelines Tab */}
|
{/* Pipelines Tab */}
|
||||||
{/* ================================================================ */}
|
{/* ================================================================ */}
|
||||||
|
|||||||
@@ -110,8 +110,7 @@ const INFRASTRUCTURE_COMPONENTS: Component[] = [
|
|||||||
{ type: 'service', name: 'ERPNext', version: 'v15', category: 'erp', port: '8090', description: 'Open Source ERP System', license: 'GPL-3.0', sourceUrl: 'https://github.com/frappe/erpnext' },
|
{ type: 'service', name: 'ERPNext', version: 'v15', category: 'erp', port: '8090', description: 'Open Source ERP System', license: 'GPL-3.0', sourceUrl: 'https://github.com/frappe/erpnext' },
|
||||||
|
|
||||||
// ===== CI/CD & VERSION CONTROL =====
|
// ===== CI/CD & VERSION CONTROL =====
|
||||||
{ type: 'service', name: 'Woodpecker CI', version: '2.x', category: 'cicd', port: '8082', description: 'Self-hosted CI/CD Pipeline (Drone Fork)', license: 'Apache-2.0', sourceUrl: 'https://github.com/woodpecker-ci/woodpecker' },
|
{ type: 'service', name: 'Gitea', version: '1.21', category: 'cicd', port: '3003', description: 'Self-hosted Git Service with Actions CI/CD', license: 'MIT', sourceUrl: 'https://github.com/go-gitea/gitea' },
|
||||||
{ type: 'service', name: 'Gitea', version: '1.21', category: 'cicd', port: '3003', description: 'Self-hosted Git Service', license: 'MIT', sourceUrl: 'https://github.com/go-gitea/gitea' },
|
|
||||||
{ type: 'service', name: 'Dokploy', version: '0.26.7', category: 'cicd', port: '3000', description: 'Self-hosted PaaS (Vercel/Heroku Alternative)', license: 'Apache-2.0', sourceUrl: 'https://github.com/Dokploy/dokploy' },
|
{ type: 'service', name: 'Dokploy', version: '0.26.7', category: 'cicd', port: '3000', description: 'Self-hosted PaaS (Vercel/Heroku Alternative)', license: 'Apache-2.0', sourceUrl: 'https://github.com/Dokploy/dokploy' },
|
||||||
|
|
||||||
// ===== DEVELOPMENT =====
|
// ===== DEVELOPMENT =====
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ Tests bleiben wo sie sind:
|
|||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-blue-200">
|
<div className="mt-4 pt-4 border-t border-blue-200">
|
||||||
<p className="text-sm text-blue-600">
|
<p className="text-sm text-blue-600">
|
||||||
<strong>Daten-Fluss:</strong> Woodpecker CI → POST /api/tests/ci-result → PostgreSQL → Test Dashboard
|
<strong>Daten-Fluss:</strong> Gitea Actions → POST /api/tests/ci-result → PostgreSQL → Test Dashboard
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
// Woodpecker API configuration
|
|
||||||
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
|
|
||||||
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
|
|
||||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-core:8000'
|
|
||||||
|
|
||||||
export interface PipelineStep {
|
|
||||||
name: string
|
|
||||||
state: 'pending' | 'running' | 'success' | 'failure' | 'skipped'
|
|
||||||
exit_code: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Pipeline {
|
|
||||||
id: number
|
|
||||||
number: number
|
|
||||||
status: 'pending' | 'running' | 'success' | 'failure' | 'error'
|
|
||||||
event: string
|
|
||||||
branch: string
|
|
||||||
commit: string
|
|
||||||
message: string
|
|
||||||
author: string
|
|
||||||
created: number
|
|
||||||
started: number
|
|
||||||
finished: number
|
|
||||||
steps: PipelineStep[]
|
|
||||||
errors?: string[]
|
|
||||||
repo_name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WoodpeckerStatusResponse {
|
|
||||||
status: 'online' | 'offline'
|
|
||||||
pipelines: Pipeline[]
|
|
||||||
lastUpdate: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFromBackendProxy(repoId: string, limit: number): Promise<WoodpeckerStatusResponse> {
|
|
||||||
// Use backend-core proxy that reads Woodpecker sqlite DB directly
|
|
||||||
const url = `${BACKEND_URL}/api/v1/woodpecker/pipelines?repo=${repoId}&limit=${limit}`
|
|
||||||
const response = await fetch(url, { cache: 'no-store' })
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
status: 'offline',
|
|
||||||
pipelines: [],
|
|
||||||
lastUpdate: new Date().toISOString(),
|
|
||||||
error: `Backend Woodpecker Proxy Fehler (${response.status})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return {
|
|
||||||
status: data.status || 'online',
|
|
||||||
pipelines: (data.pipelines || []).map((p: any) => ({
|
|
||||||
id: p.id,
|
|
||||||
number: p.number,
|
|
||||||
status: p.status,
|
|
||||||
event: p.event,
|
|
||||||
branch: p.branch || 'main',
|
|
||||||
commit: p.commit || '',
|
|
||||||
message: p.message || '',
|
|
||||||
author: p.author || '',
|
|
||||||
created: p.created,
|
|
||||||
started: p.started,
|
|
||||||
finished: p.finished,
|
|
||||||
repo_name: p.repo_name,
|
|
||||||
steps: (p.steps || []).map((s: any) => ({
|
|
||||||
name: s.name,
|
|
||||||
state: s.state,
|
|
||||||
exit_code: s.exit_code || 0,
|
|
||||||
error: s.error
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
lastUpdate: data.lastUpdate || new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFromWoodpeckerAPI(repoId: string, limit: number): Promise<WoodpeckerStatusResponse> {
|
|
||||||
const response = await fetch(
|
|
||||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
status: 'offline',
|
|
||||||
pipelines: [],
|
|
||||||
lastUpdate: new Date().toISOString(),
|
|
||||||
error: `Woodpecker API nicht erreichbar (${response.status})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawPipelines = await response.json()
|
|
||||||
|
|
||||||
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const steps: PipelineStep[] = []
|
|
||||||
|
|
||||||
if (p.workflows) {
|
|
||||||
for (const workflow of p.workflows) {
|
|
||||||
if (workflow.children) {
|
|
||||||
for (const child of workflow.children) {
|
|
||||||
steps.push({
|
|
||||||
name: child.name,
|
|
||||||
state: child.state,
|
|
||||||
exit_code: child.exit_code,
|
|
||||||
error: child.error
|
|
||||||
})
|
|
||||||
if (child.state === 'failure' && child.error) {
|
|
||||||
errors.push(`${child.name}: ${child.error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: p.id,
|
|
||||||
number: p.number,
|
|
||||||
status: p.status,
|
|
||||||
event: p.event,
|
|
||||||
branch: p.branch,
|
|
||||||
commit: p.commit?.substring(0, 7) || '',
|
|
||||||
message: p.message || '',
|
|
||||||
author: p.author,
|
|
||||||
created: p.created,
|
|
||||||
started: p.started,
|
|
||||||
finished: p.finished,
|
|
||||||
steps,
|
|
||||||
errors: errors.length > 0 ? errors : undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'online',
|
|
||||||
pipelines,
|
|
||||||
lastUpdate: new Date().toISOString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const searchParams = request.nextUrl.searchParams
|
|
||||||
const repoId = searchParams.get('repo') || '0'
|
|
||||||
const limit = parseInt(searchParams.get('limit') || '10')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If WOODPECKER_TOKEN is set, use the Woodpecker API directly
|
|
||||||
// Otherwise, use the backend proxy that reads the sqlite DB
|
|
||||||
if (WOODPECKER_TOKEN) {
|
|
||||||
return NextResponse.json(await fetchFromWoodpeckerAPI(repoId, limit))
|
|
||||||
} else {
|
|
||||||
return NextResponse.json(await fetchFromBackendProxy(repoId, limit))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Woodpecker API error:', error)
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'offline',
|
|
||||||
pipelines: [],
|
|
||||||
lastUpdate: new Date().toISOString(),
|
|
||||||
error: 'Fehler beim Abrufen des Woodpecker Status'
|
|
||||||
} as WoodpeckerStatusResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger a new pipeline
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { repoId = '1', branch = 'main' } = body
|
|
||||||
|
|
||||||
if (!WOODPECKER_TOKEN) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'WOODPECKER_TOKEN nicht konfiguriert - Pipeline-Start nicht moeglich' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ branch }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Pipeline konnte nicht gestartet werden' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pipeline = await response.json()
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
pipeline: {
|
|
||||||
id: pipeline.id,
|
|
||||||
number: pipeline.number,
|
|
||||||
status: pipeline.status
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Pipeline trigger error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Fehler beim Starten der Pipeline' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get pipeline logs
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { repoId = '1', pipelineNumber, stepId } = body
|
|
||||||
|
|
||||||
if (!pipelineNumber || !stepId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'pipelineNumber und stepId erforderlich' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!WOODPECKER_TOKEN) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'WOODPECKER_TOKEN nicht konfiguriert' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines/${pipelineNumber}/logs/${stepId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Logs nicht verfuegbar' },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const logs = await response.json()
|
|
||||||
return NextResponse.json({ logs })
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Pipeline logs error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Fehler beim Abrufen der Logs' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import type { WoodpeckerWebhookPayload, ExtractedError, BacklogSource } from '@/types/infrastructure-modules'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Configuration
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// Webhook secret for verification (optional but recommended)
|
|
||||||
const WEBHOOK_SECRET = process.env.WOODPECKER_WEBHOOK_SECRET || ''
|
|
||||||
|
|
||||||
// Internal API URL for log extraction
|
|
||||||
const LOG_EXTRACT_URL = process.env.NEXT_PUBLIC_APP_URL
|
|
||||||
? `${process.env.NEXT_PUBLIC_APP_URL}/api/infrastructure/log-extract/extract`
|
|
||||||
: 'http://localhost:3002/api/infrastructure/log-extract/extract'
|
|
||||||
|
|
||||||
// Test service API URL for backlog insertion
|
|
||||||
const TEST_SERVICE_URL = process.env.TEST_SERVICE_URL || 'http://localhost:8086'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Helper Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify webhook signature (if secret is configured)
|
|
||||||
*/
|
|
||||||
function verifySignature(request: NextRequest, body: string): boolean {
|
|
||||||
if (!WEBHOOK_SECRET) return true // Skip verification if no secret configured
|
|
||||||
|
|
||||||
const signature = request.headers.get('X-Woodpecker-Signature')
|
|
||||||
if (!signature) return false
|
|
||||||
|
|
||||||
// Simple HMAC verification (Woodpecker uses SHA256)
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const expectedSignature = crypto
|
|
||||||
.createHmac('sha256', WEBHOOK_SECRET)
|
|
||||||
.update(body)
|
|
||||||
.digest('hex')
|
|
||||||
|
|
||||||
return signature === `sha256=${expectedSignature}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map error category to backlog priority
|
|
||||||
*/
|
|
||||||
function categoryToPriority(category: string): 'critical' | 'high' | 'medium' | 'low' {
|
|
||||||
switch (category) {
|
|
||||||
case 'security_warning':
|
|
||||||
return 'critical'
|
|
||||||
case 'build_error':
|
|
||||||
return 'high'
|
|
||||||
case 'license_violation':
|
|
||||||
return 'high'
|
|
||||||
case 'test_failure':
|
|
||||||
return 'medium'
|
|
||||||
case 'dependency_issue':
|
|
||||||
return 'low'
|
|
||||||
default:
|
|
||||||
return 'medium'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map error category to error_type for backlog
|
|
||||||
*/
|
|
||||||
function categoryToErrorType(category: string): string {
|
|
||||||
switch (category) {
|
|
||||||
case 'security_warning':
|
|
||||||
return 'security'
|
|
||||||
case 'build_error':
|
|
||||||
return 'build'
|
|
||||||
case 'license_violation':
|
|
||||||
return 'license'
|
|
||||||
case 'test_failure':
|
|
||||||
return 'test'
|
|
||||||
case 'dependency_issue':
|
|
||||||
return 'dependency'
|
|
||||||
default:
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert extracted errors into backlog
|
|
||||||
*/
|
|
||||||
async function insertIntoBacklog(
|
|
||||||
errors: ExtractedError[],
|
|
||||||
pipelineNumber: number,
|
|
||||||
source: BacklogSource
|
|
||||||
): Promise<{ inserted: number; failed: number }> {
|
|
||||||
let inserted = 0
|
|
||||||
let failed = 0
|
|
||||||
|
|
||||||
for (const error of errors) {
|
|
||||||
try {
|
|
||||||
// Create backlog item
|
|
||||||
const backlogItem = {
|
|
||||||
test_name: error.message.substring(0, 200), // Truncate long messages
|
|
||||||
test_file: error.file_path || null,
|
|
||||||
service: error.service || 'unknown',
|
|
||||||
framework: `ci_cd_pipeline_${pipelineNumber}`,
|
|
||||||
error_message: error.message,
|
|
||||||
error_type: categoryToErrorType(error.category),
|
|
||||||
status: 'open',
|
|
||||||
priority: categoryToPriority(error.category),
|
|
||||||
fix_suggestion: error.suggested_fix || null,
|
|
||||||
notes: `Auto-generated from pipeline #${pipelineNumber}, step: ${error.step}, line: ${error.line}`,
|
|
||||||
source, // Custom field to track origin
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to insert into test service backlog
|
|
||||||
const response = await fetch(`${TEST_SERVICE_URL}/api/v1/backlog`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(backlogItem),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
inserted++
|
|
||||||
} else {
|
|
||||||
console.warn(`Failed to insert backlog item: ${response.status}`)
|
|
||||||
failed++
|
|
||||||
}
|
|
||||||
} catch (insertError) {
|
|
||||||
console.error('Backlog insertion error:', insertError)
|
|
||||||
failed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { inserted, failed }
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API Handler
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/webhooks/woodpecker
|
|
||||||
*
|
|
||||||
* Webhook endpoint fuer Woodpecker CI/CD Events.
|
|
||||||
*
|
|
||||||
* Bei Pipeline-Failure:
|
|
||||||
* 1. Extrahiert Logs mit /api/infrastructure/logs/extract
|
|
||||||
* 2. Parsed Fehler nach Kategorie
|
|
||||||
* 3. Traegt automatisch in Backlog ein
|
|
||||||
*
|
|
||||||
* Request Body (Woodpecker Webhook Format):
|
|
||||||
* - event: 'pipeline_success' | 'pipeline_failure' | 'pipeline_started'
|
|
||||||
* - repo_id: number
|
|
||||||
* - pipeline_number: number
|
|
||||||
* - branch?: string
|
|
||||||
* - commit?: string
|
|
||||||
* - author?: string
|
|
||||||
* - message?: string
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const bodyText = await request.text()
|
|
||||||
|
|
||||||
// Verify webhook signature
|
|
||||||
if (!verifySignature(request, bodyText)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid webhook signature' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: WoodpeckerWebhookPayload = JSON.parse(bodyText)
|
|
||||||
|
|
||||||
// Log all events for debugging
|
|
||||||
console.log(`Woodpecker webhook: ${payload.event} for pipeline #${payload.pipeline_number}`)
|
|
||||||
|
|
||||||
// Only process pipeline_failure events
|
|
||||||
if (payload.event !== 'pipeline_failure') {
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'ignored',
|
|
||||||
message: `Event ${payload.event} wird nicht verarbeitet`,
|
|
||||||
pipeline_number: payload.pipeline_number,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Extract logs from failed pipeline
|
|
||||||
console.log(`Extracting logs for failed pipeline #${payload.pipeline_number}`)
|
|
||||||
|
|
||||||
const extractResponse = await fetch(LOG_EXTRACT_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
pipeline_number: payload.pipeline_number,
|
|
||||||
repo_id: String(payload.repo_id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!extractResponse.ok) {
|
|
||||||
const errorText = await extractResponse.text()
|
|
||||||
console.error('Log extraction failed:', errorText)
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'error',
|
|
||||||
message: 'Log-Extraktion fehlgeschlagen',
|
|
||||||
pipeline_number: payload.pipeline_number,
|
|
||||||
}, { status: 500 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractionResult = await extractResponse.json()
|
|
||||||
const errors: ExtractedError[] = extractionResult.errors || []
|
|
||||||
|
|
||||||
console.log(`Extracted ${errors.length} errors from pipeline #${payload.pipeline_number}`)
|
|
||||||
|
|
||||||
// 2. Insert errors into backlog
|
|
||||||
if (errors.length > 0) {
|
|
||||||
const backlogResult = await insertIntoBacklog(
|
|
||||||
errors,
|
|
||||||
payload.pipeline_number,
|
|
||||||
'ci_cd'
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(`Backlog: ${backlogResult.inserted} inserted, ${backlogResult.failed} failed`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'processed',
|
|
||||||
pipeline_number: payload.pipeline_number,
|
|
||||||
branch: payload.branch,
|
|
||||||
commit: payload.commit,
|
|
||||||
errors_found: errors.length,
|
|
||||||
backlog_inserted: backlogResult.inserted,
|
|
||||||
backlog_failed: backlogResult.failed,
|
|
||||||
categories: {
|
|
||||||
test_failure: errors.filter(e => e.category === 'test_failure').length,
|
|
||||||
build_error: errors.filter(e => e.category === 'build_error').length,
|
|
||||||
security_warning: errors.filter(e => e.category === 'security_warning').length,
|
|
||||||
license_violation: errors.filter(e => e.category === 'license_violation').length,
|
|
||||||
dependency_issue: errors.filter(e => e.category === 'dependency_issue').length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'processed',
|
|
||||||
pipeline_number: payload.pipeline_number,
|
|
||||||
message: 'Keine Fehler extrahiert',
|
|
||||||
errors_found: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Webhook processing error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Webhook-Verarbeitung fehlgeschlagen' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/webhooks/woodpecker
|
|
||||||
*
|
|
||||||
* Health check endpoint
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'ready',
|
|
||||||
endpoint: '/api/webhooks/woodpecker',
|
|
||||||
events: ['pipeline_failure'],
|
|
||||||
description: 'Woodpecker CI/CD Webhook Handler',
|
|
||||||
configured: {
|
|
||||||
webhook_secret: WEBHOOK_SECRET ? 'yes' : 'no',
|
|
||||||
log_extract_url: LOG_EXTRACT_URL,
|
|
||||||
test_service_url: TEST_SERVICE_URL,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -92,25 +92,7 @@ function usePipelineLiveStatus(): PipelineLiveStatus | null {
|
|||||||
const [status, setStatus] = useState<PipelineLiveStatus | null>(null)
|
const [status, setStatus] = useState<PipelineLiveStatus | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Optional: Fetch live status from API
|
// Live status fetching not yet implemented
|
||||||
// For now, return null and display static content
|
|
||||||
// Uncomment below to enable live status fetching
|
|
||||||
/*
|
|
||||||
const fetchStatus = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/infrastructure/woodpecker/status')
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setStatus(data)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch pipeline status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchStatus()
|
|
||||||
const interval = setInterval(fetchStatus, 30000) // Poll every 30s
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
*/
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return status
|
return status
|
||||||
@@ -246,7 +228,7 @@ export function DevOpsPipelineSidebar({
|
|||||||
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
||||||
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
||||||
{currentTool === 'ci-cd' && (
|
{currentTool === 'ci-cd' && (
|
||||||
<span>Verwalten Sie Woodpecker Pipelines und Deployments</span>
|
<span>Verwalten Sie Gitea Actions Pipelines und Deployments</span>
|
||||||
)}
|
)}
|
||||||
{currentTool === 'tests' && (
|
{currentTool === 'tests' && (
|
||||||
<span>Ueberwachen Sie 280+ Tests ueber alle Services</span>
|
<span>Ueberwachen Sie 280+ Tests ueber alle Services</span>
|
||||||
@@ -458,7 +440,7 @@ export function DevOpsPipelineSidebarResponsive({
|
|||||||
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
||||||
{currentTool === 'ci-cd' && (
|
{currentTool === 'ci-cd' && (
|
||||||
<>
|
<>
|
||||||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Woodpecker Pipelines und Deployments verwalten
|
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Gitea Actions Pipelines und Deployments verwalten
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{currentTool === 'tests' && (
|
{currentTool === 'tests' && (
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const navigation: NavCategory[] = [
|
|||||||
id: 'ci-cd',
|
id: 'ci-cd',
|
||||||
name: 'CI/CD Dashboard',
|
name: 'CI/CD Dashboard',
|
||||||
href: '/infrastructure/ci-cd',
|
href: '/infrastructure/ci-cd',
|
||||||
description: 'Gitea & Woodpecker Pipelines',
|
description: 'Gitea Actions Pipelines',
|
||||||
purpose: 'CI/CD Dashboard mit Pipelines, Deployment-Status und Container-Management.',
|
purpose: 'CI/CD Dashboard mit Pipelines, Deployment-Status und Container-Management.',
|
||||||
audience: ['DevOps', 'Entwickler'],
|
audience: ['DevOps', 'Entwickler'],
|
||||||
subgroup: 'DevOps Pipeline',
|
subgroup: 'DevOps Pipeline',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Shared Types & Constants for Infrastructure/DevOps Modules
|
* Shared Types & Constants for Infrastructure/DevOps Modules
|
||||||
*
|
*
|
||||||
* Diese Datei enthaelt gemeinsame Typen und Konstanten fuer die DevOps-Pipeline:
|
* Diese Datei enthaelt gemeinsame Typen und Konstanten fuer die DevOps-Pipeline:
|
||||||
* - CI/CD: Woodpecker Pipelines & Deployments
|
* - CI/CD: Gitea Actions Pipelines & Deployments
|
||||||
* - Tests: Test Dashboard & Backlog
|
* - Tests: Test Dashboard & Backlog
|
||||||
* - SBOM: Software Bill of Materials & Lizenz-Checks
|
* - SBOM: Software Bill of Materials & Lizenz-Checks
|
||||||
* - Security: DevSecOps Scans & Vulnerabilities
|
* - Security: DevSecOps Scans & Vulnerabilities
|
||||||
@@ -230,24 +230,6 @@ export interface LogExtractionResponse {
|
|||||||
// Webhook Types
|
// Webhook Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Woodpecker Webhook Event Types
|
|
||||||
*/
|
|
||||||
export type WoodpeckerEventType = 'pipeline_success' | 'pipeline_failure' | 'pipeline_started'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Woodpecker Webhook Payload
|
|
||||||
*/
|
|
||||||
export interface WoodpeckerWebhookPayload {
|
|
||||||
event: WoodpeckerEventType
|
|
||||||
repo_id: number
|
|
||||||
pipeline_number: number
|
|
||||||
branch?: string
|
|
||||||
commit?: string
|
|
||||||
author?: string
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// LLM Integration Types
|
// LLM Integration Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -346,18 +328,14 @@ export interface PipelineLiveStatus {
|
|||||||
export const INFRASTRUCTURE_API_ENDPOINTS = {
|
export const INFRASTRUCTURE_API_ENDPOINTS = {
|
||||||
/** CI/CD Endpoints */
|
/** CI/CD Endpoints */
|
||||||
CI_CD: {
|
CI_CD: {
|
||||||
PIPELINES: '/api/admin/infrastructure/woodpecker',
|
PIPELINES: '/api/v1/security/sbom/pipeline/history',
|
||||||
TRIGGER: '/api/admin/infrastructure/woodpecker/trigger',
|
STATUS: '/api/v1/security/sbom/pipeline/status',
|
||||||
LOGS: '/api/admin/infrastructure/woodpecker/logs',
|
TRIGGER: '/api/v1/security/sbom/pipeline/trigger',
|
||||||
},
|
},
|
||||||
/** Log Extraction Endpoints */
|
/** Log Extraction Endpoints */
|
||||||
LOG_EXTRACT: {
|
LOG_EXTRACT: {
|
||||||
EXTRACT: '/api/infrastructure/log-extract/extract',
|
EXTRACT: '/api/infrastructure/log-extract/extract',
|
||||||
},
|
},
|
||||||
/** Webhook Endpoints */
|
|
||||||
WEBHOOKS: {
|
|
||||||
WOODPECKER: '/api/webhooks/woodpecker',
|
|
||||||
},
|
|
||||||
/** LLM Endpoints */
|
/** LLM Endpoints */
|
||||||
LLM: {
|
LLM: {
|
||||||
ANALYZE: '/api/ai/analyze',
|
ANALYZE: '/api/ai/analyze',
|
||||||
@@ -375,7 +353,6 @@ export const INFRASTRUCTURE_API_ENDPOINTS = {
|
|||||||
*/
|
*/
|
||||||
export const DEVOPS_ARCHITECTURE = {
|
export const DEVOPS_ARCHITECTURE = {
|
||||||
services: [
|
services: [
|
||||||
{ name: 'Woodpecker CI', port: 8000, description: 'CI/CD Pipeline Server' },
|
|
||||||
{ name: 'Gitea', port: 3003, description: 'Git Repository Server' },
|
{ name: 'Gitea', port: 3003, description: 'Git Repository Server' },
|
||||||
{ name: 'Syft', type: 'CLI', description: 'SBOM Generator' },
|
{ name: 'Syft', type: 'CLI', description: 'SBOM Generator' },
|
||||||
{ name: 'Grype', type: 'CLI', description: 'Vulnerability Scanner' },
|
{ name: 'Grype', type: 'CLI', description: 'Vulnerability Scanner' },
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ from email_template_api import (
|
|||||||
)
|
)
|
||||||
from system_api import router as system_router
|
from system_api import router as system_router
|
||||||
from security_api import router as security_router
|
from security_api import router as security_router
|
||||||
from woodpecker_proxy_api import router as woodpecker_router
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Middleware imports
|
# Middleware imports
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -106,7 +104,6 @@ app.include_router(system_router) # already has paths defined in r
|
|||||||
|
|
||||||
# Security / DevSecOps dashboard
|
# Security / DevSecOps dashboard
|
||||||
app.include_router(security_router, prefix="/api")
|
app.include_router(security_router, prefix="/api")
|
||||||
app.include_router(woodpecker_router, prefix="/api")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Startup / Shutdown events
|
# Startup / Shutdown events
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
"""
|
|
||||||
Woodpecker CI Proxy API
|
|
||||||
|
|
||||||
Liest Pipeline-Daten direkt aus der Woodpecker SQLite-Datenbank.
|
|
||||||
Wird als Fallback verwendet, wenn kein WOODPECKER_TOKEN konfiguriert ist.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Query
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/v1/woodpecker", tags=["Woodpecker CI"])
|
|
||||||
|
|
||||||
WOODPECKER_DB = Path("/woodpecker-data/woodpecker.sqlite")
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
|
||||||
if not WOODPECKER_DB.exists():
|
|
||||||
return None
|
|
||||||
conn = sqlite3.connect(f"file:{WOODPECKER_DB}?mode=ro", uri=True)
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
return conn
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
|
||||||
async def get_status():
|
|
||||||
conn = get_db()
|
|
||||||
if not conn:
|
|
||||||
return {"status": "offline", "error": "Woodpecker DB nicht gefunden"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
repos = [dict(r) for r in conn.execute(
|
|
||||||
"SELECT id, name, full_name, active FROM repos ORDER BY id"
|
|
||||||
).fetchall()]
|
|
||||||
|
|
||||||
total_pipelines = conn.execute("SELECT COUNT(*) FROM pipelines").fetchone()[0]
|
|
||||||
success = conn.execute("SELECT COUNT(*) FROM pipelines WHERE status='success'").fetchone()[0]
|
|
||||||
failure = conn.execute("SELECT COUNT(*) FROM pipelines WHERE status='failure'").fetchone()[0]
|
|
||||||
|
|
||||||
latest = conn.execute("SELECT MAX(created) FROM pipelines").fetchone()[0]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "online",
|
|
||||||
"repos": repos,
|
|
||||||
"stats": {
|
|
||||||
"total_pipelines": total_pipelines,
|
|
||||||
"success": success,
|
|
||||||
"failure": failure,
|
|
||||||
"success_rate": round(success / total_pipelines * 100, 1) if total_pipelines > 0 else 0,
|
|
||||||
},
|
|
||||||
"last_activity": datetime.fromtimestamp(latest).isoformat() if latest else None,
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pipelines")
|
|
||||||
async def get_pipelines(
|
|
||||||
repo: int = Query(default=0, description="Repo ID (0 = alle)"),
|
|
||||||
limit: int = Query(default=10, ge=1, le=100),
|
|
||||||
):
|
|
||||||
conn = get_db()
|
|
||||||
if not conn:
|
|
||||||
return {"status": "offline", "pipelines": [], "lastUpdate": datetime.now().isoformat()}
|
|
||||||
|
|
||||||
try:
|
|
||||||
base_sql = """SELECT p.id, p.repo_id, p.number, p.status, p.event, p.branch,
|
|
||||||
p."commit", p.message, p.author, p.created, p.started, p.finished,
|
|
||||||
r.name as repo_name
|
|
||||||
FROM pipelines p
|
|
||||||
JOIN repos r ON r.id = p.repo_id"""
|
|
||||||
|
|
||||||
if repo > 0:
|
|
||||||
rows = conn.execute(
|
|
||||||
base_sql + " WHERE p.repo_id = ? ORDER BY p.id DESC LIMIT ?",
|
|
||||||
(repo, limit)
|
|
||||||
).fetchall()
|
|
||||||
else:
|
|
||||||
rows = conn.execute(
|
|
||||||
base_sql + " ORDER BY p.id DESC LIMIT ?",
|
|
||||||
(limit,)
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
pipelines = []
|
|
||||||
for r in rows:
|
|
||||||
p = dict(r)
|
|
||||||
|
|
||||||
# Get steps directly (steps.pipeline_id links to pipelines.id)
|
|
||||||
steps = [dict(s) for s in conn.execute(
|
|
||||||
"""SELECT s.name, s.state, s.exit_code, s.error
|
|
||||||
FROM steps s
|
|
||||||
WHERE s.pipeline_id = ?
|
|
||||||
ORDER BY s.pid""",
|
|
||||||
(p["id"],)
|
|
||||||
).fetchall()]
|
|
||||||
|
|
||||||
p["steps"] = steps
|
|
||||||
p["commit"] = (p.get("commit") or "")[:7]
|
|
||||||
msg = p.get("message") or ""
|
|
||||||
p["message"] = msg.split("\n")[0][:100]
|
|
||||||
pipelines.append(p)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "online",
|
|
||||||
"pipelines": pipelines,
|
|
||||||
"lastUpdate": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/repos")
|
|
||||||
async def get_repos():
|
|
||||||
conn = get_db()
|
|
||||||
if not conn:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
repos = []
|
|
||||||
for r in conn.execute("SELECT id, name, full_name, active FROM repos ORDER BY id").fetchall():
|
|
||||||
repo = dict(r)
|
|
||||||
latest = conn.execute(
|
|
||||||
'SELECT status, created FROM pipelines WHERE repo_id = ? ORDER BY id DESC LIMIT 1',
|
|
||||||
(repo["id"],)
|
|
||||||
).fetchone()
|
|
||||||
if latest:
|
|
||||||
repo["last_status"] = latest["status"]
|
|
||||||
repo["last_activity"] = datetime.fromtimestamp(latest["created"]).isoformat()
|
|
||||||
repos.append(repo)
|
|
||||||
return repos
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
@@ -23,7 +23,6 @@ volumes:
|
|||||||
gitea_data:
|
gitea_data:
|
||||||
gitea_config:
|
gitea_config:
|
||||||
gitea_runner_data:
|
gitea_runner_data:
|
||||||
woodpecker_data:
|
|
||||||
# ERP
|
# ERP
|
||||||
erpnext_db_data:
|
erpnext_db_data:
|
||||||
erpnext_redis_queue_data:
|
erpnext_redis_queue_data:
|
||||||
@@ -238,7 +237,6 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
volumes:
|
volumes:
|
||||||
- woodpecker_data:/woodpecker-data:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}?options=-csearch_path%3Dcore,public
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}?options=-csearch_path%3Dcore,public
|
||||||
@@ -505,56 +503,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- breakpilot-network
|
- breakpilot-network
|
||||||
|
|
||||||
woodpecker-server:
|
|
||||||
image: woodpeckerci/woodpecker-server:v3
|
|
||||||
container_name: bp-core-woodpecker-server
|
|
||||||
ports:
|
|
||||||
- "8090:8000"
|
|
||||||
volumes:
|
|
||||||
- woodpecker_data:/var/lib/woodpecker
|
|
||||||
environment:
|
|
||||||
WOODPECKER_OPEN: "true"
|
|
||||||
WOODPECKER_HOST: ${WOODPECKER_HOST:-http://macmini:8090}
|
|
||||||
WOODPECKER_ADMIN: ${WOODPECKER_ADMIN:-pilotadmin}
|
|
||||||
WOODPECKER_GITEA: "true"
|
|
||||||
WOODPECKER_GITEA_URL: http://macmini:3003
|
|
||||||
WOODPECKER_GITEA_CLIENT: ${WOODPECKER_GITEA_CLIENT:-}
|
|
||||||
WOODPECKER_GITEA_SECRET: ${WOODPECKER_GITEA_SECRET:-}
|
|
||||||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-woodpecker-secret}
|
|
||||||
WOODPECKER_DATABASE_DRIVER: sqlite3
|
|
||||||
WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite
|
|
||||||
WOODPECKER_LOG_LEVEL: warn
|
|
||||||
WOODPECKER_PLUGINS_PRIVILEGED: "plugins/docker"
|
|
||||||
WOODPECKER_PLUGINS_TRUSTED_CLONE: "true"
|
|
||||||
extra_hosts:
|
|
||||||
- "macmini:192.168.178.100"
|
|
||||||
depends_on:
|
|
||||||
gitea:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- breakpilot-network
|
|
||||||
|
|
||||||
woodpecker-agent:
|
|
||||||
image: woodpeckerci/woodpecker-agent:v3
|
|
||||||
container_name: bp-core-woodpecker-agent
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
WOODPECKER_SERVER: woodpecker-server:9000
|
|
||||||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-woodpecker-secret}
|
|
||||||
WOODPECKER_MAX_WORKFLOWS: "2"
|
|
||||||
WOODPECKER_LOG_LEVEL: warn
|
|
||||||
WOODPECKER_BACKEND: docker
|
|
||||||
DOCKER_HOST: unix:///var/run/docker.sock
|
|
||||||
WOODPECKER_BACKEND_DOCKER_EXTRA_HOSTS: "macmini:192.168.178.100"
|
|
||||||
WOODPECKER_BACKEND_DOCKER_NETWORK: breakpilot-network
|
|
||||||
depends_on:
|
|
||||||
- woodpecker-server
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- breakpilot-network
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# DOCUMENTATION & UTILITIES
|
# DOCUMENTATION & UTILITIES
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@@ -632,8 +580,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
BACKEND_URL: http://backend-core:8000
|
BACKEND_URL: http://backend-core:8000
|
||||||
WOODPECKER_URL: http://bp-core-woodpecker-server:8000
|
|
||||||
WOODPECKER_TOKEN: ${WOODPECKER_TOKEN:-}
|
|
||||||
OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434}
|
OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434}
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|||||||
Reference in New Issue
Block a user