From d025923ef5df56f96bc5ddab248819fd48bcdaff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:07:53 +0000 Subject: [PATCH] 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> --- .github/workflows/tests.yaml | 157 ++++++++++++++++++++++++++++------- CONTRIBUTING.md | 17 ++-- docs/CIWorkflow.md | 120 ++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 37 deletions(-) create mode 100644 docs/CIWorkflow.md diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4bb89e40..484c1645 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58b1d29a..01fcf296 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/docs/CIWorkflow.md b/docs/CIWorkflow.md new file mode 100644 index 00000000..fb2ffa57 --- /dev/null +++ b/docs/CIWorkflow.md @@ -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.