# Syndarix CI/CD Pipeline # Gitea Actions workflow for continuous integration and deployment # # Pipeline Structure: # - lint: Fast feedback (linting and type checking) # - test: Run test suites (depends on lint) # - build: Build Docker images (depends on test) # - deploy: Deploy to production (depends on build, only on main) name: CI/CD Pipeline on: push: branches: - main - dev - 'feature/**' pull_request: branches: - main - dev env: PYTHON_VERSION: "3.12" NODE_VERSION: "20" UV_VERSION: "0.4.x" jobs: # =========================================================================== # LINT JOB - Fast feedback first # =========================================================================== lint: name: Lint & Type Check runs-on: ubuntu-latest strategy: matrix: component: [backend, frontend] steps: - name: Checkout code uses: actions/checkout@v4 # ----- Backend Linting ----- - name: Set up Python if: matrix.component == 'backend' uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv if: matrix.component == 'backend' uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} - name: Cache uv dependencies if: matrix.component == 'backend' uses: actions/cache@v4 with: path: | ~/.cache/uv backend/.venv key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} restore-keys: | uv-${{ runner.os }}- - name: Install backend dependencies if: matrix.component == 'backend' working-directory: backend run: uv sync --extra dev --frozen - name: Run ruff linting if: matrix.component == 'backend' working-directory: backend run: uv run ruff check app - name: Run ruff format check if: matrix.component == 'backend' working-directory: backend run: uv run ruff format --check app - name: Run mypy type checking if: matrix.component == 'backend' working-directory: backend run: uv run mypy app # ----- Frontend Linting ----- - name: Set up Node.js if: matrix.component == 'frontend' uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Cache npm dependencies if: matrix.component == 'frontend' uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - name: Install frontend dependencies if: matrix.component == 'frontend' working-directory: frontend run: npm ci - name: Run ESLint if: matrix.component == 'frontend' working-directory: frontend run: npm run lint - name: Run TypeScript type check if: matrix.component == 'frontend' working-directory: frontend run: npm run type-check - name: Run Prettier format check if: matrix.component == 'frontend' working-directory: frontend run: npm run format:check # =========================================================================== # TEST JOB - Run test suites # =========================================================================== test: name: Test runs-on: ubuntu-latest needs: lint strategy: matrix: component: [backend, frontend] steps: - name: Checkout code uses: actions/checkout@v4 # ----- Backend Tests ----- - name: Set up Python if: matrix.component == 'backend' uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv if: matrix.component == 'backend' uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} - name: Cache uv dependencies if: matrix.component == 'backend' uses: actions/cache@v4 with: path: | ~/.cache/uv backend/.venv key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} restore-keys: | uv-${{ runner.os }}- - name: Install backend dependencies if: matrix.component == 'backend' working-directory: backend run: uv sync --extra dev --frozen - name: Run pytest with coverage if: matrix.component == 'backend' working-directory: backend env: IS_TEST: "True" run: | uv run pytest --cov=app --cov-report=xml --cov-report=term-missing --cov-fail-under=90 - name: Upload backend coverage report if: matrix.component == 'backend' uses: actions/upload-artifact@v4 with: name: backend-coverage path: backend/coverage.xml retention-days: 7 # ----- Frontend Tests ----- - name: Set up Node.js if: matrix.component == 'frontend' uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Cache npm dependencies if: matrix.component == 'frontend' uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - name: Install frontend dependencies if: matrix.component == 'frontend' working-directory: frontend run: npm ci - name: Run Jest unit tests if: matrix.component == 'frontend' working-directory: frontend run: npm test -- --coverage --passWithNoTests - name: Upload frontend coverage report if: matrix.component == 'frontend' uses: actions/upload-artifact@v4 with: name: frontend-coverage path: frontend/coverage/ retention-days: 7 # =========================================================================== # BUILD JOB - Build Docker images # =========================================================================== build: name: Build runs-on: ubuntu-latest needs: test strategy: matrix: component: [backend, frontend] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Cache Docker layers uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: docker-${{ matrix.component }}-${{ github.sha }} restore-keys: | docker-${{ matrix.component }}- - name: Build backend Docker image if: matrix.component == 'backend' uses: docker/build-push-action@v5 with: context: ./backend file: ./backend/Dockerfile target: production push: false tags: syndarix-backend:${{ github.sha }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - name: Build frontend Docker image if: matrix.component == 'frontend' uses: docker/build-push-action@v5 with: context: ./frontend file: ./frontend/Dockerfile target: runner push: false tags: syndarix-frontend:${{ github.sha }} build-args: | NEXT_PUBLIC_API_URL=http://localhost:8000 cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max # Prevent cache from growing indefinitely - name: Move cache run: | rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache # =========================================================================== # DEPLOY JOB - Deploy to production (only on main branch) # =========================================================================== deploy: name: Deploy runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' && github.event_name == 'push' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Deploy notification run: | echo "Deployment to production would happen here" echo "Branch: ${{ github.ref }}" echo "Commit: ${{ github.sha }}" echo "Actor: ${{ github.actor }}" # TODO: Add actual deployment steps when infrastructure is ready # Options: # - SSH to production server and run docker-compose pull && docker-compose up -d # - Use Kubernetes deployment # - Use cloud provider deployment (AWS ECS, GCP Cloud Run, etc.) # - Trigger webhook to deployment orchestrator # =========================================================================== # SECURITY SCAN JOB - Run on main and dev branches # =========================================================================== security: name: Security Scan runs-on: ubuntu-latest needs: lint if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} - name: Install backend dependencies working-directory: backend run: uv sync --extra dev --frozen - name: Run Bandit security scan (via ruff) working-directory: backend run: | # Ruff includes flake8-bandit (S rules) for security scanning # Run with explicit security rules only uv run ruff check app --select=S --ignore=S101,S104,S105,S106,S603,S607 - name: Run pip-audit for dependency vulnerabilities working-directory: backend run: | # pip-audit checks for known vulnerabilities in Python dependencies uv run pip-audit --require-hashes --disable-pip -r <(uv pip compile pyproject.toml) || true # Note: Using || true temporarily while setting up proper remediation - name: Check for secrets in code run: | # Basic check for common secret patterns # In production, use tools like gitleaks or trufflehog echo "Checking for potential hardcoded secrets..." ! grep -rn --include="*.py" --include="*.ts" --include="*.tsx" --include="*.js" \ -E "(api_key|apikey|secret_key|secretkey|password|passwd|token)\s*=\s*['\"][^'\"]{8,}['\"]" \ backend/app frontend/src || echo "No obvious secrets found" - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install frontend dependencies working-directory: frontend run: npm ci - name: Run npm audit working-directory: frontend run: | npm audit --audit-level=high || true # Note: Using || true to not fail on moderate vulnerabilities # In production, consider stricter settings # =========================================================================== # E2E TEST JOB - Run end-to-end tests with Playwright # =========================================================================== e2e-tests: name: E2E Tests runs-on: ubuntu-latest needs: [lint, test] if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.event_name == 'pull_request' services: postgres: image: pgvector/pgvector:pg17 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: syndarix_test ports: - 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7-alpine ports: - 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v4 with: version: ${{ env.UV_VERSION }} - name: Install backend dependencies working-directory: backend run: uv sync --extra dev --frozen - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install frontend dependencies working-directory: frontend run: npm ci - name: Install Playwright browsers working-directory: frontend run: npx playwright install --with-deps chromium - name: Start backend server working-directory: backend env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/syndarix_test REDIS_URL: redis://localhost:6379/0 SECRET_KEY: test-secret-key-for-e2e-tests-only ENVIRONMENT: test IS_TEST: "True" run: | # Run migrations uv run python -c "from app.database import create_tables; import asyncio; asyncio.run(create_tables())" || true # Start backend in background uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 & # Wait for backend to be ready sleep 10 - name: Run Playwright E2E tests working-directory: frontend env: NEXT_PUBLIC_API_URL: http://localhost:8000 run: | npm run build npm run test:e2e -- --project=chromium - name: Upload Playwright report uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: frontend/playwright-report/ retention-days: 7