name: CI/CD Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: test: name: Run Tests runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: test_db 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 strategy: matrix: python-version: [3.9, 3.10, 3.11] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Cache pip packages uses: actions/cache@v3 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.txt pip install -r requirements-dev.txt pip install coveralls - name: Set up environment run: | cp backend/.env.example backend/.env cp frontend/.env.example frontend/.env chmod +x backend/manage.py - name: Run database migrations run: | cd backend python manage.py migrate env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db REDIS_URL: redis://localhost:6379/0 - name: Run backend tests run: | cd backend python manage.py test --verbosity=2 --cov=. --cov-report=xml --cov-report=term-missing env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db REDIS_URL: redis://localhost:6379/0 SECRET_KEY: test-secret-key-for-ci - name: Run frontend tests run: | cd frontend npm install npm run test npm run build - name: Run integration tests run: | cd backend python manage.py test tests.integration --verbosity=2 env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db REDIS_URL: redis://localhost:6379/0 - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: unittests name: codecov-umbrella security: name: Security Scan runs-on: ubuntu-latest needs: test steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Bandit Security Scan uses: PyCQA/bandit-action@v1 with: path: backend config: .bandit - name: Run Safety Check run: | pip install safety safety check -r requirements.txt - name: Run Semgrep Security Scan uses: returntocorp/semgrep-action@v1 with: config: p/security-audit paths: backend code-quality: name: Code Quality runs-on: ubuntu-latest needs: test steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.10 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Run Black formatting check run: | black --check backend/ - name: Run Flake8 linting run: | flake8 backend/ - name: Run isort import sorting check run: | isort --check-only backend/ - name: Run MyPy type checking run: | mypy backend/ --ignore-missing-imports - name: Run ESLint for frontend run: | cd frontend npm install npm run lint build-and-push: name: Build and Push Images runs-on: ubuntu-latest needs: [test, security, code-quality] if: github.ref == 'refs/heads/main' permissions: contents: read packages: write steps: - name: Checkout code 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 id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: Build backend image uses: docker/build-push-action@v5 with: context: backend platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }}-backend labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Build frontend image uses: docker/build-push-action@v5 with: context: frontend platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }}-frontend labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max staging-deploy: name: Deploy to Staging runs-on: ubuntu-latest needs: build-and-push if: github.ref == 'refs/heads/develop' environment: name: staging url: https://staging.malaysian-sme-platform.com steps: - name: Checkout code uses: actions/checkout@v4 - name: Deploy to staging uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.STAGING_HOST }} username: ${{ secrets.STAGING_USER }} key: ${{ secrets.STAGING_SSH_KEY }} script: | cd /opt/malaysian-sme-platform docker-compose -f docker-compose.staging.yml pull docker-compose -f docker-compose.staging.yml up -d docker system prune -f - name: Run health checks run: | curl -f https://staging.malaysian-sme-platform.com/health/ || exit 1 curl -f https://staging.malaysian-sme-platform.com/api/health/ || exit 1 production-deploy: name: Deploy to Production runs-on: ubuntu-latest needs: build-and-push if: github.ref == 'refs/heads/main' environment: name: production url: https://api.malaysian-sme-platform.com steps: - name: Checkout code uses: actions/checkout@v4 - name: Create GitHub deployment uses: actions/create-deployment@v1 id: deployment with: token: ${{ secrets.GITHUB_TOKEN }} environment: production - name: Deploy to production uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} script: | # Create backup before deployment ./scripts/backup-database.sh # Deploy with zero downtime cd /opt/malaysian-sme-platform # Pull new images docker-compose -f docker-compose.prod.yml pull # Perform rolling update docker-compose -f docker-compose.prod.yml up -d --no-deps backend sleep 30 docker-compose -f docker-compose.prod.yml up -d --no-deps frontend # Run database migrations docker-compose -f docker-compose.prod.yml exec -T backend python manage.py migrate # Collect static files docker-compose -f docker-compose.prod.yml exec -T backend python manage.py collectstatic --noinput # Clean up docker system prune -f - name: Run production health checks run: | curl -f https://api.malaysian-sme-platform.com/health/ || exit 1 curl -f https://api.malaysian-sme-platform.com/api/health/ || exit 1 curl -f https://app.malaysian-sme-platform.com/ || exit 1 - name: Update deployment status uses: actions/update-deployment@v1 if: always() with: token: ${{ secrets.GITHUB_TOKEN }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} state: ${{ job.status }} performance-test: name: Performance Testing runs-on: ubuntu-latest needs: staging-deploy if: github.ref == 'refs/heads/develop' steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up k6 uses: grafana/k6-action@v0.3.0 with: filename: tests/performance/load-test.js - name: Run performance tests run: | cd tests/performance k6 run load-test.js --env STAGING_URL=https://staging.malaysian-sme-platform.com - name: Upload performance results uses: actions/upload-artifact@v3 with: name: performance-results path: tests/performance/results/ notify: name: Notify Team runs-on: ubuntu-latest needs: [production-deploy, performance-test] if: always() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') steps: - name: Send Slack notification uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} channel: '#deployments' webhook_url: ${{ secrets.SLACK_WEBHOOK }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - name: Send email notification uses: dawidd6/action-send-mail@v3 with: server_address: smtp.gmail.com server_port: 465 username: ${{ secrets.EMAIL_USERNAME }} password: ${{ secrets.EMAIL_PASSWORD }} subject: "Deployment ${{ job.status }} - ${{ github.repository }}" body: | Deployment to ${{ github.ref }} completed with status: ${{ job.status }} Commit: ${{ github.sha }} Author: ${{ github.actor }} View logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} to: devops@malaysian-sme-platform.com from: ci-cd@malaysian-sme-platform.com