2026-07-29 13:05:14 +02:00
|
|
|
#!/bin/bash
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# CI/CD Pipeline for LeoCRM — Quality Gates
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# This script runs all quality checks before a deployment is allowed.
|
|
|
|
|
# It should be run in CI (GitHub Actions, Forgejo Actions, etc.) or locally.
|
|
|
|
|
#
|
|
|
|
|
# Exit codes:
|
|
|
|
|
# 0 = all checks passed
|
|
|
|
|
# 1 = one or more checks failed
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-08-12 20:47:43 +02:00
|
|
|
set -eo pipefail
|
2026-07-29 13:05:14 +02:00
|
|
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
|
|
|
|
|
check() {
|
|
|
|
|
local name="$1"
|
|
|
|
|
local cmd="$2"
|
|
|
|
|
echo -e "${YELLOW}[CI] Running: ${name}${NC}"
|
2026-08-12 20:47:43 +02:00
|
|
|
if eval "$cmd" > /tmp/ci_check_output 2>&1; then
|
|
|
|
|
tail -5 /tmp/ci_check_output
|
2026-07-29 13:05:14 +02:00
|
|
|
echo -e "${GREEN}[CI] PASS: ${name}${NC}"
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
2026-08-12 20:47:43 +02:00
|
|
|
tail -5 /tmp/ci_check_output
|
2026-07-29 13:05:14 +02:00
|
|
|
echo -e "${RED}[CI] FAIL: ${name}${NC}"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── 1. Python Compile Check ──────────────────────────────────────────────────
|
|
|
|
|
check "Python Compile" "python3 -m compileall app/ alembic/ -q"
|
|
|
|
|
|
|
|
|
|
# ── 2. Cross-Plugin Import Check ─────────────────────────────────────────────
|
|
|
|
|
check "Cross-Plugin Imports" "python3 scripts/check_cross_plugin_imports.py"
|
|
|
|
|
|
|
|
|
|
# ── 3. Alembic Revision Graph ────────────────────────────────────────────────
|
|
|
|
|
check "Alembic Revisions" "alembic heads 2>&1 | grep -c 'head' | grep -q '^1$'"
|
|
|
|
|
|
2026-07-29 23:17:03 +02:00
|
|
|
# ── 3b. Alembic Migration Test (only if DATABASE_URL is set) ──────────────────
|
|
|
|
|
if [ -n "${DATABASE_URL:-}" ]; then
|
|
|
|
|
check "Alembic Migration Test" "bash scripts/test_migrations.sh ${DATABASE_URL}"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: Alembic Migration Test (no DATABASE_URL)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-03 15:49:03 +02:00
|
|
|
# ── 3c. Migration Hash Check (0092 and earlier must not change) ────────────────
|
2026-08-12 20:47:43 +02:00
|
|
|
check "Migration Hash Check (<=0092)" "python3 scripts/check_migration_hashes.py"
|
2026-08-03 15:49:03 +02:00
|
|
|
|
2026-07-29 13:05:14 +02:00
|
|
|
# ── 4. TypeScript Type Check ─────────────────────────────────────────────────
|
|
|
|
|
check "TypeScript Type Check" "cd frontend && npx tsc --noEmit"
|
|
|
|
|
|
|
|
|
|
# ── 5. Frontend Build ────────────────────────────────────────────────────────
|
|
|
|
|
check "Frontend Build" "cd frontend && npm run build"
|
|
|
|
|
|
|
|
|
|
# ── 6. Python Tests (if collectable) ─────────────────────────────────────────
|
|
|
|
|
check "Test Collection" "python3 -m pytest --collect-only -q tests/ 2>&1 | tail -3"
|
|
|
|
|
|
2026-08-03 15:49:03 +02:00
|
|
|
# ── 6b. Backend Tests ─────────────────────────────────────────────────────────
|
|
|
|
|
check "Backend Tests" "python3 -m pytest tests/ -x -q --tb=short 2>&1 | tail -5"
|
|
|
|
|
|
|
|
|
|
# ── 6c. Frontend Tests ────────────────────────────────────────────────────────
|
|
|
|
|
check "Frontend Tests" "cd frontend && npx vitest run --reporter=verbose 2>&1 | tail -5"
|
|
|
|
|
|
2026-07-29 13:05:14 +02:00
|
|
|
# ── 7. Security: SQL Injection Check ─────────────────────────────────────────
|
|
|
|
|
check "SQL Injection Check" "! grep -rn 'text(f"SELECT.*{' app/services/ --include='*.py' >/dev/null 2>&1"
|
|
|
|
|
|
|
|
|
|
# ── 8. Security: Jinja2 Sandbox Check ─────────────────────────────────────────
|
|
|
|
|
check "Jinja2 Sandbox Check" "! grep -rn 'Environment(' app/plugins/builtins/report_generator/ --include='*.py' | grep -v Sandboxed >/dev/null 2>&1"
|
|
|
|
|
|
|
|
|
|
# ── 9. Security: RLS Variable Check ──────────────────────────────────────────
|
|
|
|
|
check "RLS Variable Check" "grep -q 'app.current_tenant_id' app/core/db/__init__.py"
|
|
|
|
|
|
|
|
|
|
# ── 10. Security: Fail-Closed Plugin Gate ────────────────────────────────────
|
|
|
|
|
check "Fail-Closed Plugin Gate" "! grep -A2 'except Exception:' app/deps.py | grep -q 'pass$' >/dev/null 2>&1"
|
|
|
|
|
|
2026-07-29 23:20:03 +02:00
|
|
|
# ── 11. Ruff Linter (if installed) ────────────────────────────────────────────
|
|
|
|
|
if command -v ruff &>/dev/null; then
|
|
|
|
|
check "Ruff Linter" "ruff check app/ --select E,F,W,I --ignore E501"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: Ruff Linter (not installed)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 12. Cross-Tenant Security Test (if pytest + DB available) ─────────────────
|
|
|
|
|
if [ -n "${DATABASE_URL:-}" ] && [ -f tests/test_cross_tenant_standalone.py ]; then
|
|
|
|
|
check "Cross-Tenant Security Test" "python3 -m pytest tests/test_cross_tenant_standalone.py -v --tb=short -p no:cacheprovider 2>&1 | tail -5"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: Cross-Tenant Security Test (no DATABASE_URL or test file)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 13. Dependency Scan (if pip-audit installed) ─────────────────────────────
|
|
|
|
|
if command -v pip-audit &>/dev/null; then
|
|
|
|
|
check "Dependency Scan (pip-audit)" "pip-audit -r requirements.txt --strict 2>&1 | tail -5"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: Dependency Scan (pip-audit not installed)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 14. Container Smoke Test (if Docker available) ───────────────────────────
|
|
|
|
|
if command -v docker &>/dev/null && [ -f Dockerfile ]; then
|
|
|
|
|
check "Container Smoke Test" "curl -sk https://crm.media-on.de/api/v1/health 2>&1 | grep -q 'healthy'"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: Container Smoke Test (no Docker or no Dockerfile)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 15. npm ci strict mode (no fallback to npm install) ───────────────────────
|
|
|
|
|
if [ -f frontend/package-lock.json ]; then
|
2026-08-03 15:49:03 +02:00
|
|
|
check "npm ci (strict)" "cd frontend && npm ci --legacy-peer-deps --prefer-offline 2>&1 | tail -3"
|
2026-07-29 23:20:03 +02:00
|
|
|
else
|
|
|
|
|
echo -e "${YELLOW}[CI] SKIP: npm ci (no package-lock.json)${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-29 13:05:14 +02:00
|
|
|
# ── Summary ──────────────────────────────────────────────────────────────────
|
|
|
|
|
echo ""
|
|
|
|
|
echo "============================================================"
|
|
|
|
|
echo " CI/CD Summary: ${PASS} passed, ${FAIL} failed"
|
|
|
|
|
echo "============================================================"
|
|
|
|
|
|
|
|
|
|
if [ $FAIL -gt 0 ]; then
|
|
|
|
|
echo -e "${RED}[CI] BUILD FAILED — ${FAIL} checks failed${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
else
|
|
|
|
|
echo -e "${GREEN}[CI] BUILD PASSED — all ${PASS} checks passed${NC}"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|