Complete CI/CD Pipeline
Build a complete CI/CD pipeline with GitHub Actions integrating testing, linting, formatting, type checking, security scanning, coverage gates, and deployment
Complete CI/CD Pipeline
This capstone lesson brings together everything you've learned — testing, coverage, linting, formatting, type checking, and security scanning — into a complete CI/CD pipeline. You'll build a professional-grade GitHub Actions workflow with quality gates at every stage.
Pipeline Architecture
The Complete GitHub Actions Workflow
1. Main Workflow (Triggered on PR and Push)
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
PYTHON_VERSION: '3.12'
COVERAGE_THRESHOLD: '85'
jobs:
# ============================================================
# STAGE 1: Code Quality Checks (fast feedback)
# ============================================================
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Check formatting (Black)
run: |
black --check --diff --line-length 100 src/ tests/
continue-on-error: false
- name: Check import order (isort)
run: |
isort --check-only --diff --profile black src/ tests/
continue-on-error: false
- name: Lint with Ruff
run: |
ruff check src/ tests/
continue-on-error: false
- name: Lint with Flake8
run: |
flake8 src/ tests/ --statistics --max-line-length=100
continue-on-error: false
- name: Upload lint reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: |
**/ruff_report.txt
**/flake8_report.txt
# ============================================================
# STAGE 2: Static Analysis
# ============================================================
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install mypy types-requests types-PyYAML
- name: Type check with mypy
run: |
mypy src/ --strict --ignore-missing-imports
continue-on-error: false
- name: Security scan with bandit
run: |
bandit -r src/ -ll -f json -o bandit_report.json
continue-on-error: false
- name: Upload security report
if: always()
uses: actions/upload-artifact@v4
with:
name: security-reports
path: bandit_report.json
# ============================================================
# STAGE 3: Testing & Coverage
# ============================================================
test:
name: Tests & Coverage
runs-on: ubuntu-latest
needs: [quality, static-analysis]
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -e .
- name: Run unit tests with coverage
run: |
pytest tests/unit/ \
--cov=src \
--cov-report=term-missing \
--cov-report=xml:coverage-unit.xml \
--cov-fail-under=${{ env.COVERAGE_THRESHOLD }} \
--junitxml=test-report-unit.xml \
-v
env:
DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
- name: Run integration tests
run: |
pytest tests/integration/ \
--cov=src \
--cov-report=term-missing \
--cov-report=xml:coverage-integration.xml \
--cov-append \
--junitxml=test-report-integration.xml \
-v
env:
DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
- name: Combine coverage reports
run: |
pip install coverage
coverage combine
coverage report --fail-under=${{ env.COVERAGE_THRESHOLD }}
coverage xml -o coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests,integration
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
coverage*.xml
test-report*.xml
# ============================================================
# STAGE 4: Build & Package
# ============================================================
build:
name: Build Package
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build package
run: |
pip install build
python -m build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: package
path: dist/
# ============================================================
# STAGE 5: Deploy (only on main branch push)
# ============================================================
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/download-artifact@v4
with:
name: package
path: dist/
- name: Deploy to production
run: |
echo "Deploying to production..."
# Example: scp dist/* user@server:/app/
# Example: docker push registry.example.com/myapp:latest
# Example: aws s3 sync dist/ s3://myapp-releases/
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
- name: Health check
run: |
echo "Running health check..."
# curl -f https://myapp.com/health || exit 1Pipeline Workflow Visualization
Quality Gates
Quality gates ensure that every change meets a minimum bar before merging:
# Quality gate configuration
name: Quality Gate
on:
pull_request_review:
types: [submitted]
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v7
with:
script: |
const requiredChecks = [
'Code Quality',
'Static Analysis',
'Tests & Coverage',
];
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.pull_request.head.sha,
});
const failed = checks.check_runs
.filter(c => requiredChecks.includes(c.name))
.filter(c => c.conclusion !== 'success');
if (failed.length > 0) {
core.setFailed(
`Required checks failed: ${failed.map(c => c.name).join(', ')}`
);
}Coverage Quality Gate Script
# scripts/check_coverage_gate.py
"""
Check if coverage meets the threshold.
Usage: python scripts/check_coverage_gate.py coverage.xml 85
"""
import sys
import xml.etree.ElementTree as ET
def check_coverage(coverage_file: str, threshold: float) -> bool:
tree = ET.parse(coverage_file)
root = tree.getroot()
total_lines = 0
covered_lines = 0
for package in root.findall(".//package"):
for cls in package.findall(".//class"):
lines = cls.find("lines")
if lines is not None:
for line in lines.findall("line"):
total_lines += 1
if line.get("hits") != "0":
covered_lines += 1
if total_lines == 0:
print("No lines found in coverage report")
return False
coverage_pct = (covered_lines / total_lines) * 100
print(f"Coverage: {coverage_pct:.2f}% (threshold: {threshold}%)")
if coverage_pct < threshold:
print(f"❌ FAILED: Coverage {coverage_pct:.2f}% < {threshold}%")
return False
print(f"✅ PASSED: Coverage {coverage_pct:.2f}% >= {threshold}%")
return True
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: check_coverage_gate.py <coverage.xml> <threshold>")
sys.exit(1)
success = check_coverage(sys.argv[1], float(sys.argv[2]))
sys.exit(0 if success else 1)Development Requirements
# requirements-dev.txt
# Testing
pytest>=8.0,<9.0
pytest-cov>=5.0,<6.0
pytest-xdist>=3.0,<4.0
pytest-asyncio>=0.21,<1.0
# Linting
ruff>=0.4,<1.0
flake8>=7.0,<8.0
flake8-docstrings>=1.7,<2.0
flake8-bugbear>=24.0,<25.0
# Formatting
black>=24.0,<25.0
isort>=5.13,<6.0
# Static analysis
mypy>=1.10,<2.0
bandit>=1.7,<2.0
types-requests>=2.31
types-PyYAML>=6.0
# Pre-commit
pre-commit>=3.7,<4.0
# Coverage
coverage>=7.0,<8.0
# Build
build>=1.0,<2.0
twine>=5.0,<6.0Makefile for Local Development
# Makefile
.PHONY: help setup install lint format typecheck security test coverage build clean
help:
@echo "Available commands:"
@echo " make setup - Install all dependencies"
@echo " make lint - Run all linters"
@echo " make format - Format all code"
@echo " make typecheck - Run mypy type checking"
@echo " make security - Run bandit security scan"
@echo " make test - Run all tests"
@echo " make coverage - Run tests with coverage report"
@echo " make build - Build distribution packages"
@echo " make clean - Remove build artifacts"
setup:
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pre-commit install
lint:
ruff check src/ tests/
flake8 src/ tests/ --statistics --max-line-length=100
format:
isort --profile black src/ tests/
black --line-length 100 src/ tests/
format-check:
isort --check-only --profile black src/ tests/
black --check --line-length 100 src/ tests/
typecheck:
mypy src/ --strict --ignore-missing-imports
security:
bandit -r src/ -ll
test:
pytest tests/ -v --cov=src
coverage:
pytest tests/ --cov=src --cov-report=term-missing --cov-report=html --cov-fail-under=85
build:
python -m pip install --upgrade build
python -m build
clean:
rm -rf dist/ build/ *.egg-info .coverage coverage.xml coverage_html/ __pycache__
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
all: lint format-check typecheck security test coverage
@echo "✅ All checks passed!"Environment-Specific Workflows
PR Workflow (Fast)
# .github/workflows/pr.yml
name: PR Checks
on: pull_request
jobs:
quick-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Quick checks
run: |
pip install ruff flake8 black isort mypy bandit
ruff check src/
black --check --diff src/
isort --check-only --diff --profile black src/
mypy src/ --ignore-missing-imports
bandit -r src/ -llMain Branch Workflow (Full)
# .github/workflows/main.yml
name: Main Branch CI/CD
on:
push:
branches: [main]
jobs:
full-pipeline:
name: Full Pipeline
uses: ./.github/workflows/ci.yml
secrets: inherit
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [full-pipeline]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build package
run: |
pip install build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [publish]
steps:
- uses: actions/checkout@v4
- uses: softprops/action-gh-release@v1
with:
generate_release_notes: truePipeline Metrics and Monitoring
| Stage | Duration | Pass Rate | What to Monitor |
|---|---|---|---|
| Quality | ~1-2 min | > 99% | Number of lint warnings |
| Static Analysis | ~2-3 min | > 99% | mypy errors, bandit findings |
| Tests | ~5-15 min | > 95% | Flaky tests, coverage trends |
| Build | ~1-2 min | > 99% | Build time, dependency changes |
| Deploy | ~2-5 min | > 99% | Deployment failures, rollbacks |
Practice Exercises
-
Create the Complete Workflow: Create a
.github/workflows/ci.ymlthat includes quality checks, static analysis, and testing. Push it to a GitHub repository and verify it runs. -
Add a Service Container: Modify the CI workflow to include a PostgreSQL or Redis service container. Update your integration tests to use this service.
-
Coverage Gate: Add a custom coverage gate that fails the build if coverage drops below 80%. Use the
coveragePython module to parse the XML report. -
Conditional Deploy: Create a workflow that only deploys when commits are pushed to the
mainbranch AND all previous stages pass. Add a manual approval step using environments. -
Parallel Jobs: Structure your workflow so that linting, type checking, and security scanning run in parallel, then tests run after all three complete. Measure the time savings.
-
Flaky Test Detection: Add a step that re-runs failed tests once to detect flaky tests. If a test passes on re-run, mark it as flaky but don't fail the build.
-
Build Matrix: Create a build matrix that runs tests against Python 3.10, 3.11, and 3.12. Ensure all versions pass before deployment.
-
Complete Project Setup: Set up a complete Python project with: pyproject.toml, pre-commit hooks, Makefile, full CI/CD pipeline, and README with build status badges.
Summary
- GitHub Actions provides a powerful, integrated CI/CD platform
- Multi-stage pipelines separate concerns: quality → analysis → test → build → deploy
- Quality gates at each stage prevent bad code from progressing
- Service containers enable real integration tests in CI
- Parallel execution speeds up feedback for independent tasks
- Conditional deployment ensures only tested, quality code reaches production
- Artifacts preserve reports and build outputs across workflow stages
- Local parity via Makefile ensures developers can reproduce CI checks locally
You've built a complete CI/CD pipeline. Every commit now runs through formatting, linting, type checking, security scanning, testing, coverage gates, and automated deployment. This is professional-grade software engineering in practice.