Refactor CI workflow for clarity and efficiency

Refactor CI workflow to simplify configuration and improve readability. Consolidate steps, update job dependencies, and enhance linting and testing stages.
This commit is contained in:
Christian Krakau-Louis
2026-03-02 10:54:05 +01:00
committed by GitHub
parent 3d38813da5
commit 7cb5407bcf
+73 -243
View File
@@ -2,15 +2,10 @@ name: CI Pipeline
on: on:
push: push:
branches: branches: [main, develop]
- main tags: ['v*', '[0-9]+.*']
- develop
tags:
- 'v*'
- '[0-9]+.*'
pull_request: pull_request:
branches: branches: [main]
- main
permissions: permissions:
contents: read contents: read
@@ -25,348 +20,183 @@ env:
jobs: jobs:
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
# Stage 1: Ruff Lint & Format (runs first to catch style issues early) # Stage 1: Static Analysis (The "Immediate" Gate)
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
lint: lint:
name: Ruff Lint & Format name: Ruff Lint & Format
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: 'pip' # Caching enabled
- name: Install Ruff - name: Install Ruff
run: pip install ruff run: pip install ruff
- name: Show Ruff version (debug)
run: ruff --version
- name: Check for merge conflict markers - name: Check for merge conflict markers
run: | run: |
if git grep -rn -E '^(<{7} |>{7} |={7}$)' -- '.'; then if git grep -rn -E '^(<{7} |>{7} |={7}$)' -- '.'; then
echo "ERROR: Merge conflict markers found in tracked files." echo "ERROR: Merge conflict markers found."
exit 1 exit 1
fi fi
- run: ruff check app/ tests/
- name: Run Ruff Lint (check) - run: ruff format --check app/ tests/
# ruff check can --fix locally, but CI should only check (no modifications)
run: ruff check app/ tests/
- name: Run Ruff Format check
# ruff format only supports --check; do not pass --fix here
run: ruff format --check app/ tests/
# ══════════════════════════════════════════════════════════════════════════
# Stage 1b: HTML Accessibility Lint (catches a11y regressions early)
# ══════════════════════════════════════════════════════════════════════════
html-lint: html-lint:
name: HTML Accessibility Lint name: HTML Accessibility Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: 'pip'
- name: Install djLint - run: pip install djlint>=1.36.0
run: pip install djlint>=1.36.0 - run: djlint frontend/templates/ --lint
- name: Lint HTML templates for accessibility
run: djlint frontend/templates/ --lint
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
# Stage 1: Mypy type-checking (runs in parallel with lint & html-lint) # Stage 2: Parallel Heavy Lifters (Tests & Mypy)
# All 3 of these now run at the same time as soon as Linting passes.
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
mypy: mypy:
name: Mypy name: Mypy Type Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
# No needs — runs immediately in Stage 1 alongside Ruff and HTML lint needs: [lint] # Blocks only on fast linting
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: 'pip'
- name: Install Dependencies - name: Install Dependencies
run: | run: pip install -r requirements-dev.txt
python -m pip install --upgrade pip - run: mypy app/
pip install -r requirements-dev.txt
- name: Run Mypy
run: mypy app/
# ══════════════════════════════════════════════════════════════════════════
# Stage 2: Dependency Vulnerability Scan (parallel background track —
# does NOT block tests; still gates build/deploy)
# ══════════════════════════════════════════════════════════════════════════
dependency-scan:
name: Dependency Vulnerability Scan
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 pip-audit
run: pip install pip-audit>=2.7.0
- name: Run pip-audit on production dependencies
run: pip-audit -r requirements.txt --desc on
- name: Run pip-audit on dev dependencies
run: pip-audit -r requirements-dev.txt --desc on
# ══════════════════════════════════════════════════════════════════════════
# Stage 3: Quick Tests (unit + basic integration — fast fail gate;
# starts as soon as Stage 1 static analysis passes)
# ══════════════════════════════════════════════════════════════════════════
test-quick: test-quick:
name: Quick Tests name: Quick Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 needs: [lint] # Parallel with Mypy
needs: [lint, html-lint, mypy]
services: services:
redis: redis:
image: redis:7 image: redis:7
ports: ports: ["6379:6379"]
- 6379:6379 options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: 'pip'
- name: Install Dependencies - name: Install Dependencies
run: | run: pip install -r requirements-dev.txt
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run Quick Tests - name: Run Quick Tests
run: > run: >
pytest tests/ -v pytest tests/ -v --timeout=120 --cov=app --cov-report=xml
--timeout=120
--cov=app --cov-report=xml --cov-report=term
--junitxml=junit.xml -o junit_family=legacy
-m "not e2e and not requires_docker and not requires_external and not slow" -m "not e2e and not requires_docker and not requires_external and not slow"
- name: Upload coverage
- name: Upload coverage reports to Codecov if: always()
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./junit.xml
report_type: test_results
fail_ci_if_error: false
- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: test-results-quick
path: |
junit.xml
coverage.xml
# ══════════════════════════════════════════════════════════════════════════
# Stage 4: Integration Tests (Docker containers, external services;
# only runs if Quick Tests pass)
# ══════════════════════════════════════════════════════════════════════════
test-integration: test-integration:
name: Integration Tests name: Integration Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 needs: [lint] # NOW PARALLEL (No longer waits for test-quick)
needs: [test-quick] # Only run after quick tests pass (fail early)
services: services:
redis: redis:
image: redis:7 image: redis:7
ports: ports: ["6379:6379"]
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
rabbitmq: rabbitmq:
image: rabbitmq:3-management image: rabbitmq:3-management
ports: ports: ["5672:5672", "15672:15672"]
- 5672:5672 options: --health-cmd "rabbitmq-diagnostics -q ping" --health-interval 10s --health-timeout 5s --health-retries 5
- 15672:15672
options: >-
--health-cmd "rabbitmq-diagnostics -q ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: 'pip'
- name: Install Dependencies - name: Install Dependencies
run: | run: pip install -r requirements-dev.txt
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run Integration Tests - name: Run Integration Tests
run: > run: >
pytest tests/ -v pytest tests/ -v --timeout=300
--timeout=300
--junitxml=junit-integration.xml -o junit_family=legacy
-m "(requires_docker or requires_external or slow) and not e2e" -m "(requires_docker or requires_external or slow) and not e2e"
- name: Upload integration test results dependency-scan:
if: ${{ !cancelled() }} name: Security Scan
uses: actions/upload-artifact@v4
with:
name: test-results-integration
path: junit-integration.xml
# ══════════════════════════════════════════════════════════════════════════
# Stage 5: Build & Push Docker Image (only on push to main/develop/tags;
# gates on ALL prior stages including dependency scan)
# ══════════════════════════════════════════════════════════════════════════
build:
name: Build & Push Docker Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test-quick, test-integration, lint, html-lint, mypy, dependency-scan] needs: [lint]
if: github.event_name == 'push'
steps: steps:
- name: Checkout Code - uses: actions/checkout@v4
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 - name: Generate Build Metadata
run: | run: |
chmod +x scripts/generate_build_metadata.sh chmod +x scripts/generate_build_metadata.sh
./scripts/generate_build_metadata.sh ./scripts/generate_build_metadata.sh
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to Registries
- 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 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
- 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 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile
platforms: linux/amd64
push: true push: true
sbom: true tags: ${{ env.IMAGE_NAME }}:latest
provenance: mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
# ══════════════════════════════════════════════════════════════════════════
# Stage 5: Update preprod K8s manifest (ArgoCD GitOps, only on main push)
# ══════════════════════════════════════════════════════════════════════════
update-k8s-manifest: update-k8s-manifest:
name: Update Preprod K8s Manifest name: Update Preprod
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build] needs: [build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push' if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps: steps:
- name: Compute image tag
id: tag
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "image=ghcr.io/${{ github.repository_owner }}/docuelevate:main-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "tag=main-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
- name: Checkout k8s-cluster-state - name: Checkout k8s-cluster-state
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: christianlouis/k8s-cluster-state repository: christianlouis/k8s-cluster-state
token: ${{ secrets.GH_PAT }} token: ${{ secrets.GH_PAT }}
path: k8s-cluster-state path: k8s-cluster-state
- name: Update image tag
- name: Update image tag in preprod manifest
uses: mikefarah/yq@v4.44.6 uses: mikefarah/yq@v4.44.6
env:
IMAGE: ${{ steps.tag.outputs.image }}
with: with:
cmd: | cmd: yq -i '.images[0].newTag = "${{ github.sha }}"' k8s-cluster-state/apps/docuelevate/preprod/docuelevate-stack.yaml
yq -i '(.. | select(tag == "!!str") | select(test("^(ghcr\\.io/christianlouis/docuelevate|christianlouis/docuelevate):"))) = strenv(IMAGE)' \ - name: Push changes
k8s-cluster-state/apps/docuelevate/preprod/docuelevate-stack.yaml
- name: Commit and push
run: | run: |
cd k8s-cluster-state cd k8s-cluster-state
git config user.name "github-actions[bot]" git config user.name "github-actions"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "actions@github.com"
git add apps/docuelevate/preprod/docuelevate-stack.yaml git add .
if git diff --staged --quiet; then git commit -m "chore: update image to ${{ github.sha }}"
echo "No changes to commit -- image tag already up to date" git push
else
git commit -m "chore(preprod): update docuelevate image to ${{ steps.tag.outputs.tag }}"
git push
fi