diff --git a/.github/workflows/prod-server.yml b/.github/workflows/prod-server.yml index 7b997a73..8e21f83f 100644 --- a/.github/workflows/prod-server.yml +++ b/.github/workflows/prod-server.yml @@ -3,7 +3,7 @@ name: Spring CD (Production) # main 브랜치 PR에서만 실행 (이미 빌드된 Docker 이미지 사용) on: - pull_request: + push: branches: - main paths: @@ -18,7 +18,7 @@ jobs: # ================================== # CD: Deploy to Production Environment # ================================== - cd-test: + cd-prod: runs-on: ubuntu-latest steps: @@ -60,7 +60,6 @@ jobs: echo "📋 Checking current NPM configuration... 📋" CURRENT_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") - echo "Current Config: $CURRENT_CONFIG" CURRENT_TARGET=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_host // .forward_host') CURRENT_PORT=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_port // .forward_port') diff --git a/.github/workflows/test-server-cd.yml b/.github/workflows/test-server-cd.yml new file mode 100644 index 00000000..9f32a143 --- /dev/null +++ b/.github/workflows/test-server-cd.yml @@ -0,0 +1,116 @@ +name: Spring CD (Test Server) + +on: + push: + branches: + - develop + +jobs: + cd-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to Test Environment + uses: appleboy/ssh-action@v0.1.7 + with: + host: ${{ secrets.TEST_SERVER_HOST }} + username: ec2-user + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + # GHCR 로그인 + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.repository_owner }}" --password-stdin + docker pull ghcr.io/${{ github.repository }}/zoopzoop:latest + + NPM_HOST="localhost:81" + NPM_EMAIL="${{ secrets.NPM_ADMIN_EMAIL }}" + NPM_PASSWORD="${{ secrets.NPM_ADMIN_PASSWORD }}" + PROXY_HOST_ID="${{ secrets.NPM_PROXY_HOST_ID }}" + + # NPM 토큰 + TOKEN=$(curl -s -X POST "http://${NPM_HOST}/api/tokens" \ + -H "Content-Type: application/json" \ + -d "{\"identity\":\"${NPM_EMAIL}\",\"secret\":\"${NPM_PASSWORD}\"}" | jq -r '.token') + if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then + echo "❌ Failed to get NPM API token" + exit 1 + fi + + CURRENT_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ + "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") + + CURRENT_TARGET=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_host // .forward_host') + CURRENT_PORT=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_port // .forward_port') + echo "Current NPM target: $CURRENT_TARGET:$CURRENT_PORT" + + if [ "$(docker ps -q -f name=zoopzoop-blue)" ]; then + NEW_CONTAINER=zoopzoop-green + OLD_CONTAINER=zoopzoop-blue + NEW_PORT=8082 + else + NEW_CONTAINER=zoopzoop-blue + OLD_CONTAINER=zoopzoop-green + NEW_PORT=8081 + fi + + docker run -d --restart unless-stopped \ + -p $NEW_PORT:8080 \ + --name $NEW_CONTAINER \ + --network common \ + -e SPRING_DATASOURCE_URL="${{secrets.TEST_DB_URL}}" \ + -e SPRING_DATASOURCE_USERNAME="${{secrets.TEST_DB_USERNAME}}" \ + -e SPRING_DATASOURCE_PASSWORD="${{secrets.TEST_DB_PASSWORD}}" \ + ghcr.io/${{ github.repository }}/zoopzoop:latest + + # 헬스체크 + for i in {1..30}; do + if curl -s http://localhost:$NEW_PORT/actuator/health | grep -q '"status":"UP"'; then + echo "✅ New container is healthy!" + break + else + echo "Waiting for new container to be healthy..." + sleep 5 + fi + if [ $i -eq 30 ]; then + echo "❌ Health check failed. Rolling back..." + docker stop $NEW_CONTAINER || true + docker rm $NEW_CONTAINER || true + exit 1 + fi + done + + # NPM 트래픽 스위칭 + DOMAIN_NAME=$(echo $CURRENT_CONFIG | jq -r '.domain_names[0]') + CERT_ID=$(echo "$CURRENT_CONFIG" | jq -r '.certificate_id') + + SWITCH_RESPONSE=$(curl -s -w "%{http_code}" -X PUT "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"domain_names\": [\"$DOMAIN_NAME\"], + \"forward_scheme\": \"http\", + \"forward_host\": \"$NEW_CONTAINER\", + \"forward_port\": 8080, + \"caching_enabled\": false, + \"block_exploits\": true, + \"advanced_config\": \"\", + \"locations\": [], + \"certificate_id\": $CERT_ID, + \"ssl_forced\": 1, + \"hsts_enabled\": 1, + \"hsts_subdomains\": 1 + }") + + HTTP_CODE=${SWITCH_RESPONSE: -3} + if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 201 ]; then + echo "❌ Traffic switching failed! HTTP Code: $HTTP_CODE" + echo "Response: ${SWITCH_RESPONSE%???}" + docker stop $NEW_CONTAINER || true + docker rm $NEW_CONTAINER || true + exit 1 + fi + + docker stop $OLD_CONTAINER || true + docker rm $OLD_CONTAINER || true diff --git a/.github/workflows/test-server-ci.yml b/.github/workflows/test-server-ci.yml new file mode 100644 index 00000000..c7c0e445 --- /dev/null +++ b/.github/workflows/test-server-ci.yml @@ -0,0 +1,108 @@ +# 워크플로우 이름 +name: Spring CI/CD Pipeline (Develop) + +# develop 브랜치 PR에서만 실행 +on: + pull_request: + branches: + - develop + paths: + - 'src/**' + - 'build.gradle*' + - 'settings.gradle*' + - 'gradle/**' + - 'Dockerfile' + - '.github/workflows/**' + +jobs: + # ================================== + # CI: Test and Build and Push Docker Image + # ================================== + ci: + runs-on: ubuntu-latest + + steps: + # 1. 소스 코드 체크아웃 + - name: Checkout source code + uses: actions/checkout@v4 + + # 2. JDK 21 설치 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # 3. Gradle 캐시 설정 + - 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- + + # 4. gradlew 실행 권한 부여 + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + # 5. application-secrets.yml 생성 + - name: Generate application-secrets.yml + run: | + mkdir -p src/main/resources + echo "${{ secrets.APPLICATION_SECRET_YML }}" > src/main/resources/application-secrets.yml + echo "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" >> src/main/resources/application-secrets.yml + + # 6. application-secrets-server.yml 생성 + - name: Generate application-secrets-server.yml + run: | + mkdir -p src/main/resources + echo "${{ secrets.APPLICATION_SECRET_SERVER_YML }}" > src/main/resources/application-secrets-server.yml + echo "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" >> src/main/resources/application-secrets-server.yml + + # 7. Gradle 테스트 실행 + - name: Test with Gradle + run: ./gradlew test + + # 8. 테스트 결과 요약 출력 + - name: Show test results + run: | + echo "==== Test Results ====" + if compgen -G "build/test-results/test/TEST-*.xml" > /dev/null; then + total=$(grep ' src/main/resources/application-secrets.yml - echo "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" >> src/main/resources/application-secrets.yml - - # 6. application-secrets-server.yml 생성 - - name: Generate application-secrets-server.yml - run: | - mkdir -p src/main/resources - echo "${{ secrets.APPLICATION_SECRET_SERVER_YML }}" > src/main/resources/application-secrets-server.yml - echo "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" >> src/main/resources/application-secrets-server.yml - - # 7. Gradle 테스트 실행 - - name: Test with Gradle - run: ./gradlew test - - # 8. 테스트 결과 요약 출력 - - name: Show test results - run: | - echo "==== Test Results ====" - if compgen -G "build/test-results/test/TEST-*.xml" > /dev/null; then - total=$(grep '