name: Deployment Pipeline on: push: branches: [ main, develop ] tags: [ 'v*' ] pull_request: types: [opened, synchronize, reopened] workflow_dispatch: inputs: environment: description: 'Target environment' required: true default: 'staging' type: choice options: - staging - production version: description: 'Version to deploy' required: false type: string env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: prepare-deployment: name: Prepare Deployment runs-on: ubuntu-latest outputs: environment: ${{ steps.env.outputs.environment }} version: ${{ steps.version.outputs.version }} should_deploy: ${{ steps.deployment-check.outputs.should_deploy }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Determine environment id: env run: | if [ "${{ github.ref }}" = "refs/heads/main" ]; then echo "environment=production" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then echo "environment=staging" >> $GITHUB_OUTPUT elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT else echo "environment=none" >> $GITHUB_OUTPUT fi - name: Determine version id: version run: | if [[ "${{ github.ref }}" =~ ^refs/tags/v ]]; then echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT else echo "version=${{ github.sha }}" >> $GITHUB_OUTPUT fi - name: Check deployment readiness id: deployment-check run: | if [ "${{ steps.env.outputs.environment }}" = "none" ]; then echo "should_deploy=false" >> $GITHUB_OUTPUT else echo "should_deploy=true" >> $GITHUB_OUTPUT fi build-and-test: name: Build and Test runs-on: ubuntu-latest needs: prepare-deployment if: needs.prepare-deployment.outputs.should_deploy == 'true' strategy: matrix: service: [backend, frontend] 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}} type=raw,value=${{ needs.prepare-deployment.outputs.version }} - name: Build ${{ matrix.service }} image uses: docker/build-push-action@v5 with: context: ${{ matrix.service }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }}-${{ matrix.service }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.prepare-deployment.outputs.version }} ENVIRONMENT=${{ needs.prepare-deployment.outputs.environment }} - name: Run security scan on ${{ matrix.service }} uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-deployment.outputs.version }}-${{ matrix.service }} format: 'sarif' output: 'trivy-${{ matrix.service }}.sarif' - name: Upload Trivy scan results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-${{ matrix.service }}.sarif' pre-deployment-checks: name: Pre-deployment Checks runs-on: ubuntu-latest needs: [prepare-deployment, build-and-test] steps: - name: Checkout code uses: actions/checkout@v4 - name: Run database migration check env: DATABASE_URL: ${{ secrets[format('{0}_DATABASE_URL', upper(needs.prepare-deployment.outputs.environment))] }} run: | python scripts/check-migrations.py - name: Validate configuration run: | python scripts/validate-config.py --environment ${{ needs.prepare-deployment.outputs.environment }} - name: Check deployment prerequisites run: | python scripts/deployment-prerequisites.py --environment ${{ needs.prepare-deployment.outputs.environment }} - name: Generate deployment manifest run: | python scripts/generate-deployment-manifest.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --version ${{ needs.prepare-deployment.outputs.version }} \ --output deployment-manifest.json - name: Upload deployment manifest uses: actions/upload-artifact@v3 with: name: deployment-manifest path: deployment-manifest.json staging-deployment: name: Deploy to Staging runs-on: ubuntu-latest needs: [prepare-deployment, pre-deployment-checks] if: needs.prepare-deployment.outputs.environment == 'staging' environment: name: staging url: https://staging.malaysian-sme-platform.com steps: - name: Checkout code uses: actions/checkout@v4 - name: Download deployment manifest uses: actions/download-artifact@v3 with: name: deployment-manifest - 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 # Backup current deployment ./scripts/backup-deployment.sh staging # Pull new images docker-compose -f docker-compose.staging.yml pull # Deploy with blue-green strategy docker-compose -f docker-compose.staging-bluegreen.yml up -d # Wait for health checks ./scripts/wait-for-health.sh staging # Switch traffic docker-compose -f docker-compose.staging.yml down docker-compose -f docker-compose.staging.yml up -d # Run database migrations docker-compose -f docker-compose.staging.yml exec -T backend python manage.py migrate # Collect static files docker-compose -f docker-compose.staging.yml exec -T backend python manage.py collectstatic --noinput # Clean up docker system prune -f - name: Run post-deployment tests run: | python scripts/post-deployment-tests.py --environment staging - name: Verify deployment run: | curl -f https://staging.malaysian-sme-platform.com/health/ || exit 1 curl -f https://staging.malaysian-sme-platform.com/api/health/ || exit 1 - name: Send deployment notification uses: 8398a7/action-slack@v3 with: status: success channel: '#deployments' webhook_url: ${{ secrets.SLACK_WEBHOOK }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} production-deployment: name: Deploy to Production runs-on: ubuntu-latest needs: [prepare-deployment, pre-deployment-checks] if: needs.prepare-deployment.outputs.environment == 'production' environment: name: production url: https://api.malaysian-sme-platform.com steps: - name: Checkout code uses: actions/checkout@v4 - name: Create production deployment uses: actions/create-deployment@v1 id: deployment with: token: ${{ secrets.GITHUB_TOKEN }} environment: production ref: ${{ github.sha }} - name: Download deployment manifest uses: actions/download-artifact@v3 with: name: deployment-manifest - name: Create backup before deployment run: | ssh -i ${{ secrets.PRODUCTION_SSH_KEY }} ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} \ 'cd /opt/malaysian-sme-platform && ./scripts/backup-database.sh' - 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: | cd /opt/malaysian-sme-platform # Pre-deployment checks ./scripts/pre-deployment-checks.sh production # Rolling deployment docker-compose -f docker-compose.prod.yml pull # Deploy backend first docker-compose -f docker-compose.prod.yml up -d --no-deps backend sleep 30 # Verify backend health ./scripts/wait-for-backend-health.sh # Deploy frontend docker-compose -f docker-compose.prod.yml up -d --no-deps frontend sleep 15 # Run database migrations docker-compose -f docker-compose.prod.yml exec -T backend python manage.py migrate --noinput # Collect static files docker-compose -f docker-compose.prod.yml exec -T backend python manage.py collectstatic --noinput # Update remaining services docker-compose -f docker-compose.prod.yml up -d # Post-deployment verification ./scripts/post-deployment-verification.sh - name: Run production smoke tests run: | python scripts/smoke-tests.py --environment production - name: Verify deployment run: | curl -f https://api.malaysian-sme-platform.com/health/ || exit 1 curl -f https://app.malaysian-sme-platform.com/ || exit 1 curl -f https://admin.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 }} - name: Send deployment notification if: always() uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} channel: '#deployments' webhook_url: ${{ secrets.SLACK_WEBHOOK }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} rollback-protection: name: Rollback Protection runs-on: ubuntu-latest needs: [staging-deployment, production-deployment] if: always() && (needs.staging-deployment.result == 'failure' || needs.production-deployment.result == 'failure') steps: - name: Initiate rollback run: | python scripts/initiate-rollback.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --failure-reason "Deployment failed" \ --rollback-to ${{ github.event.before }} - name: Send rollback notification uses: 8398a7/action-slack@v3 with: status: failure channel: '#emergency' webhook_url: ${{ secrets.EMERGENCY_SLACK_WEBHOOK }} env: SLACK_WEBHOOK_URL: ${{ secrets.EMERGENCY_SLACK_WEBHOOK }} deployment-verification: name: Deployment Verification runs-on: ubuntu-latest needs: [staging-deployment, production-deployment] if: always() && (needs.staging-deployment.result == 'success' || needs.production-deployment.result == 'success') steps: - name: Run end-to-end tests run: | python scripts/e2e-tests.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --timeout 300 - name: Performance validation run: | python scripts/performance-validation.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --threshold-percent 10 - name: Security validation run: | python scripts/security-validation.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} - name: Generate deployment report run: | python scripts/generate-deployment-report.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --version ${{ needs.prepare-deployment.outputs.version }} \ --output deployment-report.html - name: Upload deployment report uses: actions/upload-artifact@v3 with: name: deployment-report path: deployment-report.html cleanup: name: Cleanup runs-on: ubuntu-latest needs: [deployment-verification, rollback-protection] if: always() steps: - name: Cleanup old Docker images run: | ssh -i ${{ secrets.PRODUCTION_SSH_KEY }} ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} \ 'docker system prune -f --filter "until=72h"' - name: Cleanup old backups run: | ssh -i ${{ secrets.PRODUCTION_SSH_KEY }} ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} \ 'find /backups -name "*.sql" -mtime +7 -delete' - name: Update deployment metrics run: | python scripts/update-deployment-metrics.py \ --environment ${{ needs.prepare-deployment.outputs.environment }} \ --status ${{ job.status }} \ --duration ${{ job.duration }}