Merge pull request #258 from christianlouis/copilot/refactor-linter-workflow

ci: split linters into independent parallel jobs
This commit is contained in:
Christian Krakau-Louis
2026-02-12 04:11:22 +01:00
committed by GitHub
3 changed files with 260 additions and 37 deletions
+128 -32
View File
@@ -2,8 +2,13 @@ name: Run Tests & Linting
on: [push, pull_request]
permissions:
contents: read
jobs:
# ── Tests ──────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
services:
redis:
@@ -27,10 +32,10 @@ jobs:
--health-retries 5
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.11"
@@ -46,6 +51,7 @@ jobs:
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 }}
@@ -58,37 +64,127 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Run Linter (Flake8)
run: flake8 app/ --max-line-length=120 --extend-ignore=E203,W503
continue-on-error: false
- name: Run Code Formatter (Black)
run: black --check app/ --line-length=120
continue-on-error: false
- name: Run Type Checker (Mypy)
run: mypy app/ --ignore-missing-imports
continue-on-error: true
- name: Run Linter (Pylint)
run: pylint app/ --max-line-length=120 --disable=C0111,C0103,R0903
continue-on-error: true
- name: Run Security Linter (Bandit) - Full Report
run: |
echo "Running Bandit security scan..."
bandit -r app/ -f json -o bandit-report.json || true
continue-on-error: true
- name: Run Security Linter (Bandit) - Fail on High/Medium
run: |
echo "Running Bandit security scan (fail on high/medium severity)..."
bandit -r app/ -ll
continue-on-error: false
- name: Upload Bandit Report
- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
junit.xml
coverage.xml
# ── Flake8 ─────────────────────────────────────────────────────────────
flake8:
name: Flake8
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 Dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
- name: Run Flake8
run: flake8 app/ --max-line-length=120 --extend-ignore=E203,W503
# ── Black ──────────────────────────────────────────────────────────────
black:
name: Black
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 Dependencies
run: |
python -m pip install --upgrade pip
pip install black
- name: Run Black
run: black --check app/ --line-length=120
# ── Mypy ───────────────────────────────────────────────────────────────
mypy:
name: Mypy
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 Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run Mypy
run: mypy app/ --ignore-missing-imports
# ── Pylint ─────────────────────────────────────────────────────────────
pylint:
name: Pylint
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 Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run Pylint
run: pylint app/ --max-line-length=120 --disable=C0111,C0103,R0903
# ── Bandit ─────────────────────────────────────────────────────────────
bandit:
name: Bandit
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 Dependencies
run: |
python -m pip install --upgrade pip
pip install bandit
- name: Run Bandit - Full Report
if: ${{ !cancelled() }}
run: bandit -r app/ -f json -o bandit-report.json || true
- name: Run Bandit - Fail on High/Medium
run: bandit -r app/ -ll
- name: Upload Bandit Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
if: always()
with:
name: bandit-report
path: bandit-report.json
+12 -5
View File
@@ -264,12 +264,18 @@ Tests are organized using pytest markers:
#### Running Tests in CI
Tests run automatically in GitHub Actions for all pull requests. The CI environment:
Tests run automatically in GitHub Actions for all pull requests. The CI workflow splits every check into an **independent parallel job** so that a failure in one tool never blocks the others:
1. Installs all dependencies from `requirements-dev.txt`
2. Runs pytest with coverage
3. Uploads coverage reports to Codecov
4. Fails the build if tests don't pass or coverage drops
| Job | Tool | What it checks |
|--------|--------|--------------------------------------|
| `test` | pytest | Unit/integration tests + coverage |
| `flake8`| flake8 | PEP 8 style |
| `black` | black | Code formatting |
| `mypy` | mypy | Static type checking |
| `pylint`| pylint | Code quality |
| `bandit`| bandit | Security vulnerabilities |
All jobs are enforced — failures in any linter will block the PR. For full details see [docs/CIWorkflow.md](docs/CIWorkflow.md).
### Code Style
@@ -317,6 +323,7 @@ DocuElevate/
### Documentation
- **[AGENTIC_CODING.md](AGENTIC_CODING.md)** - Comprehensive guide for AI agents and developers
- **[README.md](README.md)** - Project overview and quickstart
- **[docs/CIWorkflow.md](docs/CIWorkflow.md)** - CI pipeline and linter details for maintainers
- **[ROADMAP.md](ROADMAP.md)** - Future features and long-term vision
- **[MILESTONES.md](MILESTONES.md)** - Release planning and versioning
- **[TODO.md](TODO.md)** - Current tasks and priorities
+120
View File
@@ -0,0 +1,120 @@
# CI Workflow Guide
This document describes the CI/CD pipeline for DocuElevate, focusing on the **Run Tests & Linting** workflow.
## Overview
The CI workflow (`.github/workflows/tests.yaml`) runs automatically on every push and pull request. It is designed so that **each linter and the test suite run as independent jobs**, ensuring that a failure in one tool never blocks the results from another.
## Workflow Jobs
| Job | Tool | Purpose | Enforced |
|--------|--------|---------------------------------------------|----------|
| `test` | pytest | Unit/integration tests with coverage | ✅ |
| `flake8`| flake8 | PEP 8 style linting | ✅ |
| `black` | black | Code formatting check | ✅ |
| `mypy` | mypy | Static type checking | ✅ |
| `pylint`| pylint | Code quality analysis | ✅ |
| `bandit`| bandit | Security vulnerability scanning | ✅ |
All six jobs start **in parallel** as soon as the workflow is triggered. No job depends on or waits for any other job.
### Tests
- Runs pytest with coverage reporting (XML + terminal).
- Excludes E2E tests that require Docker-in-Docker (`-m "not e2e"`).
- Uses Redis and RabbitMQ service containers.
- Uploads coverage and JUnit XML results to Codecov.
- Uploads `junit.xml` and `coverage.xml` as workflow artifacts (always, even on failure).
### Flake8
- Checks `app/` against PEP 8 with `max-line-length=120`.
- Ignores `E203` (whitespace before `:`) and `W503` (line break before binary operator), matching the Black formatter.
### Black
- Verifies that all files in `app/` are formatted with `black --line-length=120`.
- Runs in `--check` mode (no files are modified).
### Mypy
- Type checks `app/` with `--ignore-missing-imports`.
- Requires full project dependencies (installs `requirements-dev.txt`).
### Pylint
- Analyzes `app/` with `max-line-length=120`.
- Disables `C0111` (missing docstrings), `C0103` (naming conventions), and `R0903` (too few public methods).
- Requires full project dependencies (installs `requirements-dev.txt`).
### Bandit
- Produces a full JSON report (`bandit-report.json`) uploaded as a workflow artifact.
- **Fails the job** if any high- or medium-severity issues are found (`-ll` flag).
- The JSON report is always uploaded, even if the severity check fails.
## Artifacts
The following artifacts are uploaded after every run:
| Artifact | Contents | Condition |
|------------------|--------------------------------------|----------------------|
| `test-results` | `junit.xml`, `coverage.xml` | Always (unless cancelled) |
| `bandit-report` | `bandit-report.json` | Always (unless cancelled) |
## Running Linters Locally
You can run the same checks locally before pushing:
```bash
# Install dev dependencies
pip install -r requirements-dev.txt
# Run each linter
flake8 app/ --max-line-length=120 --extend-ignore=E203,W503
black --check app/ --line-length=120
mypy app/ --ignore-missing-imports
pylint app/ --max-line-length=120 --disable=C0111,C0103,R0903
bandit -r app/ -ll
# Run tests
pytest tests/ -v --cov=app --cov-report=term -m "not e2e"
```
Or use pre-commit hooks to run checks automatically on each commit:
```bash
pip install pre-commit
pre-commit install
pre-commit run --all-files
```
## Design Decisions
### Why Separate Jobs Instead of Steps?
Previously, all linters ran as sequential steps in a single job. This meant:
- A failure in flake8 would prevent black, mypy, pylint, and bandit from running.
- Contributors only saw feedback from the **first** tool that failed, not all of them.
By splitting into independent jobs:
- **All tools always run** regardless of other failures.
- Contributors see **all** feedback in a single CI run.
- Jobs run **in parallel**, reducing total wall-clock time.
### Why Are All Linters Enforced?
All linters are set to fail the CI (no `continue-on-error`). This ensures:
- The codebase stays consistently formatted (Black).
- Style issues are caught early (Flake8).
- Type errors surface before merge (Mypy).
- Code quality standards are maintained (Pylint).
- Security issues are flagged immediately (Bandit).
## Copilot Code Compliance
All code — whether written by hand or suggested by GitHub Copilot — goes through the same CI pipeline. Copilot-generated code is linted, type-checked, and security-scanned identically to human-written code. Contributors using Copilot should ensure suggestions pass all checks before committing.