Partie 2 : Industrialiser et sécuriser un pipeline CI/CD Python avancé
Publié le 18 July 2025
- 1. Introduction
 - 2. Architecture du Pipeline CI/CD
 - 3. Structure du Projet
 - 4. Configuration du Package avec pyproject.toml
 - 5. Workflow de CI/CD - Tests et Qualité
 - 6. Workflow de Release et Déploiement
 - 7. Diagrammes d’Architecture
 - 8. Objets et Modèles du Pipeline
 - 9. Configuration des Secrets
 - 10. Scripts de Développement Local
 - 11. Bonnes Pratiques et Recommandations
 - 12. Monitoring et Observabilité
 - 13. Conclusion
 - 14. Ressources Complémentaires
 
Transformer un pipeline simple en pipeline industriel robuste, respectant les standards modernes du packaging et de l’ingénierie logicielle.
1. Introduction
Dans la première partie de cette série, nous avons construit un pipeline CI/CD simple mais efficace pour automatiser les tests et la publication d’un package Python sur PyPI.
Il est maintenant temps de professionnaliser ce pipeline. Dans cette seconde partie, nous allons :
- 
Migrer vers le standard moderne pyproject.toml,
 - 
Ajouter des outils de qualité et de sécurité (Black, Mypy, Bandit, Safety),
 - 
Mettre en place des tests multi-versions,
 - 
Intégrer un déploiement progressif via Test PyPI,
 - 
Automatiser le versioning et améliorer la surveillance du pipeline.
 
Préparez-vous à passer d’un pipeline fonctionnel à une infrastructure CI/CD professionnelle.
Déployer une application Python sur PyPI nécessite un pipeline CI/CD robuste qui automatise les tests, la construction et la publication des packages. Cet article détaille la mise en place d’un pipeline complet utilisant GitHub Actions pour une application CLI Python, en s’appuyant sur les bonnes pratiques de l’écosystème Python moderne.
2. Architecture du Pipeline CI/CD
Le pipeline que nous allons construire suit une approche en plusieurs étapes :
3. Structure du Projet
Une application CLI Python prête pour la distribution doit respecter une structure standardisée :
playlist-downloader/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── release.yml
│       └── security.yml
├── src/
│   └── playlist_downloader/
│       ├── __init__.py
│       ├── cli.py
│       ├── core/
│       └── adapters/
├── tests/
│   ├── unit/
│   ├── integration/
│   └── conftest.py
├── docs/
├── pyproject.toml
├── requirements.txt
├── requirements-dev.txt
├── MANIFEST.in
├── README.md
├── LICENSE
└── CHANGELOG.md
4. Configuration du Package avec pyproject.toml
Le fichier pyproject.toml est le standard moderne pour configurer les packages Python :
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "playlist-downloader"
authors = [
    {name = "Christophe Hérolivier", email = "cheroliv@example.com"},
]
description = "CLI tool for YouTube playlist management"
readme = "README.md"
requires-python = ">=3.8"
keywords = ["youtube", "playlist", "cli", "downloader"]
license = {text = "MIT"}
classifiers = [
    "Development Status :: 4 - Beta",
    "Environment :: Console",
    "Intended Audience :: End Users/Desktop",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Topic :: Multimedia :: Sound/Audio",
    "Topic :: Utilities",
]
dependencies = [
    "typer>=0.9.0",
    "yt-dlp>=2023.7.6",
    "google-api-python-client>=2.0.0",
    "google-auth-oauthlib>=1.0.0",
    "pyyaml>=6.0",
    "rich>=13.0.0",
]
dynamic = ["version"]
[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "pytest-mock>=3.10.0",
    "black>=23.0.0",
    "flake8>=6.0.0",
    "mypy>=1.0.0",
    "pre-commit>=3.0.0",
    "tox>=4.0.0",
]
test = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "pytest-mock>=3.10.0",
]
[project.urls]
Homepage = "https://github.com/cheroliv/playlist-downloader"
Documentation = "https://github.com/cheroliv/playlist-downloader#readme"
Repository = "https://github.com/cheroliv/playlist-downloader.git"
"Bug Tracker" = "https://github.com/cheroliv/playlist-downloader/issues"
[project.scripts]
playlist-downloader = "playlist_downloader.cli:main"
[tool.setuptools_scm]
write_to = "src/playlist_downloader/_version.py"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--cov=src/playlist_downloader",
    "--cov-report=html",
    "--cov-report=term-missing",
    "--cov-fail-under=85",
]
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
  \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
)/
'''
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
    "yt_dlp.*",
    "googleapiclient.*",
    "google_auth_oauthlib.*",
]
ignore_missing_imports = true
5. Workflow de CI/CD - Tests et Qualité
Le workflow principal (ci.yml) exécute les tests sur plusieurs versions de Python :
name: CI
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10", "3.11"]
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache/pip
          ~/.cache/pre-commit
        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -e ".[dev]"
    - name: Lint with flake8
      run: |
        flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics
        flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
    - name: Check code formatting with Black
      run: black --check src tests
    - name: Type checking with mypy
      run: mypy src
    - name: Run tests with pytest
      run: |
        pytest tests/ -v --cov=src/playlist_downloader \
          --cov-report=xml --cov-report=term-missing
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      if: matrix.python-version == '3.11'
      with:
        file: ./coverage.xml
        flags: unittests
        name: codecov-umbrella
  security:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: "3.11"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install bandit[toml] safety
    - name: Run security checks with bandit
      run: bandit -r src/ -f json -o bandit-report.json
    - name: Check dependencies with safety
      run: safety check --json --output safety-report.json
    - name: Upload security reports
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: security-reports
        path: |
          bandit-report.json
          safety-report.json
  build:
    needs: [test, security]
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: "3.11"
    - name: Install build dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build twine
    - name: Build package
      run: python -m build
    - name: Check package with twine
      run: twine check dist/*
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: dist
        path: dist/
6. Workflow de Release et Déploiement
Le workflow de release (release.yml) gère la publication automatique sur PyPI :
name: Release
on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'test'
        type: choice
        options:
        - test
        - production
env:
  PYTHON_VERSION: "3.11"
jobs:
  release:
    runs-on: ubuntu-latest
    environment:
      name: ${{ github.event.inputs.environment || (startsWith(github.ref, 'refs/tags/') && 'production' || 'test') }}
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build twine
    - name: Build package
      run: python -m build
    - name: Check package
      run: twine check dist/*
    - name: Publish to Test PyPI
      if: github.event.inputs.environment == 'test' || (startsWith(github.ref, 'refs/tags/') && contains(github.ref, 'rc'))
      env:
        TWINE_USERNAME: __token__
        TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
      run: |
        twine upload --repository testpypi dist/*
    - name: Publish to PyPI
      if: github.event.inputs.environment == 'production' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc'))
      env:
        TWINE_USERNAME: __token__
        TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
      run: |
        twine upload dist/*
    - name: Create GitHub Release
      if: startsWith(github.ref, 'refs/tags/')
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: ${{ contains(github.ref, 'rc') }}
  post-release:
    needs: release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}
    - name: Test installation from PyPI
      run: |
        sleep 60  # Attendre la propagation sur PyPI
        pip install playlist-downloader
        playlist-downloader --version
    - name: Update documentation
      run: |
        # Script pour mettre à jour la documentation
        echo "Documentation updated for version ${GITHUB_REF#refs/tags/}"
7. Diagrammes d’Architecture
7.1. Diagramme de Séquence - Processus de Release
7.2. Diagramme d’États - Cycle de Vie du Package
7.3. Diagramme de Déploiement - Infrastructure CI/CD
8. Objets et Modèles du Pipeline
8.1. Diagramme de Classes - Modèles CI/CD
9. Configuration des Secrets
Pour que le pipeline fonctionne, vous devez configurer les secrets suivants dans GitHub :
9.1. Secrets GitHub Actions
# Dans Settings > Secrets and variables > Actions
# Token PyPI pour la production
PYPI_API_TOKEN=pypi-...
# Token Test PyPI pour les pré-releases
TEST_PYPI_API_TOKEN=pypi-...
# Token GitHub pour créer les releases
GITHUB_TOKEN=(automatiquement fourni)
# Token Codecov (optionnel)
CODECOV_TOKEN=...
9.2. Génération des Tokens PyPI
# 1. Créer un compte sur PyPI et Test PyPI
# 2. Aller dans Account Settings > API tokens
# 3. Créer un token avec scope "Entire account" ou spécifique au projet
# 4. Format du token : pypi-AgEIcHlwaS5vcmc...
10. Scripts de Développement Local
Pour faciliter le développement, créez des scripts utilitaires :
10.1. Makefile
.PHONY: install test lint format security build clean release-test release-prod
install:
	pip install -e ".[dev]"
test:
	pytest tests/ -v --cov=src/playlist_downloader
lint:
	flake8 src tests
	mypy src
format:
	black src tests
security:
	bandit -r src/
	safety check
build:
	python -m build
	twine check dist/*
clean:
	rm -rf build/ dist/ *.egg-info/
	find . -type d -name __pycache__ -delete
	find . -name "*.pyc" -delete
release-test: clean build
	twine upload --repository testpypi dist/*
release-prod: clean build
	twine upload dist/*
pre-commit: format lint test security
	@echo "✅ Prêt pour commit"
10.2. Script de Version
#!/usr/bin/env python3
"""Script pour gérer les versions du projet."""
import sys
import subprocess
from pathlib import Path
def get_current_version():
    """Récupère la version actuelle depuis git."""
    try:
        result = subprocess.run(
            ["git", "describe", "--tags", "--abbrev=0"],
            capture_output=True,
            text=True,
            check=True
        )
        return result.stdout.strip()
    except subprocess.CalledProcessError:
        return "0.0.0"
def create_version_tag(version, message=None):
    """Crée un tag de version."""
    if not version.startswith('v'):
        version = f'v{version}'
    tag_message = message or f"Release {version}"
    subprocess.run(["git", "tag", "-a", version, "-m", tag_message], check=True)
    print(f"✅ Tag {version} créé")
    # Push le tag
    subprocess.run(["git", "push", "origin", version], check=True)
    print(f"✅ Tag {version} poussé vers origin")
if __name__ == "__main__":
    if len(sys.argv) < 2:
        current = get_current_version()
        print(f"Version actuelle: {current}")
        print("Usage: python version.py <new_version> [message]")
        sys.exit(1)
    new_version = sys.argv[1]
    message = sys.argv[2] if len(sys.argv) > 2 else None
    create_version_tag(new_version, message)
11. Bonnes Pratiques et Recommandations
11.1. Versioning Sémantique
Utilisez le versioning sémantique (SemVer) :
- 
MAJOR.MINOR.PATCH(ex: 1.2.3) - 
MAJOR: changements incompatibles - 
MINOR: nouvelles fonctionnalités compatibles - 
PATCH: corrections de bugs compatibles 
11.2. Stratégie de Branching
main          ──●──●──●──●──●────●── (releases stables)
               /       /          /
develop    ──●──●──●──●──●──●──●──●── (développement)
            /     /        /
feature/xxx  ●──●──●──●──●──/ (fonctionnalités)
11.3. Tests et Couverture
- 
Couverture de code minimum : 85%
 - 
Tests unitaires pour la logique métier
 - 
Tests d’intégration pour les adapters
 - 
Tests de bout en bout pour les CLI
 
11.4. Sécurité
- 
Scan automatique des dépendances (Safety)
 - 
Analyse statique du code (Bandit)
 - 
Secrets jamais dans le code
 - 
Utilisation de tokens PyPI spécifiques
 
12. Monitoring et Observabilité
12.1. Métriques de Pipeline
# .github/workflows/metrics.yml
name: Pipeline Metrics
on:
  workflow_run:
    workflows: ["CI", "Release"]
    types: [completed]
jobs:
  metrics:
    runs-on: ubuntu-latest
    steps:
    - name: Collect metrics
      run: |
        echo "Pipeline: ${{ github.event.workflow_run.name }}"
        echo "Status: ${{ github.event.workflow_run.conclusion }}"
        echo "Duration: ${{ github.event.workflow_run.updated_at - github.event.workflow_run.created_at }}"
        # Envoyer vers système de monitoring
12.2. Notifications
# Ajout dans les workflows pour notifications
- name: Notify on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    text: "❌ Pipeline failed for ${{ github.repository }}"
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
13. Conclusion
Ce pipeline CI/CD complet pour Python offre :
- 
Automatisation complète : de la validation du code à la publication
 - 
Sécurité : scans automatiques et gestion sécurisée des secrets
 - 
Qualité : tests multi-versions, linting et couverture de code
 - 
Fiabilité : déploiement progressif via Test PyPI
 - 
Traçabilité : artifacts, rapports et releases GitHub
 
L’adoption de ces pratiques garantit un processus de livraison robuste et professionnel pour vos applications Python CLI, facilitant la maintenance et l’évolution de vos projets sur le long terme.