refactor: configure pre-commit and CI/CD pipeline

- Restructured GitHub Actions workflow with separate jobs for linting, testing, and security
- Configured pre-commit hooks: black, isort, flake8, yamllint
- Added setup.cfg for centralized configuration
- Relaxed flake8 rules (B008, D* docstrings) for FastAPI compatibility
- Removed bandit (pbr dependency issue) - can be added later
- All pre-commit checks now passing
This commit is contained in:
Johan
2026-02-02 15:46:05 +01:00
parent 35d6e2ff05
commit 0d0dd3cfcf
14 changed files with 316 additions and 45 deletions

4
.bandit Normal file
View File

@@ -0,0 +1,4 @@
[bandit]
exclude_dirs = ['/tests', '/.venv']
tests = []
skips = []

16
.flake8 Normal file
View File

@@ -0,0 +1,16 @@
[flake8]
max-line-length = 100
extend-ignore = E203, W503, D100, D101, D102, D103, D104, D105, D106, B008, B009
exclude =
.git,
__pycache__,
.venv,
venv,
.eggs,
*.egg,
build,
dist
per-file-ignores =
__init__.py:F401
backend/app/main.py:F405

View File

@@ -1,16 +1,17 @@
name: CI name: Task Manager CI/CD
on: on:
push: push:
branches: [ main ] branches: [main, develop]
pull_request: pull_request:
branches: [main] branches: [main]
jobs: jobs:
backend: lint:
name: Lint & Format Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
@@ -18,23 +19,65 @@ jobs:
with: with:
python-version: '3.10' python-version: '3.10'
- name: Install backend dependencies - name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install black flake8 isort
- name: Check code formatting (black)
run: black --check backend/
- name: Check import sorting (isort)
run: isort --check-only backend/
- name: Lint code (flake8)
run: flake8 backend/ --max-line-length=100
test:
name: Run Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: | run: |
if [ -f backend/requirements.txt ]; then
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r backend/requirements.txt pip install -r backend/requirements.txt
else pip install pytest pytest-cov
echo "No backend/requirements.txt found, skipping install"
fi
- name: Sanity check - compile Python files - name: Run unit tests
run: python -m compileall backend
- name: Run tests if present
run: | run: |
if [ -d backend/tests ]; then if [ -d backend/tests ]; then
python -m pip install pytest pytest backend/tests/ -v --cov=backend/app
pytest -q
else else
echo "No tests found; skipping pytest" echo "No tests found in backend/tests/"
fi fi
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: 'backend/'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'

View File

@@ -1,10 +1,71 @@
# See https://pre-commit.com for more information ---
# See https://pre-commit.com/hooks.html for more hooks # Pre-commit configuration for Task Manager project
# Install: pip install pre-commit && pre-commit install
# Run manually: pre-commit run --all-files
repos: repos:
# Standard file checks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0 rev: v4.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
name: Trim trailing whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
name: Fix end-of-file marker
- id: check-yaml - id: check-yaml
name: Check YAML validity
- id: check-json
name: Check JSON validity
- id: check-added-large-files - id: check-added-large-files
name: Check for large files
args: ['--maxkb=1000']
- id: check-case-conflict
name: Check for case conflicts
- id: mixed-line-ending
name: Fix mixed line endings
args: ['--fix=lf']
# Python code formatting (Black)
- repo: https://github.com/psf/black
rev: 24.1.1
hooks:
- id: black
name: Format code with black
language_version: python3
args: ['--line-length=100']
# Import sorting (isort)
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
name: Sort imports with isort
args: ['--profile=black']
# Linting (Flake8)
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
name: Lint code with flake8
args: ['--max-line-length=100', '--ignore=E203,W503,D100,D101,D102,D103,D104,D105,D106,B008']
additional_dependencies: ['flake8-bugbear']
# Security checks (Bandit)
# YAML linting
- repo: https://github.com/adrienverge/yamllint
rev: v1.33.0
hooks:
- id: yamllint
name: Lint YAML files
args: ['-d', '{extends: relaxed, rules: {line-length: {max: 120}}}']
ci:
autofix_commit_msg: 'chore: auto-fixes from pre-commit hooks'
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: 'chore: pre-commit autoupdate'
autoupdate_schedule: weekly
skip: [bandit]
submodules: false

91
CICD.md Normal file
View File

@@ -0,0 +1,91 @@
# CI/CD Documentation
## Pre-commit Hooks
Pre-commit hooks automatically check and fix code quality issues before commits.
### Installation
```bash
pip install pre-commit
pre-commit install
```
### Manual Execution
```bash
# Run all hooks on changed files
pre-commit run
# Run all hooks on all files
pre-commit run --all-files
# Run a specific hook
pre-commit run black --all-files
pre-commit run flake8 --all-files
```
### Hooks Configured
- **trailing-whitespace**: Remove trailing whitespace
- **end-of-file-fixer**: Ensure files end with newline
- **check-yaml**: Validate YAML syntax
- **check-json**: Validate JSON syntax
- **check-added-large-files**: Prevent large files (>1MB)
- **check-case-conflict**: Detect case conflicts
- **mixed-line-ending**: Fix mixed line endings
- **black**: Format Python code
- **isort**: Sort imports
- **flake8**: Lint Python code (max 100 chars/line)
- **bandit**: Security checks
- **yamllint**: Lint YAML files
## GitHub Actions CI/CD
### Workflow: Task Manager CI/CD
**Triggers:**
- Push to `main` or `develop` branches
- Pull requests to `main` branch
**Jobs:**
#### 1. **Lint & Format Check**
- Checks black formatting
- Checks isort import order
- Runs flake8 linting
- Runs bandit security checks
#### 2. **Run Tests**
- Depends on lint job passing
- Installs dependencies from `backend/requirements.txt`
- Runs pytest with coverage
- Requires tests in `backend/tests/` (optional)
#### 3. **Security Scan**
- Runs Trivy vulnerability scanner on filesystem
- Uploads results to GitHub Security tab
### Quick Fix
To automatically fix formatting issues locally:
```bash
black backend/
isort backend/
```
## Configuration Files
- **.pre-commit-config.yaml**: Pre-commit hooks configuration
- **setup.cfg**: isort, flake8, and pytest configuration
- **.flake8**: Flake8 linting rules
- **.bandit**: Bandit security configuration
- **.github/workflows/ci.yml**: GitHub Actions workflow
## Development Workflow
1. **Local Development**: Use pre-commit hooks to catch issues early
2. **Commit**: Pre-commit hooks run before commit
3. **Push**: GitHub Actions runs lint, test, and security checks
4. **PR**: Review status checks before merge

View File

@@ -1,5 +1,5 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase from sqlalchemy.orm import DeclarativeBase, sessionmaker
# SQLite local file. In CI, you can swap to Postgres later. # SQLite local file. In CI, you can swap to Postgres later.
DATABASE_URL = "sqlite:///./taskmanager.db" DATABASE_URL = "sqlite:///./taskmanager.db"

View File

@@ -1,15 +1,15 @@
import os import os
import yaml
from fastapi import FastAPI, Body, Depends, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from sqlalchemy import select, text
from datetime import datetime, timezone from datetime import datetime, timezone
import yaml
from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import select, text
from sqlalchemy.orm import Session
from .db import Base, engine, get_db from .db import Base, engine, get_db
from .models import Task from .models import Task
from .schemas import TaskCreate, TaskUpdate, TaskOut from .schemas import TaskCreate, TaskOut, TaskUpdate
API_KEY = "devsecops-demo-secret-<a_remplacer>" API_KEY = "devsecops-demo-secret-<a_remplacer>"
@@ -31,21 +31,25 @@ Base.metadata.create_all(bind=engine)
def debug(): def debug():
return {"env": dict(os.environ)} return {"env": dict(os.environ)}
@app.get("/health") @app.get("/health")
def health(): def health():
return {"status": "ok"} return {"status": "ok"}
@app.get("/admin/stats") @app.get("/admin/stats")
def admin_stats(x_api_key: str | None = Header(default=None)): def admin_stats(x_api_key: str | None = Header(default=None)):
if x_api_key != API_KEY: if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized") raise HTTPException(status_code=401, detail="Unauthorized")
return {"tasks": ""} return {"tasks": ""}
@app.post("/import") @app.post("/import")
def import_yaml(payload: str = Body(embed=True)): def import_yaml(payload: str = Body(embed=True)):
data = yaml.full_load(payload) data = yaml.full_load(payload)
return {"imported": True, "keys": list(data.keys()) if isinstance(data, dict) else "n/a"} return {"imported": True, "keys": list(data.keys()) if isinstance(data, dict) else "n/a"}
@app.get("/tasks", response_model=list[TaskOut]) @app.get("/tasks", response_model=list[TaskOut])
def list_tasks(db: Session = Depends(get_db)): def list_tasks(db: Session = Depends(get_db)):
tasks = db.execute(select(Task).order_by(Task.id.desc())).scalars().all() tasks = db.execute(select(Task).order_by(Task.id.desc())).scalars().all()

View File

@@ -1,6 +1,8 @@
from sqlalchemy import String, Integer, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import DateTime, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base from .db import Base
@@ -10,6 +12,12 @@ class Task(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False) title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(String(1000), nullable=True) description: Mapped[str | None] = mapped_column(String(1000), nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="TODO") # TODO/DOING/DONE status: Mapped[str] = mapped_column(
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) String(20), nullable=False, default="TODO"
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) ) # TODO/DOING/DONE
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)
)

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field
class TaskCreate(BaseModel): class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200) title: str = Field(min_length=1, max_length=200)

43
setup.cfg Normal file
View File

@@ -0,0 +1,43 @@
[metadata]
name = task-manager
version = 1.0.0
description = Task Manager API with FastAPI
author = ENI DevSecOps
[flake8]
max-line-length = 100
extend-ignore = E203, W503
exclude =
.git,
__pycache__,
.venv,
venv,
.eggs,
*.egg,
build,
dist,
.pre-commit-cache
[isort]
profile = black
line_length = 100
include_trailing_comma = True
force_grid_wrap = 0
use_parentheses = True
ensure_newline_before_comments = True
skip_glob = [.venv, */migrations/*, */node_modules/*]
[tool:pytest]
testpaths = backend/tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --strict-markers --tb=short
[coverage:run]
source = backend/app
omit =
*/site-packages/*
*/distutils/*
*/venv/*
.venv/*