CI Test Pipeline — Automated Testing on Every Commit

A CI pipeline that runs tests on every commit is the safety net that makes continuous deployment safe. Without it, “it works on my machine” becomes the only assurance you have before deploying. The blog application’s CI pipeline runs all three test layers: backend pytest (fast, in parallel with frontend), frontend Vitest (fast), and Playwright E2E (slow, run only on pushes to main or on pull requests). Coverage reports are uploaded as CI artifacts so the team can review coverage trends over time.

GitHub Actions CI Pipeline

# .github/workflows/ci.yml
name: CI

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

jobs:
  # ── Backend tests ─────────────────────────────────────────────────────────
  backend:
    name: Backend Tests
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER:     test_user
          POSTGRES_PASSWORD: test_pass
          POSTGRES_DB:       blog_test
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-retries 5

    env:
      DATABASE_URL: postgresql://test_user:test_pass@localhost/blog_test
      SECRET_KEY:   ci-secret-key-not-for-production
      ENVIRONMENT:  test

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }

      - name: Cache pip
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}

      - run: pip install -r requirements.txt

      - name: Run backend tests with coverage
        run: |
          pytest \
            --cov=app \
            --cov-report=xml:coverage-backend.xml \
            --cov-report=term-missing \
            --cov-fail-under=80 \
            -v

      - name: Upload backend coverage
        uses: actions/upload-artifact@v4
        with:
          name: backend-coverage
          path: coverage-backend.xml

  # ── Frontend unit + component tests ──────────────────────────────────────
  frontend:
    name: Frontend Tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: frontend

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: "20" }

      - name: Cache node_modules
        uses: actions/cache@v4
        with:
          path: frontend/node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('frontend/package-lock.json') }}

      - run: npm ci

      - name: Run Vitest with coverage
        run: |
          npx vitest run \
            --coverage \
            --coverage.reporter=lcov \
            --coverage.reporter=text

      - name: Upload frontend coverage
        uses: actions/upload-artifact@v4
        with:
          name: frontend-coverage
          path: frontend/coverage/lcov.info

  # ── E2E tests (runs after backend + frontend pass) ────────────────────────
  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: [backend, frontend]   # only run if unit tests pass
    if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_pass
          POSTGRES_DB: blog_e2e
        ports: ["5432:5432"]

    env:
      DATABASE_URL:        postgresql://test_user:test_pass@localhost/blog_e2e
      SECRET_KEY:          ci-e2e-secret-key
      TEST_USER_EMAIL:     e2e@example.com
      TEST_USER_PASSWORD:  Password1

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: "20" }

      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }

      - run: pip install -r requirements.txt
      - run: npm ci
        working-directory: frontend

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
        working-directory: frontend

      - name: Run Playwright E2E tests
        run: npx playwright test
        working-directory: frontend

      - name: Upload Playwright report
        if: failure()   # only upload on failure
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: frontend/playwright-report/
Note: The E2E job uses needs: [backend, frontend] to run only after both the backend and frontend unit test jobs pass. If pytest or Vitest fails, there is no point running the slower Playwright suite — the E2E tests would almost certainly fail too. This job ordering makes the CI pipeline fail fast: unit test failures are surfaced in ~2 minutes, before the 5-10 minute E2E run starts. The if condition further restricts E2E to pull requests and pushes to main, saving CI time on feature branch commits.
Tip: Add coverage badges to the project README using Codecov or Shields.io. After uploading coverage reports as CI artifacts, configure the service to post the coverage percentage on pull requests. Teams that display coverage publicly create a healthy incentive to maintain test quality. Add a coverage threshold to fail CI if coverage drops below a minimum: --cov-fail-under=80 for pytest and --coverage.thresholds.lines=80 for Vitest.
Warning: Never use production credentials, real API keys, or the production database in CI. CI environment variables should be strictly test-only: a separate test database, a fake secret key, test email credentials. The SECRET_KEY: ci-secret-key-not-for-production in the workflow above is intentionally obvious — real production secrets are in GitHub repository secrets and referenced as ${{ secrets.PRODUCTION_SECRET_KEY }} only in deployment workflows, never in test workflows.

Package.json Test Scripts

// frontend/package.json scripts
{
  "scripts": {
    "test":           "vitest",
    "test:run":       "vitest run",
    "test:coverage":  "vitest run --coverage",
    "test:ui":        "vitest --ui",
    "test:e2e":       "playwright test",
    "test:e2e:ui":    "playwright test --ui",
    "test:e2e:debug": "playwright test --debug"
  }
}

Common Mistakes

Mistake 1 — Running E2E on every feature branch commit (too slow)

❌ Wrong — 10-minute E2E suite on every git push.

✅ Correct — restrict E2E to pull requests and main branch with the if condition.

Mistake 2 — Hardcoding production secrets in CI workflows

❌ Wrong — SECRET_KEY: myProductionSecret123 in a public GitHub Actions file.

✅ Correct — test-only throwaway values in workflow files; real secrets in GitHub Secrets.

🧠 Test Yourself

The backend tests pass in 90 seconds. The frontend Vitest tests pass in 30 seconds. The Playwright E2E tests take 8 minutes. With the needs: [backend, frontend] configuration, what is the total CI time for a pull request?