Fix 3 failing E2E workflow tests: filter race conditions and export t… #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| workflow_dispatch: | |
| inputs: | |
| rollback_tag: | |
| description: 'Tag or commit SHA to rollback to (e.g., backup-20240101-120000)' | |
| required: false | |
| type: string | |
| deploy_environment: | |
| description: 'Environment to deploy to' | |
| required: false | |
| type: choice | |
| options: | |
| - none | |
| - staging | |
| - production | |
| default: none | |
| jobs: | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_USER: dmarc_test | |
| POSTGRES_PASSWORD: test_password | |
| POSTGRES_DB: dmarc_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| working-directory: ./backend | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| - name: Run tests with coverage | |
| working-directory: ./backend | |
| env: | |
| DATABASE_URL: postgresql://dmarc_test:test_password@localhost:5432/dmarc_test | |
| RAW_REPORTS_PATH: /tmp/dmarc_reports | |
| DEBUG: false | |
| REQUIRE_API_KEY: false | |
| run: | | |
| pytest -v --cov=app --cov-report=xml --cov-report=term-missing | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: ./backend/coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| lint: | |
| name: Code Quality Checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install linting tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install flake8 black isort mypy | |
| - name: Run flake8 | |
| working-directory: ./backend | |
| run: | | |
| # Stop the build if there are Python syntax errors or undefined names | |
| flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics | |
| # Exit-zero treats all errors as warnings | |
| flake8 app --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics | |
| continue-on-error: true | |
| - name: Check code formatting with black | |
| working-directory: ./backend | |
| run: | | |
| black --check app | |
| continue-on-error: true | |
| - name: Check import sorting with isort | |
| working-directory: ./backend | |
| run: | | |
| isort --check-only app | |
| continue-on-error: true | |
| frontend-test: | |
| name: Frontend Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package.json | |
| - name: Install dependencies | |
| working-directory: ./frontend | |
| run: npm ci | |
| - name: Run tests with coverage | |
| working-directory: ./frontend | |
| run: npm run test:coverage | |
| - name: Upload coverage | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: ./frontend/coverage/lcov.info | |
| flags: frontend | |
| name: frontend-coverage | |
| fail_ci_if_error: false | |
| e2e-test: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| needs: [test, frontend-test] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install Playwright | |
| working-directory: ./e2e | |
| run: | | |
| npm ci | |
| npx playwright install --with-deps chromium | |
| - name: Start services | |
| run: | | |
| docker compose up -d | |
| sleep 30 | |
| - name: Run E2E tests | |
| working-directory: ./e2e | |
| run: npx playwright test --project=chromium | |
| env: | |
| BASE_URL: http://localhost:3000 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: playwright-report | |
| path: e2e/playwright-report/ | |
| retention-days: 7 | |
| - name: Stop services | |
| if: always() | |
| run: docker compose down | |
| security: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install security tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install safety bandit | |
| - name: Check for security vulnerabilities in dependencies | |
| working-directory: ./backend | |
| run: | | |
| safety check --file requirements.txt --json | |
| continue-on-error: true | |
| - name: Run bandit security linter | |
| working-directory: ./backend | |
| run: | | |
| bandit -r app -f json -o bandit-report.json | |
| continue-on-error: true | |
| - name: Upload bandit report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: bandit-security-report | |
| path: backend/bandit-report.json | |
| docker: | |
| name: Build Docker Images | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, frontend-test] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build backend image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./backend | |
| push: false | |
| tags: dmarc-backend:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build frontend image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./frontend | |
| push: false | |
| tags: dmarc-frontend:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Test docker-compose build | |
| run: | | |
| docker-compose build | |
| deploy-production: | |
| name: Deploy to Production | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, docker, e2e-test] | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| environment: production | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up SSH key | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts | |
| - name: Build and push Docker images | |
| env: | |
| COMMIT_SHA: ${{ github.sha }} | |
| run: | | |
| # Tag images with commit SHA and latest | |
| docker tag "dmarc-backend:${COMMIT_SHA}" dmarc-backend:latest | |
| docker tag "dmarc-frontend:${COMMIT_SHA}" dmarc-frontend:latest | |
| # Save images for deployment | |
| docker save dmarc-backend:latest | gzip > backend.tar.gz | |
| docker save dmarc-frontend:latest | gzip > frontend.tar.gz | |
| - name: Transfer images to production server | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| DEPLOY_USER: ${{ secrets.DEPLOY_USER }} | |
| run: | | |
| scp -i ~/.ssh/deploy_key \ | |
| backend.tar.gz frontend.tar.gz \ | |
| "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/" | |
| - name: Deploy to production server | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST }} | |
| username: ${{ secrets.DEPLOY_USER }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| set -e | |
| cd /opt/dmarc | |
| # Pull latest code | |
| git fetch origin main | |
| git checkout main | |
| git pull origin main | |
| # Load new images | |
| docker load < /tmp/backend.tar.gz | |
| docker load < /tmp/frontend.tar.gz | |
| rm /tmp/backend.tar.gz /tmp/frontend.tar.gz | |
| # Create backup tag before deployment | |
| BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)" | |
| docker tag dmarc-backend:latest "dmarc-backend:${BACKUP_TAG}" | |
| docker tag dmarc-frontend:latest "dmarc-frontend:${BACKUP_TAG}" | |
| # Pull and deploy with Docker Compose | |
| docker compose pull | |
| docker compose up -d --remove-orphans | |
| # Wait for health check | |
| echo "Waiting for services to be healthy..." | |
| sleep 30 | |
| # Verify deployment | |
| curl -f http://localhost:8000/health || exit 1 | |
| # Clean up old images (keep last 5) | |
| docker image prune -a -f --filter "until=720h" | |
| echo "Deployment completed successfully!" | |
| - name: Run post-deployment health checks | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST }} | |
| username: ${{ secrets.DEPLOY_USER }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| # Verify all services are running | |
| cd /opt/dmarc | |
| docker compose ps | |
| # Check backend health | |
| curl -f http://localhost:8000/health | |
| # Check Celery workers | |
| docker exec dmarc-celery-worker celery -A celery_worker inspect ping | |
| echo "Health checks passed!" | |
| - name: Notify deployment success | |
| if: success() | |
| env: | |
| COMMIT_SHA: ${{ github.sha }} | |
| ACTOR: ${{ github.actor }} | |
| run: | | |
| echo "Production deployment completed successfully!" | |
| echo "Commit: ${COMMIT_SHA}" | |
| echo "Deployed by: ${ACTOR}" | |
| # Add Slack/Discord/email notification here if desired | |
| - name: Notify deployment failure | |
| if: failure() | |
| run: | | |
| echo "Production deployment failed!" | |
| echo "Manual intervention required" | |
| # Add alert notification here | |
| deploy-staging: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, docker] | |
| if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main') | |
| environment: staging | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up SSH key | |
| env: | |
| STAGING_HOST: ${{ secrets.STAGING_HOST }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/staging_key | |
| chmod 600 ~/.ssh/staging_key | |
| ssh-keyscan -H "$STAGING_HOST" >> ~/.ssh/known_hosts | |
| - name: Build Docker images | |
| env: | |
| COMMIT_SHA: ${{ github.sha }} | |
| run: | | |
| # Build and tag images for staging | |
| docker build -t "dmarc-backend:staging-${COMMIT_SHA}" ./backend | |
| docker build -t "dmarc-frontend:staging-${COMMIT_SHA}" ./frontend | |
| # Tag as latest staging | |
| docker tag "dmarc-backend:staging-${COMMIT_SHA}" dmarc-backend:staging-latest | |
| docker tag "dmarc-frontend:staging-${COMMIT_SHA}" dmarc-frontend:staging-latest | |
| # Save images | |
| docker save dmarc-backend:staging-latest | gzip > backend-staging.tar.gz | |
| docker save dmarc-frontend:staging-latest | gzip > frontend-staging.tar.gz | |
| - name: Transfer images to staging server | |
| env: | |
| STAGING_HOST: ${{ secrets.STAGING_HOST }} | |
| STAGING_USER: ${{ secrets.STAGING_USER }} | |
| run: | | |
| scp -i ~/.ssh/staging_key \ | |
| backend-staging.tar.gz frontend-staging.tar.gz \ | |
| "${STAGING_USER}@${STAGING_HOST}:/tmp/" | |
| - name: Deploy to staging server | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.STAGING_HOST }} | |
| username: ${{ secrets.STAGING_USER }} | |
| key: ${{ secrets.STAGING_SSH_KEY }} | |
| script: | | |
| set -e | |
| cd /opt/dmarc-staging | |
| # Pull latest code | |
| git fetch origin | |
| git checkout ${{ github.sha }} | |
| # Load new images | |
| docker load < /tmp/backend-staging.tar.gz | |
| docker load < /tmp/frontend-staging.tar.gz | |
| rm /tmp/backend-staging.tar.gz /tmp/frontend-staging.tar.gz | |
| # Deploy with Docker Compose | |
| docker compose -f docker-compose.staging.yml up -d --remove-orphans | |
| # Wait for services | |
| sleep 20 | |
| # Verify deployment | |
| curl -f http://localhost:8000/health || exit 1 | |
| echo "Staging deployment completed!" | |
| - name: Post deployment URL | |
| env: | |
| STAGING_URL: ${{ secrets.STAGING_URL }} | |
| run: | | |
| echo "Staging deployment successful!" | |
| echo "URL: ${STAGING_URL}" | |
| echo "Commit: ${{ github.sha }}" | |
| rollback: | |
| name: Rollback Production | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_tag != '' | |
| environment: production | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Validate rollback target | |
| env: | |
| ROLLBACK_TAG: ${{ github.event.inputs.rollback_tag }} | |
| run: | | |
| if [ -z "$ROLLBACK_TAG" ]; then | |
| echo "Error: rollback_tag input is required" | |
| exit 1 | |
| fi | |
| echo "Rollback target: ${ROLLBACK_TAG}" | |
| - name: Set up SSH key | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts | |
| - name: Perform rollback | |
| uses: appleboy/ssh-action@v1.0.3 | |
| env: | |
| ROLLBACK_TAG: ${{ github.event.inputs.rollback_tag }} | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST }} | |
| username: ${{ secrets.DEPLOY_USER }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| envs: ROLLBACK_TAG | |
| script: | | |
| set -e | |
| cd /opt/dmarc | |
| echo "Starting rollback to: ${ROLLBACK_TAG}" | |
| # Verify the backup tag exists | |
| if ! docker image inspect "dmarc-backend:${ROLLBACK_TAG}" > /dev/null 2>&1; then | |
| echo "Error: Backup image dmarc-backend:${ROLLBACK_TAG} not found" | |
| exit 1 | |
| fi | |
| # Stop current services | |
| docker compose down | |
| # Tag backup images as latest | |
| docker tag "dmarc-backend:${ROLLBACK_TAG}" dmarc-backend:latest | |
| docker tag "dmarc-frontend:${ROLLBACK_TAG}" dmarc-frontend:latest | |
| # Checkout the corresponding git commit/tag | |
| git fetch --all --tags | |
| git checkout "${ROLLBACK_TAG}" || git checkout "tags/${ROLLBACK_TAG}" || echo "Git checkout skipped" | |
| # Start services with rollback images | |
| docker compose up -d | |
| # Wait for health check | |
| echo "Waiting for services to be healthy..." | |
| sleep 30 | |
| # Verify rollback | |
| curl -f http://localhost:8000/health || exit 1 | |
| echo "Rollback to ${ROLLBACK_TAG} completed successfully!" | |
| - name: Verify rollback | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST }} | |
| username: ${{ secrets.DEPLOY_USER }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| cd /opt/dmarc | |
| # Check all services | |
| docker compose ps | |
| # Health checks | |
| curl -f http://localhost:8000/health | |
| docker exec dmarc-celery-worker celery -A celery_worker inspect ping | |
| echo "Rollback verification passed!" | |
| - name: Notify rollback | |
| if: always() | |
| env: | |
| ROLLBACK_TAG: ${{ github.event.inputs.rollback_tag }} | |
| ACTOR: ${{ github.actor }} | |
| run: | | |
| if [ "${{ job.status }}" == "success" ]; then | |
| echo "Rollback to ${ROLLBACK_TAG} completed successfully!" | |
| else | |
| echo "Rollback to ${ROLLBACK_TAG} failed!" | |
| fi | |
| echo "Initiated by: ${ACTOR}" | |
| # Add alert notification here | |
| notification: | |
| name: Notify | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, docker] | |
| if: always() | |
| steps: | |
| - name: Send notification | |
| run: | | |
| echo "Pipeline completed with status: ${{ job.status }}" | |
| # Add Slack, Discord, or email notifications here if desired |