ci: split linters into independent parallel jobs and add CI documentation
Refactors .github/workflows/tests.yaml so that flake8, black, mypy, pylint, and bandit each run as their own job in parallel with the test job. This ensures a failure in one tool never blocks the others, and contributors see full feedback from every tool on every CI run. - Upgrades actions/checkout to v4 and actions/setup-python to v5 - All linter jobs are enforced (no continue-on-error) - Test artifacts (junit.xml, coverage.xml) always uploaded - Bandit JSON report always uploaded as artifact - Adds docs/CIWorkflow.md with maintainer documentation - Updates CONTRIBUTING.md with CI workflow table and link Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+125
-32
@@ -3,7 +3,9 @@ name: Run Tests & Linting
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
# ── Tests ──────────────────────────────────────────────────────────────
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
@@ -27,10 +29,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 +48,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 +61,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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user