name: CI Pipeline on: push: branches: [main, develop] tags: ['v*', '[0-9]+.*'] pull_request: branches: [main] permissions: contents: read packages: write concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: IMAGE_NAME: christianlouis/docuelevate jobs: # ══════════════════════════════════════════════════════════════════════════ # Stage 1: Static Analysis (The "Immediate" Gate) # ══════════════════════════════════════════════════════════════════════════ lint: name: Ruff Lint & Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' # Caching enabled - name: Install Ruff run: pip install ruff - name: Check for merge conflict markers run: | if git grep -rn -E '^(<{7} |>{7} |={7}$)' -- '.'; then echo "ERROR: Merge conflict markers found." exit 1 fi - run: ruff check app/ tests/ - run: ruff format --check app/ tests/ html-lint: name: HTML Accessibility Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' - run: pip install djlint>=1.36.0 - run: djlint frontend/templates/ --lint # ══════════════════════════════════════════════════════════════════════════ # Stage 2: Parallel Heavy Lifters (Tests & Mypy) # All 3 of these now run at the same time as soon as Linting passes. # ══════════════════════════════════════════════════════════════════════════ mypy: name: Mypy Type Check runs-on: ubuntu-latest needs: [lint] # Blocks only on fast linting steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' - name: Install Dependencies run: pip install -r requirements-dev.txt - run: mypy app/ test-quick: name: Quick Tests runs-on: ubuntu-latest needs: [lint] # Parallel with Mypy services: redis: image: redis:7 ports: ["6379:6379"] options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' - name: Install Dependencies run: pip install -r requirements-dev.txt - name: Run Quick Tests run: > pytest tests/ -v --timeout=120 --cov=app --cov-report=xml -m "not e2e and not requires_docker and not requires_external and not slow" - name: Upload coverage if: always() uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} test-integration: name: Integration Tests runs-on: ubuntu-latest needs: [lint] # NOW PARALLEL (No longer waits for test-quick) services: redis: image: redis:7 ports: ["6379:6379"] rabbitmq: image: rabbitmq:3-management ports: ["5672:5672", "15672:15672"] options: --health-cmd "rabbitmq-diagnostics -q ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' - name: Install Dependencies run: pip install -r requirements-dev.txt - name: Run Integration Tests run: > pytest tests/ -v --timeout=300 -m "(requires_docker or requires_external or slow) and not e2e" dependency-scan: name: Security Scan runs-on: ubuntu-latest needs: [lint] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: 'pip' - run: pip install pip-audit>=2.7.0 - run: pip-audit -r requirements.txt --desc on # ══════════════════════════════════════════════════════════════════════════ # Stage 3: Build & Deploy (Final Quality Gate) # ══════════════════════════════════════════════════════════════════════════ build: name: Build & Push runs-on: ubuntu-latest # This job only runs if EVERYTHING above passed needs: [test-quick, test-integration, mypy, dependency-scan, html-lint] if: github.event_name == 'push' steps: - uses: actions/checkout@v4 - name: Generate Build Metadata run: | chmod +x scripts/generate_build_metadata.sh ./scripts/generate_build_metadata.sh - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Registries uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and Push uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ env.IMAGE_NAME }}:latest cache-from: type=gha cache-to: type=gha,mode=max update-k8s-manifest: name: Update Preprod runs-on: ubuntu-latest needs: [build] if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - name: Checkout k8s-cluster-state uses: actions/checkout@v4 with: repository: christianlouis/k8s-cluster-state token: ${{ secrets.GH_PAT }} path: k8s-cluster-state - name: Update image tag uses: mikefarah/yq@v4.44.6 with: cmd: yq -i '.images[0].newTag = "${{ github.sha }}"' k8s-cluster-state/apps/docuelevate/preprod/docuelevate-stack.yaml - name: Push changes run: | cd k8s-cluster-state git config user.name "github-actions" git config user.email "actions@github.com" git add . git commit -m "chore: update image to ${{ github.sha }}" git push