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: Ruff Lint & Format (runs first to catch style issues early) # ══════════════════════════════════════════════════════════════════════════ lint: name: Ruff Lint & Format runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install Ruff run: pip install ruff - name: Run Ruff Check run: ruff check app/ tests/ - name: Run Ruff Format run: ruff format --check app/ tests/ # ══════════════════════════════════════════════════════════════════════════ # Stage 2: Tests & Type Checking (run in parallel after lint passes) # ══════════════════════════════════════════════════════════════════════════ test: name: Tests runs-on: ubuntu-latest needs: [lint] # Wait for lint to pass before running tests services: redis: image: redis:7 ports: - 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 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: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install Dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Run Tests run: pytest tests/ -v --cov=app --cov-report=xml --cov-report=term --junitxml=junit.xml -o junit_family=legacy -m "not e2e" - name: Upload coverage reports to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml fail_ci_if_error: false - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: test-results path: | junit.xml coverage.xml mypy: name: Mypy runs-on: ubuntu-latest needs: [lint] # Wait for lint to pass before running type checks steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install Dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Run Mypy run: mypy app/ # ══════════════════════════════════════════════════════════════════════════ # Stage 3: Build & Push Docker Image (only after all Stage 2 jobs pass) # ══════════════════════════════════════════════════════════════════════════ build: name: Build & Push Docker Image runs-on: ubuntu-latest needs: [test, lint, mypy] if: github.event_name == 'push' steps: - name: Checkout Code 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 Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for tags id: meta uses: docker/metadata-action@v5 with: images: | ${{ env.IMAGE_NAME }} ghcr.io/${{ github.repository_owner }}/docuelevate tags: | type=ref,event=branch type=sha,prefix={{branch}}- type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push Docker Image uses: docker/build-push-action@v6 with: context: . file: Dockerfile platforms: linux/amd64 push: true sbom: true provenance: mode=max tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # ══════════════════════════════════════════════════════════════════════════ # Stage 4: Deploy (only after build succeeds, only on main branch) # ══════════════════════════════════════════════════════════════════════════ deploy: name: Deploy to Production runs-on: ubuntu-latest needs: [build] if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - name: Call Deployment Webhook run: | curl -X POST https://docker2.kuechenserver.org/api/stacks/webhooks/960c7d8e-97ec-4175-a8dc-73f037b02349