Merge branch 'release' into feat/RosieOh #2
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 - Blue/Green Deployment | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: 'Deployment Environment' | |
| required: true | |
| default: 'staging' | |
| type: choice | |
| options: | |
| - staging | |
| - production | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| STAGING_HEALTH_URL: ${{ secrets.STAGING_HEALTH_URL }} | |
| PRODUCTION_HEALTH_URL: ${{ secrets.PRODUCTION_HEALTH_URL }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| security-events: write | |
| concurrency: | |
| group: carecode-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # =========================================== | |
| # Test & Build Job | |
| # =========================================== | |
| test-and-build: | |
| name: Test and Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup SSH key | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| - name: Cache Gradle packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| restore-keys: | | |
| ${{ runner.os }}-gradle- | |
| - name: Run tests | |
| run: ./gradlew clean test jacocoTestReport | |
| - name: Build application | |
| run: ./gradlew build -x test | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: application-jar | |
| path: build/libs/*.jar | |
| retention-days: 7 | |
| # =========================================== | |
| # Security Scan Job | |
| # =========================================== | |
| security-scan: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| needs: test-and-build | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup SSH key | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.PRODUCTION_SSH_PRIVATE_KEY }} | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@0.30.0 | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| # =========================================== | |
| # Build Docker Image Job | |
| # =========================================== | |
| build-docker: | |
| name: Build Docker Image | |
| runs-on: ubuntu-latest | |
| needs: [test-and-build, security-scan] | |
| if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' | |
| outputs: | |
| image-tag: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| 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 and push Docker image | |
| 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 | |
| # =========================================== | |
| # Deploy to Staging (Green) Job | |
| # =========================================== | |
| deploy-staging: | |
| name: Deploy to Staging (Green) | |
| runs-on: ubuntu-latest | |
| needs: build-docker | |
| if: github.ref == 'refs/heads/develop' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging') | |
| environment: staging | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Deploy to staging environment | |
| run: | | |
| test -n "${{ secrets.STAGING_DEPLOY_HOST }}" | |
| ssh -o StrictHostKeyChecking=no ${{ secrets.STAGING_DEPLOY_USER }}@${{ secrets.STAGING_DEPLOY_HOST }} \ | |
| "docker pull ${{ needs.build-docker.outputs.image-tag }} && docker stop carecode-staging || true && docker rm carecode-staging || true && docker run -d --name carecode-staging -p 8082:8082 --env-file /opt/carecode/.env ${{ needs.build-docker.outputs.image-tag }}" | |
| - name: Run health check | |
| run: | | |
| test -n "$STAGING_HEALTH_URL" | |
| for i in {1..20}; do | |
| if curl -fsS "$STAGING_HEALTH_URL/actuator/health" | grep -q '"status":"UP"'; then | |
| echo "Staging health check passed" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "Staging health check failed" | |
| exit 1 | |
| - name: Notify deployment status | |
| if: always() | |
| run: | | |
| echo "Staging deployment completed" | |
| # 슬랙, 이메일 등 알림 추가 | |
| # =========================================== | |
| # Deploy to Production (Blue/Green) Job | |
| # =========================================== | |
| deploy-production: | |
| name: Deploy to Production (Blue/Green) | |
| runs-on: ubuntu-latest | |
| needs: build-docker | |
| if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production') | |
| environment: production | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Determine deployment strategy | |
| id: strategy | |
| run: | | |
| CURRENT_ENV=$(curl -fsS "${{ secrets.PRODUCTION_ROUTER_STATUS_URL }}" || echo "blue") | |
| if [ "$CURRENT_ENV" = "blue" ]; then | |
| echo "target-env=green" >> $GITHUB_OUTPUT | |
| echo "current-env=blue" >> $GITHUB_OUTPUT | |
| else | |
| echo "target-env=blue" >> $GITHUB_OUTPUT | |
| echo "current-env=green" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Deploy to target environment | |
| run: | | |
| test -n "${{ secrets.PRODUCTION_DEPLOY_HOST }}" | |
| ssh -o StrictHostKeyChecking=no ${{ secrets.PRODUCTION_DEPLOY_USER }}@${{ secrets.PRODUCTION_DEPLOY_HOST }} \ | |
| "docker pull ${{ needs.build-docker.outputs.image-tag }} && docker stop carecode-${{ steps.strategy.outputs.target-env }} || true && docker rm carecode-${{ steps.strategy.outputs.target-env }} || true && docker run -d --name carecode-${{ steps.strategy.outputs.target-env }} -p 8082:8082 --env-file /opt/carecode/.env ${{ needs.build-docker.outputs.image-tag }}" | |
| - name: Wait for deployment to be ready | |
| run: | | |
| test -n "$PRODUCTION_HEALTH_URL" | |
| for i in {1..20}; do | |
| if curl -fsS "$PRODUCTION_HEALTH_URL/actuator/health" | grep -q '"status":"UP"'; then | |
| echo "Production target is healthy" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "Production health check failed" | |
| exit 1 | |
| - name: Switch traffic to new environment | |
| run: | | |
| test -n "${{ secrets.PRODUCTION_ROUTER_SWITCH_URL }}" | |
| curl -fsS -X POST "${{ secrets.PRODUCTION_ROUTER_SWITCH_URL }}" \ | |
| -H "Authorization: Bearer ${{ secrets.PRODUCTION_ROUTER_TOKEN }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"target\":\"${{ steps.strategy.outputs.target-env }}\"}" | |
| - name: Verify deployment | |
| run: | | |
| curl -fsS "$PRODUCTION_HEALTH_URL/actuator/health" | grep -q '"status":"UP"' | |
| - name: Rollback if needed | |
| if: failure() | |
| run: | | |
| curl -fsS -X POST "${{ secrets.PRODUCTION_ROUTER_SWITCH_URL }}" \ | |
| -H "Authorization: Bearer ${{ secrets.PRODUCTION_ROUTER_TOKEN }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"target\":\"${{ steps.strategy.outputs.current-env }}\"}" | |
| - name: Notify deployment status | |
| if: always() | |
| run: | | |
| echo "Production deployment completed" | |
| # 슬랙, 이메일 등 알림 추가 | |
| # =========================================== | |
| # Cleanup Job | |
| # =========================================== | |
| cleanup: | |
| name: Cleanup | |
| runs-on: ubuntu-latest | |
| needs: [deploy-staging, deploy-production] | |
| if: always() | |
| steps: | |
| - name: Cleanup old images | |
| run: | | |
| echo "Cleaning up old Docker images" | |
| # 오래된 Docker 이미지 정리 | |
| - name: Cleanup old deployments | |
| run: | | |
| echo "Cleaning up old deployments" | |
| # 오래된 배포 정리 |