diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ae615c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,228 @@ +name: CI + +on: + push: + branches: [main, develop] + tags: + - 'v*' + pull_request: + branches: [main, develop] + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday (security scans) + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # ── Phase 1: Lint ────────────────────────────────────────────────────── + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python linting tools + run: | + python -m pip install --upgrade pip + pip install black ruff mypy + pip install -r backend/requirements.txt + + - name: Check code formatting with Black + run: black --check backend/ + + - name: Lint with Ruff + run: ruff check backend/ + + - name: Type check with mypy + run: mypy backend/app --ignore-missing-imports + continue-on-error: true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + run: npm ci + working-directory: frontend + + - name: Lint frontend with ESLint + run: npm run lint + working-directory: frontend + + # ── Phase 2: Test ────────────────────────────────────────────────────── + test: + name: Test + needs: lint + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: pop3_forwarder_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + pip install pytest pytest-asyncio pytest-cov httpx + + - name: Run tests with coverage + env: + DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/pop3_forwarder_test + REDIS_URL: redis://localhost:6379/0 + SECRET_KEY: test-secret-key-for-ci-cd-at-least-32-chars + ENCRYPTION_KEY: test-encryption-key-for-ci-cd-at-least-32-chars + run: | + cd backend + pytest tests/ -v --cov=app --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./backend/coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + # ── Phase 3: Security ────────────────────────────────────────────────── + security: + name: Security + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install bandit safety + pip install -r backend/requirements.txt + + - name: Run Bandit security scan + run: bandit -r backend/app -ll + continue-on-error: true + + - name: Check dependencies for known vulnerabilities + run: safety check --json + continue-on-error: true + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python, javascript-typescript + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + # ── Phase 4: Build ───────────────────────────────────────────────────── + build: + name: Build & Push Docker Image + needs: security + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + attestations: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + id: build-push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build-push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index dee9a2c..0000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build and Push Docker Image - -on: - push: - branches: - - main - - master - tags: - - 'v*' - pull_request: - branches: - - main - - master - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Docker image - id: build - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - - - name: Generate artifact attestation - if: github.event_name != 'pull_request' - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.build.outputs.digest }} - push-to-registry: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 54c7747..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Code Quality - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - lint: - runs-on: ubuntu-latest - - permissions: - contents: read - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black ruff mypy - pip install -r backend/requirements.txt - - - name: Check code formatting with Black - run: | - black --check backend/ - - - name: Lint with Ruff - run: | - ruff check backend/ - - - name: Type check with mypy - run: | - mypy backend/app --ignore-missing-imports - continue-on-error: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index c2cd1b2..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Security Scan - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - schedule: - - cron: '0 0 * * 0' # Weekly on Sunday - -jobs: - security: - runs-on: ubuntu-latest - - permissions: - contents: read - security-events: write # For CodeQL - actions: read - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install bandit safety - pip install -r backend/requirements.txt - - - name: Run Bandit security scan - run: | - bandit -r backend/app -ll - continue-on-error: true - - - name: Check dependencies for known vulnerabilities - run: | - safety check --json - continue-on-error: true - - - name: Run CodeQL Analysis - uses: github/codeql-action/init@v3 - with: - languages: python - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index b02acb4..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Tests - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - test: - runs-on: ubuntu-latest - - permissions: - contents: read - pull-requests: write # For coverage comments - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: pop3_forwarder_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r backend/requirements.txt - pip install pytest pytest-asyncio pytest-cov httpx - - - name: Run tests with coverage - env: - DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/pop3_forwarder_test - REDIS_URL: redis://localhost:6379/0 - SECRET_KEY: test-secret-key-for-ci-cd-at-least-32-chars - ENCRYPTION_KEY: test-encryption-key-for-ci-cd-at-least-32-chars - run: | - cd backend - pytest tests/ -v --cov=app --cov-report=xml --cov-report=term - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./backend/coverage.xml - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false