# BreakPilot Compliance — CI Pipeline # # 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 # # Deploy is handled by build-push-deploy.yml on push to main. name: CI on: push: branches: [main] pull_request: branches: [main] jobs: # ── Branch naming convention (PR only) ────────────────────────────────── branch-name: runs-on: docker container: alpine:3.20 if: github.event_name == 'pull_request' steps: - name: Validate branch name run: | 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 if: github.event_name == 'pull_request' steps: - name: Checkout run: | 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] in commits touching guardrails run: | changed=$(git diff --name-only base...HEAD) 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 modified without [guardrail-change] in any commit message." exit 1 fi # ── 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' container: golangci/golangci-lint:v1.62-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: Lint ai-compliance-sdk run: | [ -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' container: python:3.12-slim steps: - name: Checkout 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 (ruff) + type-check (mypy) run: | pip install --quiet ruff mypy fail=0 for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do [ -d "$svc" ] || continue echo "=== ruff: $svc ===" && ruff check "$svc/" --output-format=github || fail=1 done if [ -f "backend-compliance/mypy.ini" ]; then 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' 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: Lint + type-check 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 lint ===" && (cd "$svc" && npx next lint) || fail=1 echo "=== $svc: tsc ===" && (cd "$svc" && npx tsc --noEmit) || fail=1 done exit $fail # ── 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 # ── 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: CGO_ENABLED: "0" steps: - name: Checkout run: | apk add --no-cache git git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - name: Test ai-compliance-sdk run: | [ -d "ai-compliance-sdk" ] || exit 0 cd ai-compliance-sdk go test -v -coverprofile=coverage.out ./... go tool cover -func=coverage.out | tail -1 test-python-backend: runs-on: docker container: python:3.12-slim env: CI: "true" steps: - name: Checkout 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: Test backend-compliance run: | [ -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 pytest pytest-asyncio python -m pytest compliance/tests/ -v --tb=short test-python-document-crawler: runs-on: docker container: python:3.12-slim env: CI: "true" steps: - name: Checkout 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: Test document-crawler run: | [ -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 pip install --quiet --no-cache-dir pytest pytest-asyncio python -m pytest tests/ -v --tb=short test-python-dsms-gateway: runs-on: docker container: python:3.12-slim env: CI: "true" steps: - name: Checkout 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: Test dsms-gateway run: | [ -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 # ── OpenAPI contract validation (always) ───────────────────────────────── validate-canonical-controls: runs-on: docker container: python:3.12-slim steps: - name: Checkout 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: Validate controls run: python scripts/validate-controls.py