Skip to content

CI/CD Integration

Pipeline Design Principles

Tests in CI must be: - Fast — fast feedback prevents context switching - Reliable — false positives kill trust in the pipeline - Informative — failures must point to root cause immediately


Pipeline Stages

Push / PR
    │
    ▼
[1] Lint & Type Check    (ruff, mypy) — ~30s
    │
    ▼
[2] Unit Tests           (-m unit, no I/O) — ~1 min
    │
    ▼
[3] Integration Tests    (-m integration) — ~3 min
    │
    ▼
[4] API Tests            (-m api) — ~5 min
    │
    ▼
[5] Smoke E2E            (-m smoke and e2e) — ~3 min
    │
    ▼
[6] Report & Publish     (Allure, coverage badge)

Each stage gates the next. Linting failures block all test stages.


GitHub Actions Example

name: Test Suite

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv run ruff check .
      - run: uv run mypy .

  unit-tests:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv run pytest -m unit -n auto --junitxml=test-results/junit.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: unit-test-results
          path: test-results/

  api-tests:
    needs: unit-tests
    runs-on: ubuntu-latest
    env:
      TEST_ENV: staging
      TEST_API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: |
          uv run pytest -m api -n auto \
            --timeout=60 \
            --reruns=2 \
            --junitxml=test-results/junit.xml \
            --alluredir=test-results/allure-results
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-results
          path: test-results/

  e2e-smoke:
    needs: api-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv run playwright install chromium --with-deps
      - run: |
          uv run pytest -m "smoke and e2e" \
            --timeout=90 \
            --junitxml=test-results/e2e-junit.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: e2e-results
          path: test-results/

Quality Gates

Gates prevent merging code that degrades test quality.

Gate Threshold Action on Fail
Test pass rate 100% (excluding quarantined) Block merge
Coverage ≥ 90% (unit), ≥ 80% (integration) Block merge
Flakiness reruns ≤ 2% of total tests Warning
E2E duration ≤ 5 min for smoke suite Warning

Security Gates

Add security gates as first-class pipeline checks (not optional reporting).

Gate Tool Example Threshold Action on Fail
Secret scanning Gitleaks / TruffleHog 0 leaked secrets Block merge
Dependency vulnerabilities pip-audit / OSV-Scanner / Trivy 0 critical, 0 high in prod deps Block merge
SAST Semgrep / CodeQL 0 critical findings Block merge
SBOM generation CycloneDX / Syft SBOM artifact required Block release
# Example security job (GitHub Actions)
security-gates:
  needs: lint
  runs-on: ubuntu-latest
  permissions:
    contents: read
    security-events: write
  steps:
    - uses: actions/checkout@v4
    - uses: astral-sh/setup-uv@v3
    - name: Secrets scan
      run: uv run gitleaks detect --source . --verbose
    - name: Dependency scan
      run: uv run pip-audit --strict
    - name: SAST scan
      run: uv run semgrep --config auto --error
    - name: Generate SBOM
      run: uv run cyclonedx-py environment --output-file test-results/sbom.json
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: sbom
        path: test-results/sbom.json

Coverage Gate

uv run pytest --cov=src --cov-fail-under=90 -m "unit or integration"

Coverage Badge in README

- name: Generate coverage badge
  run: |
    uv run coverage-badge -o coverage.svg -f

Pre-Commit Integration

Catch issues before push — saves CI time:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
      - id: ruff-format

  - repo: local
    hooks:
      - id: unit-tests
        name: Unit Tests
        entry: uv run pytest -m unit -q
        language: system
        pass_filenames: false

Test Environment Provisioning

Tests are deterministic only when environments are deterministic. Use ephemeral environments per PR and bootstrap them from code.

Guidelines: - Create isolated preview environments for pull requests. - Pin runtime versions (Python, browser, DB image, migration version). - Apply migrations and seed baseline test data on each run. - Reset test data per run (truncate/recreate schema or rollback snapshot). - Destroy preview environments after pipeline completion.

# Ephemeral environment sketch
preview-env:
  if: github.event_name == 'pull_request'
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: astral-sh/setup-uv@v3
    - name: Start stack
      run: docker compose -f docker-compose.test.yml up -d --wait
    - name: Bootstrap schema
      run: uv run alembic upgrade head
    - name: Seed deterministic baseline
      run: uv run python data/seed.py --env preview --deterministic
    - name: Run tests
      run: uv run pytest -m "integration or api"
    - name: Teardown stack
      if: always()
      run: docker compose -f docker-compose.test.yml down -v

Test Impact Analysis (TIA)

Run only relevant tests for fast PR feedback, but keep a safe fallback policy.

Recommended policy: - PR pipeline: run impacted unit/integration/API tests based on changed files. - If mapping confidence is low, run full unit + integration suites. - Always run smoke E2E for critical journeys. - Nightly: run full regression suite regardless of change scope.

# Example: changed files -> impacted pytest marks
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
uv run python scripts/select_tests.py --changed "$CHANGED_FILES" > .impacted_marks
MARK_EXPR=$(cat .impacted_marks)

if [ -z "$MARK_EXPR" ]; then
  uv run pytest -m "unit or integration" -n auto
else
  uv run pytest -m "$MARK_EXPR" -n auto
fi

Fallback triggers (force broader run): - Changes in framework/, conftest.py, shared fixtures, auth, or DB migrations. - Changes touching selectors/page objects used across multiple flows. - Any flaky or unstable impacted subset in two consecutive runs.


Nightly Full Suite

name: Nightly Full Regression

on:
  schedule:
    - cron: "0 2 * * *"  # 02:00 UTC daily

jobs:
  full-regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv run playwright install --with-deps
      - run: |
          uv run pytest -n auto \
            --timeout=120 \
            --reruns=1 \
            --alluredir=test-results/allure
      - name: Notify on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: '{"text":"Nightly regression failed: ${{ github.run_url }}"}'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}