diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 478b1af..b091871 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,45 +1,41 @@ -# Gitea Actions CI/CD Pipeline -# BreakPilot Compliance +# BreakPilot Compliance — CI Pipeline # -# Services: -# Go: ai-compliance-sdk -# Python: backend-compliance, document-crawler, dsms-gateway -# Node.js: admin-compliance, developer-portal +# Feature branch workflow: +# feat/* | feature/* | fix/* | hotfix/* | chore/* | refactor/* | docs/* | test/* | ci/* +# → open PR targeting main +# → all jobs run as PR gates +# → squash merge to main +# → subset of jobs re-run on main to catch merge surprises # -# Workflow: -# Push auf main → Tests → Deploy (Orca) -# Pull Request → Lint + Tests (kein Deploy) +# Deploy is handled by build-push-deploy.yml on push to main. -name: CI/CD +name: CI on: push: - branches: [main, develop] + branches: [main] pull_request: - branches: [main, develop] + branches: [main] jobs: - # ======================================== - # Guardrails — LOC budget + architecture gates - # Runs on every push/PR. Fails fast and cheap. - # ======================================== - loc-budget: + # ── Branch naming convention (PR only) ────────────────────────────────── + branch-name: runs-on: docker container: alpine:3.20 + if: github.event_name == 'pull_request' steps: - - name: Checkout + - name: Validate branch name run: | - apk add --no-cache git bash - git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - - name: Enforce 500-line hard cap (whole repo) - run: | - chmod +x scripts/check-loc.sh - scripts/check-loc.sh - # Phase 5: whole-repo blocking gate. Phases 1-4 have drained the legacy - # baseline; any remaining oversized files must be listed in - # .claude/rules/loc-exceptions.txt with a written rationale. + BRANCH="${GITHUB_HEAD_REF}" + if ! echo "$BRANCH" | grep -qE '^(feat|feature|fix|hotfix|chore|refactor|docs|test|ci)/.+'; then + echo "::error::Branch '$BRANCH' does not follow naming convention." + echo "Required prefix: feat/ feature/ fix/ hotfix/ chore/ refactor/ docs/ test/ ci/" + exit 1 + fi + echo "Branch name OK: $BRANCH" + # ── Guardrail integrity (PR only) ──────────────────────────────────────── guardrail-integrity: runs-on: docker container: alpine:3.20 @@ -50,20 +46,47 @@ jobs: apk add --no-cache git bash git clone --depth 20 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . git fetch origin ${GITHUB_BASE_REF}:base - - name: Require [guardrail-change] label in PR commits touching guardrails + - name: Require [guardrail-change] in commits touching guardrails run: | changed=$(git diff --name-only base...HEAD) - echo "$changed" | grep -E '^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' || exit 0 + echo "$changed" | grep -qE '^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' || exit 0 if ! git log base..HEAD --format=%B | grep -q '\[guardrail-change\]'; then - echo "::error:: Guardrail files were modified but no commit in this PR carries [guardrail-change]." - echo "If intentional, amend one commit message with [guardrail-change] and explain why in the body." + echo "::error::Guardrail files modified without [guardrail-change] in any commit message." exit 1 fi - # ======================================== - # Lint (nur bei PRs) - # ======================================== + # ── LOC budget (always) ────────────────────────────────────────────────── + loc-budget: + runs-on: docker + container: alpine:3.20 + steps: + - name: Checkout + run: | + apk add --no-cache git bash + git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Enforce 500-line hard cap + run: | + chmod +x scripts/check-loc.sh + scripts/check-loc.sh + # ── Secret scanning (PR only) ──────────────────────────────────────────── + secret-scan: + runs-on: docker + container: zricethezav/gitleaks:v8.21.2 + if: github.event_name == 'pull_request' + steps: + - name: Checkout + run: | + apk add --no-cache git + git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Scan for secrets + run: | + gitleaks detect --source . --no-git \ + --exit-code 1 \ + --redact \ + || { echo "::error::Secrets detected — remove them before merging."; exit 1; } + + # ── Go lint + build (PR only) ──────────────────────────────────────────── go-lint: runs-on: docker if: github.event_name == 'pull_request' @@ -75,10 +98,16 @@ jobs: git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Lint ai-compliance-sdk run: | - if [ -d "ai-compliance-sdk" ]; then - cd ai-compliance-sdk && golangci-lint run --timeout 5m ./... - fi + [ -d "ai-compliance-sdk" ] || exit 0 + cd ai-compliance-sdk + golangci-lint run --timeout 5m ./... + - name: Build ai-compliance-sdk + run: | + [ -d "ai-compliance-sdk" ] || exit 0 + cd ai-compliance-sdk + go build ./... + # ── Python lint + import check (PR only) ──────────────────────────────── python-lint: runs-on: docker if: github.event_name == 'pull_request' @@ -88,29 +117,27 @@ jobs: run: | apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1 git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - - name: Lint Python services (ruff) + - name: Lint (ruff) + type-check (mypy) run: | - pip install --quiet ruff + pip install --quiet ruff mypy fail=0 for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do - if [ -d "$svc" ]; then - echo "=== ruff: $svc ===" - ruff check "$svc/" --output-format=github || fail=1 - fi + [ -d "$svc" ] || continue + echo "=== ruff: $svc ===" && ruff check "$svc/" --output-format=github || fail=1 done - exit $fail - - name: Type-check (mypy via backend-compliance/mypy.ini) - # Policy is declared in backend-compliance/mypy.ini: strict mode globally, - # with per-module overrides for legacy utility services, the SQLAlchemy - # ORM layer, and yet-unrefactored route files. Each Phase 1 Step 4 - # refactor flips a route file from loose->strict via its own mypy.ini - # override block. - run: | - pip install --quiet mypy if [ -f "backend-compliance/mypy.ini" ]; then - cd backend-compliance && mypy compliance/ + cd backend-compliance && mypy compliance/ || fail=1 fi + exit $fail + - name: Import sanity check (catches NameError at collection time) + run: | + cd backend-compliance + pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true + export PYTHONPATH="$(pwd):${PYTHONPATH:-}" + python -c "import compliance; print('Import OK')" \ + || { echo "::error::compliance package fails to import — missing import or syntax error."; exit 1; } + # ── Node.js lint + type-check (PR only) ───────────────────────────────── nodejs-lint: runs-on: docker if: github.event_name == 'pull_request' @@ -120,26 +147,105 @@ jobs: run: | apk add --no-cache git git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - - name: Lint + type-check Node.js services + - name: Lint + type-check run: | fail=0 for svc in admin-compliance developer-portal; do - if [ -d "$svc" ]; then - echo "=== $svc: install ===" - (cd "$svc" && (npm ci --silent 2>/dev/null || npm install --silent)) - echo "=== $svc: next lint ===" - (cd "$svc" && npx next lint) || fail=1 - echo "=== $svc: tsc --noEmit ===" - (cd "$svc" && npx tsc --noEmit) || fail=1 - fi + [ -d "$svc" ] || continue + echo "=== $svc: install ===" && (cd "$svc" && npm ci --silent 2>/dev/null || npm install --silent) + echo "=== $svc: next lint ===" && (cd "$svc" && npx next lint) || fail=1 + echo "=== $svc: tsc ===" && (cd "$svc" && npx tsc --noEmit) || fail=1 done exit $fail - # ======================================== - # Unit Tests - # ======================================== + # ── Node.js build — next build (PR + push to main) ─────────────────────── + nodejs-build: + runs-on: docker + container: node:20-alpine + steps: + - name: Checkout + run: | + apk add --no-cache git + git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Build Next.js services + run: | + fail=0 + for svc in admin-compliance developer-portal; do + [ -d "$svc" ] || continue + echo "=== $svc: install ===" + (cd "$svc" && npm ci --silent 2>/dev/null || npm install --silent) + echo "=== $svc: next build ===" + (cd "$svc" && \ + NEXT_PUBLIC_API_URL=https://api-dev.breakpilot.ai \ + NEXT_PUBLIC_SDK_URL=https://sdk-dev.breakpilot.ai \ + npm run build) || fail=1 + done + exit $fail - test-go-ai-compliance: + # ── Dependency audit (PR only) ─────────────────────────────────────────── + dep-audit: + runs-on: docker + if: github.event_name == 'pull_request' + container: python:3.12-slim + steps: + - name: Checkout + run: | + apt-get update -qq && apt-get install -y -qq git curl > /dev/null 2>&1 + git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Install Node.js + Go + run: | + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1 + apt-get install -y nodejs golang-go > /dev/null 2>&1 + - name: Python — pip-audit + run: | + pip install --quiet pip-audit + fail=0 + for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do + [ -f "$svc/requirements.txt" ] || continue + echo "=== pip-audit: $svc ===" + pip-audit -r "$svc/requirements.txt" --skip-editable -f columns || fail=1 + done + exit $fail + - name: Node.js — npm audit + run: | + fail=0 + for svc in admin-compliance developer-portal; do + [ -d "$svc" ] || continue + echo "=== npm audit: $svc ===" + (cd "$svc" && npm audit --audit-level=moderate --json 2>/dev/null | \ + node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); \ + const hi=Object.values(d.vulnerabilities||{}).filter(v=>['high','critical'].includes(v.severity)).length; \ + if(hi>0){console.error('HIGH/CRITICAL: '+hi);process.exit(1)}") || fail=1 + done + exit $fail + - name: Go — govulncheck + run: | + [ -d "ai-compliance-sdk" ] || exit 0 + go install golang.org/x/vuln/cmd/govulncheck@latest 2>/dev/null + cd ai-compliance-sdk && govulncheck ./... || true + # Non-blocking until Go module versions are pinned + + # ── SBOM + vulnerability scan (PR only) ───────────────────────────────── + sbom-scan: + runs-on: docker + if: github.event_name == 'pull_request' + container: alpine:3.20 + steps: + - name: Checkout + run: | + apk add --no-cache git curl bash + git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Install syft + grype + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin + - name: Generate SBOM + run: mkdir -p sbom-out && syft dir:. -o cyclonedx-json=sbom-out/sbom.cdx.json -q + - name: Vulnerability scan (fail on high+) + run: grype sbom:sbom-out/sbom.cdx.json --fail-on high -q + + # ── Tests (PR + push to main) ───────────────────────────────────────────── + test-go: runs-on: docker container: golang:1.24-alpine env: @@ -151,16 +257,12 @@ jobs: git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Test ai-compliance-sdk run: | - if [ ! -d "ai-compliance-sdk" ]; then - echo "WARNUNG: ai-compliance-sdk nicht gefunden" - exit 0 - fi + [ -d "ai-compliance-sdk" ] || exit 0 cd ai-compliance-sdk - go test -v -coverprofile=coverage.out ./... 2>&1 - COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' || echo "0%") - echo "Coverage: $COVERAGE" + go test -v -coverprofile=coverage.out ./... + go tool cover -func=coverage.out | tail -1 - test-python-backend-compliance: + test-python-backend: runs-on: docker container: python:3.12-slim env: @@ -172,14 +274,11 @@ jobs: git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Test backend-compliance run: | - if [ ! -d "backend-compliance" ]; then - echo "WARNUNG: backend-compliance nicht gefunden" - exit 0 - fi + [ -d "backend-compliance" ] || exit 0 cd backend-compliance 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 pytest pytest-asyncio + pip install --quiet --no-cache-dir pytest pytest-asyncio python -m pytest compliance/tests/ -v --tb=short test-python-document-crawler: @@ -194,10 +293,7 @@ jobs: git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Test document-crawler run: | - if [ ! -d "document-crawler" ]; then - echo "WARNUNG: document-crawler nicht gefunden" - exit 0 - fi + [ -d "document-crawler" ] || exit 0 cd document-crawler export PYTHONPATH="$(pwd):${PYTHONPATH:-}" pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true @@ -216,46 +312,14 @@ jobs: git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Test dsms-gateway run: | - if [ ! -d "dsms-gateway" ]; then - echo "WARNUNG: dsms-gateway nicht gefunden" - exit 0 - fi + [ -d "dsms-gateway" ] || exit 0 cd dsms-gateway export PYTHONPATH="$(pwd):${PYTHONPATH:-}" pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true pip install --quiet --no-cache-dir pytest pytest-asyncio python -m pytest test_main.py -v --tb=short - # ======================================== - # SBOM + license scan (compliance product → we eat our own dog food) - # ======================================== - - sbom-scan: - runs-on: docker - if: github.event_name == 'pull_request' - container: alpine:3.20 - steps: - - name: Checkout - run: | - apk add --no-cache git curl bash - git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - - name: Install syft + grype - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin - - name: Generate SBOM - run: | - mkdir -p sbom-out - syft dir:. -o cyclonedx-json=sbom-out/sbom.cdx.json -q - - name: Vulnerability scan (fail on high+) - run: | - grype sbom:sbom-out/sbom.cdx.json --fail-on high -q - # Phase 5: blocking. Any high+ CVE in the dependency graph fails the PR. - - # ======================================== - # Validate Canonical Controls - # ======================================== - + # ── OpenAPI contract validation (always) ───────────────────────────────── validate-canonical-controls: runs-on: docker container: python:3.12-slim @@ -265,8 +329,4 @@ jobs: apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1 git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Validate controls - run: | - python scripts/validate-controls.py - - # Deploy is handled by .gitea/workflows/build-push-deploy.yml - # which builds images, pushes to registry.meghsakha.com, and triggers orca. + run: python scripts/validate-controls.py