Μέρος 2 : Βιομηχανοποίηση και ασφάλιση ενός προχωρούμενο pipeline CI/CD Python
Δημοσιεύτηκε 18 July 2025
- Εισαγωγή
- Αρχιτεκτονική του pipeline CI/CD
- Δομή του έργου
- �Ρύθμιση πακέτου με το pyproject.toml
- �Ροή εργασιών CI/CD - Δοκιμές και Ποιότητα
- Workflow Έκδοσης και Διανομής
- Διαγράμματα Αρχιτεκτονικής
- Αντικείμενα και Μοντέλα του Pipeline
- Ρύθμιση μυστικών
- Σκρίπτες τοπικής ανάπτυξης
- Καλές πρακτικές και συστάσεις
- Παρακολούθηση και Παρατηρησιμότητα
- Συμπέρασμα
- Συμπληρωματικοί Πόροι
Εισαγωγή
Στο πρώτο μέρος αυτής της σειράς, κατασκευάσαμε ένα απλό αλλά αποτελεσματικό pipeline CI/CD για να αυτοματοποιήσουμε τα τεστ και τη δημοσίευση ενός πακέτου Python στο PyPI.
Τώρα είναι η ώρα για να επαγγελματοποιήσουμε αυτό το pipeline. Σε αυτό το δεύτερο μέρος, θα :
-
Μετακόμιση στον σύγχρονο πρότυπο pyproject.toml,
-
Προσθέστε εργαλεία ποιότητας και ασφάλειας (Black, Mypy, Bandit, Safety)
-
Θέτε σε εφαρμογή δοκιμές πολλαπλών εκδόσεων,
-
Ενσωματώστε μια σταδιακή ανάπτυξη μέσω του Test PyPI,
-
Αυτοματοποίηση της διαχείρισης εκδόσεων και βελτίωση της παρακολούθησης του pipeline.
Προετοιμάστε να μεταβείτε από ένα λειτουργικό pipeline σε μια επαγγελματική υποδομή CI/CD.
Η ανάπτυξη μιας εφαρμογής Python στο PyPI απαιτεί ένα ισχυρό CI/CD pipeline που αυτοματοποιεί τα τεστ, το χτίσιμο και τη δημοσίευση των πακέτων. Αυτό το άρθρο αναλυτικά περιγράφει τη ρύθμιση ενός πλήρους pipeline χρησιμοποιώντας GitHub Actions για μια εφαρμογή CLI Python, βασισμένο στις καλύτερες πρακτικές του σύγχρονου ecosystem Python.
Αρχιτεκτονική του pipeline CI/CD
Το pipeline που θα κατασκευάσουμε ακολουθεί μια προσέγγιση σε βήματα:
@startuml
!theme plain
title Παραγωγόδοχοι CI/CD Python προς το PyPI
skinparam backgroundColor #f8f9fa
skinparam componentStyle rectangle
rectangle "Πρόγραμματιστής" as dev
rectangle "Αποθετήριο GitHub" as repo {
rectangle ".github/workflows/" as workflows
rectangle "δοκιμές/" as tests
rectangle "setup.py / pyproject.toml" as setup
rectangle "requirements.txt" as req
}
rectangle "GitHub Actions" as actions {
rectangle "Τρέχων τεστ" as test_runner
rectangle "Κτίστε" as build
rectangle "Σάρωση ασφαλείας" as security
rectangle "Έλεγχος ποιότητας" as quality
}
rectangle "PyPI" as pypi {
rectangle "Δοκιμή PyPI" as test_pypi
rectangle "Παραγωγή PyPI" as prod_pypi
}
rectangle "Χρήστες" as users
dev --> repo : push/PR
repo --> actions : trigger workflow
actions --> test_runner : run tests
actions --> security : security checks
actions --> quality : code quality
actions --> build : build package
build --> test_pypi : deploy (pre-release)
build --> prod_pypi : deploy (release)
prod_pypi --> users : install package
@enduml
Δομή του έργου
Μια εφαρμογή Python CLI έτοιμη για διανομή πρέπει να τηρεί μια τυποποιημένη δομή:
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
�Ρύθμιση πακέτου με το pyproject.toml
Το αρχείο`pyproject.toml`είναι το σύγχρονο πρότυπο για τη ρύθμιση των πακέτων 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 = "[email protected]"},
]
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
�Ροή εργασιών CI/CD - Δοκιμές και Ποιότητα
Το κύριο workflow (ci.yml) εκτελεί τα τεστ σε πολλές εκδόσεις της 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/
Workflow Έκδοσης και Διανομής
Η ροή έκδοση (release.yml) διαχειρίζεται την αυτόματη δημοσίευση στο 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/}"
Διαγράμματα Αρχιτεκτονικής
Διάγραμμα ακολουθίας - διαδικασία κυκλοφορίας
@startuml
!theme plain
title Ακολουθία έκδοσης προς το PyPI
actor Developer as dev
participant "GitHub" as gh
participant "GitHub Actions" as ga
participant "Δοκιμή PyPI" as tpypi
participant "PyPI" as pypi
participant "Χρήστες" as users
dev -> gh : git push --tags v1.2.3
gh -> ga : trigger release workflow
ga -> ga : checkout code
ga -> ga : setup Python environment
ga -> ga : install dependencies
ga -> ga : run tests
ga -> ga : build package (wheel + sdist)
ga -> ga : check package with twine
alt Pre-release (rc tag)
ga -> tpypi : upload to Test PyPI
tpypi -> ga : confirm upload
else Stable release
ga -> pypi : upload to PyPI
pypi -> ga : confirm upload
end
ga -> gh : create GitHub release
ga -> ga : test installation from PyPI
users -> pypi : pip install playlist-downloader
pypi -> users : download package
@enduml
Διάγραμμα cathedral - Κύκλος ζωής του πακέ</tool_call>ς
@startuml
!theme plain
title Καταστάσεις του πακέτου Python
[*] --> Development
Development --> Testing : commit/PR
Testing --> Development : tests fail
Testing --> Built : tests pass
Built --> TestPyPI : pre-release tag
Built --> PyPI : stable tag
TestPyPI --> PyPI : validation OK
PyPI --> Published
Published --> [*]
state Development {
[*] --> Coding
Coding --> LocalTesting
LocalTesting --> Coding : fix issues
LocalTesting --> ReadyForCI : all tests pass
}
state Testing {
[*] --> CITests
CITests --> SecurityScan
SecurityScan --> QualityCheck
QualityCheck --> BuildValidation
}
@enduml
Διάγραμμα ανάπτυξης - Υποδομή CI/CD
@startuml
!theme plain
title Δομή ανάπτυξης
node "GitHub" {
component "Αποθετήριο" as repo
component "Ενέργειες τρέχων" as runner
component "Αποθήκη μυστικών" as secrets
}
node "PyPI Υποδομή" {
component "PyPI" as pypi
component "Δοκιμή PyPI" as testpypi
database "ευρετήριο πακέτων" as index
}
node "μηχανή ανάπτυξης" {
component "πελάτης Git" as git
component "Περιβάλλον Python" as python
component "IDE" as ide
}
node "Περιβάλλον Χρήστη" {
component "πιπ" as pip_client
component "περιβάλλον εκτέλεσης Python" as py_runtime
}
git --> repo : push code/tags
repo --> runner : trigger workflows
runner --> secrets : read API tokens
runner --> testpypi : upload pre-release
runner --> pypi : upload release
pypi --> index : store package
pip_client --> pypi : download package
py_runtime <-- pip_client : install package
@enduml
Αντικείμενα και Μοντέλα του Pipeline
Διάγραμμα Κλάσεων - Μοντέλα CI/CD
@startuml
!theme plain
title Μοντέλα του Pipeline CI/CD
class PipelineConfig {
+python_versions: List[str]
+test_environments: List[str]
+security_checks: bool
+coverage_threshold: float
+validate()
}
class BuildArtifact {
+name: str
+version: str
+wheel_path: str
+sdist_path: str
+checksums: Dict[str, str]
+validate_integrity()
}
class TestResult {
+test_suite: str
+python_version: str
+passed: int
+failed: int
+coverage: float
+duration: float
+is_success(): bool
}
class SecurityReport {
+bandit_issues: List[Issue]
+safety_vulnerabilities: List[Vulnerability]
+severity_level: str
+is_secure(): bool
}
class DeploymentTarget {
+name: str
+url: str
+api_token: str
+environment: str
+deploy(artifact: BuildArtifact)
}
class ReleaseManager {
+version: str
+changelog: str
+artifacts: List[BuildArtifact]
+test_results: List[TestResult]
+security_report: SecurityReport
+deploy_to_test()
+deploy_to_production()
+create_github_release()
}
PipelineConfig ||--o{ TestResult
ReleaseManager *-- BuildArtifact
ReleaseManager *-- SecurityReport
ReleaseManager o-- DeploymentTarget
DeploymentTarget ..> BuildArtifact : uses
@enduml
Ρύθμιση μυστικών
για να λειτουργήσει το pipeline, πρέπει να ρυθμίσετε τα ακόλουθα μυστικά στο GitHub :
Μυστικά 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=...
Δημιουργία των διακριτικών 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...
Σκρίπτες τοπικής ανάπτυξης
Για να διευκολύνετε την ανάπτυξη, δημιουργήστε σενάρια βοηθών:
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"
Σενάριο Έκδοσης
#!/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)
Καλές πρακτικές και συστάσεις
Σεμαντική διαχείριση εκδόσεων
Χρησιμοποιήστε τη σημασιολογική εκδοχή (SemVer):
-
MAJOR.MINOR.PATCH(ex: 1.2.3) -
MAJOR: αλλαγές ασυμβατές -
MINOR: νέες συμβατές λειτουργίες -
PATCH: συμβατές διορθώσεις σφαλμάτων
Στρατζηγια διακλαδωσης
main ──●──●──●──●──●────●── (releases stables)
/ / /
develop ──●──●──●──●──●──●──●──●── (développement)
/ / /
feature/xxx ●──●──●──●──●──/ (fonctionnalités)
δοκιμές και κάλυψη
-
Ελάχιστη καλύμversa κώδικα : 85%
-
Μονάδιες δοκιμές για την επιχειρηματική λογική
-
Ενταξιακά τεστ για τους αδαπτήρες
-
Τεστ από άκρο σε άκρο για τα CLI
Ασφάλεια
-
Αυτόματο σαρώμα εξαρτήσεων (Safety)
-
Στατική ανάλυση κώδικα (Bandit)
-
Τα μυστικά ποτέ στον κώδικα
-
Χρήση συγκεκριμένων διακριτικών PyPI
Παρακολούθηση και Παρατηρησιμότητα
Μετρικά 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
Ειδοποιήσεις
# 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 }}
Συμπέρασμα
Αυτό το πλήρες pipeline CI/CD για Python προσφέρει:
-
Πλήρης αυτοματοποίηση: από τη δοκιμασία του κώδικα προς τη δημοσίευση
-
Ασφάλειααυτόματες άρωσεις και ασφαλής διαχείριση των μυστικών
-
Ποιότηταδοκιμές πολλαπλών εκδόσεων, linting και καλυμμένο κώδικα
-
αξιοπιστία: προοδευτική ανάπτυξη μέσω Test PyPI
-
ιχνευσιμότηταartifacts, αναφορές και releases GitHub
Η υιοθέτηση αυτών των πρακτικών εξασφαλίζει μια ισχυρή και επαγγελματική διαδικασία παράδοσης για τις εφαρμογές σας Python CLI, διευκολύνοντας τη συντήρηση και την εξέλιξη των εργών σας στον μακρόπρόθεσμο χρόνο.