diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 918e0e48..96e3db25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,24 @@ jobs: - name: Build run: ./gradlew assemble --no-daemon + # 배포(deploy.yml)가 이 JAR 을 그대로 받아쓰도록 업로드해, deploy 단계의 재빌드(중복)를 없앤다. + # deploy 는 push CI 의 workflow_run 으로만 트리거되므로(PR 은 배포 안 됨) push 일 때만 올린다. + # 보존 3일이 안전한 이유: 아티팩트는 이 CI 직후(수분 내) 그 workflow_run 배포가 소비하므로 3일은 큰 여유다. + # 롤백·재배포도 옛 아티팩트에 기대지 않는다 - 자동배포는 새 push 라 늘 새 아티팩트, 수동 재배포는 + # 러너 빌드 fallback(deploy.yml), blue-green 롤백은 돌던 컨테이너 유지. 누적 방지 겸 짧게 둔다. + # JAR 이 없으면 즉시 실패시킨다. + - name: Upload JAR artifact + if: github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: app-jar + path: build/libs/*.jar + retention-days: 3 + if-no-files-found: error + # JAR 은 이미 압축된 zip 이라 기본 level 6 재압축은 크기 이득 ~0 에 CPU 만 쓴다. + # store(0)로 헛수고를 없애 업로드를 줄인다. CI 는 deploy 가 workflow_run 으로 기다리는 크리티컬 패스다. + compression-level: 0 + test: runs-on: ubuntu-latest if: github.event_name == 'pull_request' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a731c86d..2a86e38a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -87,6 +87,8 @@ jobs: environment: ${{ needs.resolve.outputs.environment }} permissions: contents: read + # 자동 배포 시 트리거한 CI 실행의 JAR 아티팩트를 크로스런 다운로드하려면 actions:read 가 필요하다. + actions: read # dev/prod 가 서로 배포를 막지 않도록 환경별 동시성 그룹. 같은 환경 내 재배포는 직렬화(취소 안 함). # 그룹은 브랜치명이 아니라 "환경"으로 묶어야 한다 — feature 브랜치 수동배포(dispatch)도 dev EC2 로 # 가므로 dev 자동배포와 같은 그룹이어야 같은 박스에서 동시 blue-green 조작 레이스를 막는다. @@ -141,13 +143,30 @@ jobs: blocks sparse-checkout-cone-mode: true - - name: Set up JDK 25 + # 자동 배포(workflow_run): 이 배포를 트리거한 CI 실행이 이미 빌드·업로드한 JAR 을 그대로 받아 + # 러너에서의 재빌드(약 47초)를 없앤다. run-id 로 그 CI 실행에 못박아, 배포 커밋과 다른 커밋의 JAR 을 + # 집는 레이스를 차단한다(그냥 '최신' 아티팩트를 집으면 배포 도중 다른 push 로 커밋 불일치가 난다). + # 자동 경로는 러너에 JDK·Gradle 이 필요 없다 — 도커 빌드의 layertools extract 는 이미지 안에서 돈다. + - name: Download JAR from triggering CI run + id: fetch_jar + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: app-jar + path: build/libs + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # 수동 배포(workflow_dispatch): 트리거한 CI 실행이 없어 아티팩트가 없다. 이때만 러너에서 직접 빌드한다. + - name: Set up JDK 25 (manual dispatch fallback) + if: github.event_name == 'workflow_dispatch' uses: actions/setup-java@v4 with: java-version: '25' distribution: 'temurin' - - name: Cache Gradle + - name: Cache Gradle (manual dispatch fallback) + if: github.event_name == 'workflow_dispatch' uses: actions/cache@v4 with: path: | @@ -156,8 +175,9 @@ jobs: key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: gradle-${{ runner.os }}- - - name: Build JAR + - name: Build JAR (manual dispatch fallback) id: build + if: github.event_name == 'workflow_dispatch' run: ./gradlew bootJar --no-daemon - name: Login to Docker Hub @@ -167,20 +187,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - + # 러너가 arm64 네이티브(EC2 Graviton 과 동일 arch)라 크로스빌드가 불필요하다. buildx 없이 기본 docker + # 데몬으로 arm64 이미지를 네이티브 빌드해 setup-buildx(약 10초)를 없앤다. 태그·image ref 는 셸 보간 + # injection 을 피해 env 로 받아 "$VAR" 로 쓴다(repo 컨벤션). - name: Build and push Docker image id: docker_push - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile.ci - push: true - platforms: linux/arm64 - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:latest - ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:${{ needs.resolve.outputs.ref }} + env: + IMAGE_LATEST: ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:latest + IMAGE_SHA: ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:${{ needs.resolve.outputs.ref }} + run: | + docker build -f Dockerfile.ci -t "$IMAGE_LATEST" -t "$IMAGE_SHA" . + docker push "$IMAGE_LATEST" + docker push "$IMAGE_SHA" - name: Upload nginx config id: upload_nginx @@ -647,6 +665,8 @@ jobs: STATUS="❌ 실패 (런타임 프로비저닝)" elif [ "${{ steps.build.outcome }}" = "failure" ]; then STATUS="❌ 실패 (Gradle 빌드)" + elif [ "${{ steps.fetch_jar.outcome }}" = "failure" ]; then + STATUS="❌ 실패 (CI 아티팩트 다운로드)" else STATUS="⚠️ 알 수 없음" fi @@ -660,7 +680,7 @@ jobs: echo "| 환경 | ${{ needs.resolve.outputs.environment }} |" echo "| 커밋 | [\`$SHORT_SHA\`]($COMMIT_URL) |" echo "| 현재 활성 슬롯 | $NOW |" - echo "| Gradle 빌드 | ${{ steps.build.outcome }} |" + echo "| JAR 준비 | ${{ github.event_name == 'workflow_run' && steps.fetch_jar.outcome || steps.build.outcome }} |" echo "| Docker 푸시 | ${{ steps.docker_push.outcome }} |" echo "| EC2 배포 | ${{ steps.deploy.outcome }} |" echo "| 헬스체크 & 전환 | ${{ steps.health.outcome }} |" @@ -743,6 +763,8 @@ jobs: CAUSE="${CAUSE:-환경 도메인↔EC2_HOST 불일치 (secret 오설정 — dev/prod 교차배선)}" elif [ "${{ steps.build.outcome }}" = "failure" ]; then CAUSE="Gradle 빌드 실패 (Build JAR)" + elif [ "${{ steps.fetch_jar.outcome }}" = "failure" ]; then + CAUSE="CI 아티팩트(JAR) 다운로드 실패 (트리거 CI 의 app-jar 누락/만료 · actions:read 권한)" elif [ "${{ steps.docker_login.outcome }}" = "failure" ]; then CAUSE="Docker Hub 로그인 실패" elif [ "${{ steps.docker_push.outcome }}" = "failure" ]; then diff --git a/build.gradle.kts b/build.gradle.kts index 074d62e9..9e1df7b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -137,3 +137,10 @@ tasks.withType { // JDK 21+부터 동적 에이전트 로딩이 제한됨. Mockito(ByteBuddy)가 런타임에 에이전트를 붙이므로 명시적 허용 필요. jvmArgs("-XX:+EnableDynamicAgentLoading") } + +// Spring Boot 플러그인은 실행 가능한 boot jar 와 라이브러리용 plain jar 를 함께 만든다. 이 앱은 라이브러리로 +// 쓰이지 않아 plain jar 가 불필요하고, build/libs 에 jar 가 2개면 배포·CI 의 `COPY build/libs/*.jar` 와 +// 아티팩트 전달이 어느 jar 인지 모호해져 깨진다. plain jar 를 꺼 산출물을 boot jar 하나로 고정한다. +tasks.named("jar") { + enabled = false +}