refactor(commerce): member·wishlist 흡수 — 6-도메인 모놀리스 (ADR-0058 R2) #123
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: images | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| workflow_dispatch: | |
| inputs: | |
| registry: | |
| description: "Override registry (default: <region>.ocir.io/<namespace>)" | |
| required: false | |
| default: "" | |
| rebuild_all: | |
| description: "변경 감지 무시하고 전 서비스 재빌드 (ADR-0058 commerce 폴드 후 JVM 17 + FE 8 = 25)" | |
| required: false | |
| default: "false" | |
| # Only one image-publish run at a time per ref. | |
| # cancel-in-progress: false — OCIR push 중 cancel 시 partial manifest 위험. | |
| # 대신 job-level timeout-minutes 로 hang 방지. | |
| concurrency: | |
| group: images-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # manifest commit-back | |
| jobs: | |
| publish: | |
| name: Build & push arm64 images to OCIR + update oci-arm manifest | |
| # arm64 native runner — QEMU 에뮬레이션 없음 → Node 24 V8 JIT 충돌 해소. | |
| # msa repo 가 public 이라 free unlimited. amd64 보다 5~10배 빠름. | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 60 # hang 시 6h default → 60m 로 단축 | |
| # `[skip ci]` prevent self-trigger on commit-back | |
| if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| # 최소권한: 이 워크플로가 접근하는 3개 repo 만 명시. | |
| # App 설치 페이지에 msa / msa-auth / msa-gifticon 셋 모두 있어야 동작. | |
| repositories: | | |
| msa | |
| msa-auth | |
| msa-gifticon | |
| - name: Checkout (root only) | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| submodules: false | |
| fetch-depth: 0 | |
| - name: Init build-required submodules (auth, gifticon) | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| git config --global \ | |
| url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf \ | |
| "https://github.com/" | |
| git submodule update --init --recursive -- auth gifticon | |
| # ───── 변경된 서비스 감지 ────────────────────────────────────────────── | |
| # 입력값: git diff HEAD~1..HEAD (직전 commit 대비) 또는 force rebuild. | |
| # 출력: jvm_services, docker_services, kustomize_services 의 space-separated 목록. | |
| # 공유 의존성(common/buildSrc/gradle 등) 변경 시 모든 backend 강제 rebuild. | |
| - name: Detect changed services | |
| id: changes | |
| env: | |
| REBUILD_ALL: ${{ github.event.inputs.rebuild_all }} | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| ALL_JVM="gateway product search search-batch search-consumer auth gifticon analytics experiment code-dictionary commerce chatbot quant recommendation agent-viewer-api" | |
| ALL_DOCKER="admin-fe gifticon-fe agent-viewer-fe portal-fe quant-fe quant-ingest recommendation-ml recommendation-ann" | |
| # 강제 rebuild 트리거 | |
| if [[ "$REBUILD_ALL" == "true" ]] \ | |
| || [[ "$GITHUB_EVENT_BEFORE" == "0000000000000000000000000000000000000000" ]] \ | |
| || [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "→ 전체 rebuild 모드 (force / first push / version tag)" | |
| echo "jvm_services=$ALL_JVM" >> "$GITHUB_OUTPUT" | |
| echo "docker_services=$ALL_DOCKER" >> "$GITHUB_OUTPUT" | |
| echo "kustomize_services=$ALL_JVM $ALL_DOCKER" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # before SHA 가 비어있거나 잘못된 경우 fallback | |
| if ! git cat-file -e "$GITHUB_EVENT_BEFORE" 2>/dev/null; then | |
| echo "→ before SHA unreachable, 전체 rebuild fallback" | |
| echo "jvm_services=$ALL_JVM" >> "$GITHUB_OUTPUT" | |
| echo "docker_services=$ALL_DOCKER" >> "$GITHUB_OUTPUT" | |
| echo "kustomize_services=$ALL_JVM $ALL_DOCKER" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| CHANGED_FILES=$(git diff --name-only "$GITHUB_EVENT_BEFORE" HEAD) | |
| echo "── 변경 파일 ($(echo "$CHANGED_FILES" | wc -l) 개):" | |
| echo "$CHANGED_FILES" | sed 's/^/ /' | |
| # 공유 의존성 변경 시 모든 JVM rebuild | |
| if echo "$CHANGED_FILES" | grep -qE '^(common/|buildSrc/|gradle/|build\.gradle\.kts$|settings\.gradle\.kts$|gradle\.properties$|gradle/libs\.versions\.toml$)'; then | |
| echo "→ 공유 의존성 변경 감지 — 전체 JVM rebuild" | |
| JVM_SVCS="$ALL_JVM" | |
| else | |
| JVM_SVCS="" | |
| fi | |
| DOCKER_SVCS="" | |
| # 경로별 서비스 매핑. | |
| # submodule pointer bump 는 path 가 'auth' (no slash) 로 나옴 → | |
| # 'auth' 와 'auth/*' 둘 다 매칭해야 함. gifticon 동일. | |
| while IFS= read -r file; do | |
| if [[ -z "$file" ]]; then | |
| continue | |
| fi | |
| case "$file" in | |
| gateway/*) JVM_SVCS+=" gateway" ;; | |
| product/*) JVM_SVCS+=" product" ;; | |
| search/app/*|search/domain/*) JVM_SVCS+=" search" ;; | |
| search/batch/*) JVM_SVCS+=" search-batch" ;; | |
| search/consumer/*) JVM_SVCS+=" search-consumer" ;; | |
| auth|auth/*) JVM_SVCS+=" auth" ;; | |
| gifticon|gifticon/app/*|gifticon/domain/*) JVM_SVCS+=" gifticon" ;; | |
| gifticon/frontend/*) DOCKER_SVCS+=" gifticon-fe" ;; | |
| inventory/*|warehouse/*|fulfillment/*|order/*|member/*|wishlist/*|commerce/*) JVM_SVCS+=" commerce" ;; | |
| analytics/*) JVM_SVCS+=" analytics" ;; | |
| experiment/*) JVM_SVCS+=" experiment" ;; | |
| code-dictionary/*) JVM_SVCS+=" code-dictionary" ;; | |
| chatbot/*) JVM_SVCS+=" chatbot" ;; | |
| quant/app/*|quant/domain/*|quant/glossary*)JVM_SVCS+=" quant" ;; | |
| quant/frontend/*) DOCKER_SVCS+=" quant-fe" ;; | |
| quant/ingest/*) DOCKER_SVCS+=" quant-ingest" ;; | |
| recommendation/app/*|recommendation/domain/*) JVM_SVCS+=" recommendation" ;; | |
| recommendation-ml/*) DOCKER_SVCS+=" recommendation-ml" ;; | |
| recommendation-ann/*) DOCKER_SVCS+=" recommendation-ann" ;; | |
| agent-viewer/api/*|agent-viewer/domain/*) JVM_SVCS+=" agent-viewer-api" ;; | |
| agent-viewer/front/*) DOCKER_SVCS+=" agent-viewer-fe" ;; | |
| admin/frontend/*) DOCKER_SVCS+=" admin-fe" ;; | |
| portal-fe/*) DOCKER_SVCS+=" portal-fe" ;; | |
| # k8s 매니페스트만 변경 → 빌드 무관 (Argo CD 가 직접 sync) | |
| # docs/ai/private/career/ideabank → 빌드 무관 | |
| esac | |
| done <<< "$CHANGED_FILES" | |
| # 중복 제거. 빌드 대상이 없을 때도 pipefail 에 걸리지 않아야 한다. | |
| normalize_services() { | |
| printf '%s\n' "$*" | tr ' ' '\n' | awk 'NF' | sort -u | tr '\n' ' ' | sed 's/ $//' | |
| } | |
| JVM_SVCS=$(normalize_services "$JVM_SVCS") | |
| DOCKER_SVCS=$(normalize_services "$DOCKER_SVCS") | |
| echo "→ 빌드 대상 JVM : ${JVM_SVCS:-없음}" | |
| echo "→ 빌드 대상 Docker : ${DOCKER_SVCS:-없음}" | |
| KUSTOMIZE_SVCS=$(normalize_services "$JVM_SVCS $DOCKER_SVCS") | |
| echo "→ manifest 갱신 대상: ${KUSTOMIZE_SVCS:-없음}" | |
| echo "jvm_services=$JVM_SVCS" >> "$GITHUB_OUTPUT" | |
| echo "docker_services=$DOCKER_SVCS" >> "$GITHUB_OUTPUT" | |
| echo "kustomize_services=$KUSTOMIZE_SVCS" >> "$GITHUB_OUTPUT" | |
| - name: 변경 없으면 조기 종료 | |
| if: ${{ steps.changes.outputs.kustomize_services == '' }} | |
| run: | | |
| echo "✓ 빌드 대상 서비스 없음 — 워크플로 종료" | |
| echo "변경된 파일이 image 빌드와 무관한 경로 (k8s/, docs/, etc.)" >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| - name: Set up JDK 25 | |
| if: ${{ steps.changes.outputs.jvm_services != '' }} | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| - name: Set up Gradle | |
| if: ${{ steps.changes.outputs.jvm_services != '' }} | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Set up Docker Buildx (native arm64) | |
| if: ${{ steps.changes.outputs.docker_services != '' }} | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: image=moby/buildkit:latest | |
| - name: Resolve registry + tag | |
| id: meta | |
| if: ${{ steps.changes.outputs.kustomize_services != '' }} | |
| env: | |
| OCIR_REGION: ${{ secrets.OCIR_REGION }} | |
| OCIR_NAMESPACE: ${{ secrets.OCIR_NAMESPACE }} | |
| run: | | |
| set -euo pipefail | |
| REGISTRY="${{ github.event.inputs.registry }}" | |
| if [[ -z "$REGISTRY" ]]; then | |
| if [[ -z "${OCIR_REGION}" || -z "${OCIR_NAMESPACE}" ]]; then | |
| echo "::error::OCIR_REGION / OCIR_NAMESPACE secrets must be set" | |
| exit 1 | |
| fi | |
| REGISTRY="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}" | |
| fi | |
| REGISTRY=$(echo "$REGISTRY" | tr '[:upper:]' '[:lower:]') | |
| TAG="${GITHUB_SHA::7}" | |
| if [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| fi | |
| echo "registry=$REGISTRY" | tee -a "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" | tee -a "$GITHUB_OUTPUT" | |
| - name: Authenticate to OCIR | |
| if: ${{ steps.changes.outputs.kustomize_services != '' }} | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ secrets.OCIR_REGION }}.ocir.io | |
| username: ${{ secrets.OCIR_USERNAME }} | |
| password: ${{ secrets.OCIR_TOKEN }} | |
| # ───── 테스트 게이트: 변경된 JVM 서비스만 (이미지 push 전 필수) ───────── | |
| - name: ./gradlew test (변경된 JVM 서비스만) | |
| if: ${{ steps.changes.outputs.jvm_services != '' }} | |
| run: | | |
| set -euo pipefail | |
| TASKS="" | |
| for svc in ${{ steps.changes.outputs.jvm_services }}; do | |
| case "$svc" in | |
| gateway) TASKS+=" :gateway:test" ;; | |
| search) TASKS+=" :search:app:test :search:domain:test" ;; | |
| search-batch) TASKS+=" :search:batch:test" ;; | |
| search-consumer) TASKS+=" :search:consumer:test" ;; | |
| agent-viewer-api) TASKS+=" :agent-viewer:api:test" ;; | |
| # ADR-0058: commerce 는 aggregator(:commerce:app)만 있고 :commerce:domain 없음 → | |
| # 폴드된 4 도메인/feature 테스트를 직접 지정 (default 의 :commerce:domain:test 는 미존재 → CI 실패). | |
| commerce) TASKS+=" :commerce:app:test :inventory:domain:test :inventory:feature:test :warehouse:domain:test :warehouse:feature:test :fulfillment:domain:test :fulfillment:feature:test :order:domain:test :order:feature:test :member:domain:test :member:feature:test :wishlist:domain:test :wishlist:feature:test" ;; | |
| *) TASKS+=" :${svc}:app:test :${svc}:domain:test" ;; | |
| esac | |
| done | |
| echo "── Gradle test tasks: $TASKS" | |
| ./gradlew --no-daemon --max-workers=1 $TASKS | |
| # ───── JVM 이미지: 변경된 서비스만 Jib 빌드 (arm64, serial) ───────────── | |
| - name: ./gradlew jib (변경된 JVM 서비스만) | |
| if: ${{ steps.changes.outputs.jvm_services != '' }} | |
| run: | | |
| set -euo pipefail | |
| # 서비스 이름 → Gradle task path 매핑 | |
| TASKS="" | |
| for svc in ${{ steps.changes.outputs.jvm_services }}; do | |
| case "$svc" in | |
| gateway) TASKS+=" :gateway:jib" ;; | |
| search) TASKS+=" :search:app:jib" ;; | |
| search-batch) TASKS+=" :search:batch:jib" ;; | |
| search-consumer) TASKS+=" :search:consumer:jib" ;; | |
| agent-viewer-api) TASKS+=" :agent-viewer:api:jib" ;; | |
| *) TASKS+=" :${svc}:app:jib" ;; | |
| esac | |
| done | |
| echo "── Gradle tasks: $TASKS" | |
| ./gradlew --no-daemon --max-workers=1 \ | |
| -PjibRegistry=${{ steps.meta.outputs.registry }} \ | |
| -PjibTag=${{ steps.meta.outputs.tag }} \ | |
| $TASKS | |
| # ───── FE/Python 이미지: 변경된 서비스만 buildx (arm64) ───────────────── | |
| - name: Build & push 변경된 Docker 이미지 | |
| if: ${{ steps.changes.outputs.docker_services != '' }} | |
| env: | |
| REGISTRY: ${{ steps.meta.outputs.registry }} | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| # 서비스 이름 → context 경로 매핑 | |
| declare -A DOCKER_CTX=( | |
| [admin-fe]="admin/frontend" | |
| [gifticon-fe]="gifticon/frontend" | |
| [agent-viewer-fe]="agent-viewer/front" | |
| [portal-fe]="portal-fe" | |
| [quant-fe]="quant/frontend" | |
| [quant-ingest]="quant/ingest" | |
| [recommendation-ml]="recommendation-ml" | |
| [recommendation-ann]="recommendation-ann" | |
| ) | |
| for svc in ${{ steps.changes.outputs.docker_services }}; do | |
| ctx="${DOCKER_CTX[$svc]:-}" | |
| # Dockerfile 부재 시 fail-fast. skip 하면 manifest 의 image tag 는 | |
| # 갱신되는데 실제 이미지는 OCIR 에 없어 Argo CD 가 ImagePullBackOff. | |
| if [[ -z "$ctx" ]]; then | |
| echo "::error::$svc 의 context 매핑이 DOCKER_CTX 에 없음" | |
| exit 1 | |
| fi | |
| if [[ ! -f "$ctx/Dockerfile" ]]; then | |
| echo "::error::$svc 의 Dockerfile 부재 ($ctx/Dockerfile). 매핑 수정 또는 서비스 정리 필요" | |
| exit 1 | |
| fi | |
| echo "── docker buildx build → $REGISTRY/$svc:$TAG" | |
| docker buildx build \ | |
| --platform linux/arm64 \ | |
| -f "$ctx/Dockerfile" \ | |
| -t "$REGISTRY/$svc:$TAG" \ | |
| --push \ | |
| "$ctx" | |
| done | |
| # ───── kustomization.yaml 의 images: 섹션 부분 갱신 ───────────────────── | |
| # 변경된 서비스의 newTag 만 새 sha 로 update. 나머지 image entry 는 그대로. | |
| - name: Update oci-arm manifest image tags (부분) | |
| if: ${{ steps.changes.outputs.kustomize_services != '' }} | |
| env: | |
| REGISTRY: ${{ steps.meta.outputs.registry }} | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if ! command -v yq &>/dev/null; then | |
| curl -sSL -o /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64 | |
| chmod +x /tmp/yq | |
| sudo mv /tmp/yq /usr/local/bin/yq | |
| fi | |
| KUSTOM=k8s/overlays/oci-arm/kustomization.yaml | |
| yq -i '.images = (.images // [])' "$KUSTOM" | |
| for svc in ${{ steps.changes.outputs.kustomize_services }}; do | |
| yq -i "(.images) |= map(select(.name != \"commerce/$svc\"))" "$KUSTOM" | |
| yq -i "(.images) += [{\"name\": \"commerce/$svc\", \"newName\": \"$REGISTRY/$svc\", \"newTag\": \"$TAG\"}]" "$KUSTOM" | |
| done | |
| yq -i '(.images) |= sort_by(.name)' "$KUSTOM" | |
| echo "── 갱신된 images: 항목 미리보기" | |
| yq '.images[] | select(.newTag == "'"${TAG}"'") | "\(.name) → \(.newName):\(.newTag)"' "$KUSTOM" | |
| # main branch push 일 때만 commit-back. | |
| # tag push (v*) 는 detached HEAD 라 git push to main 불가 + 의미상 release | |
| # 시점엔 manifest 갱신 별개로 처리. | |
| - name: Commit manifest update | |
| if: ${{ steps.changes.outputs.kustomize_services != '' && github.ref == 'refs/heads/main' }} | |
| run: | | |
| set -euo pipefail | |
| if git diff --quiet k8s/overlays/oci-arm/kustomization.yaml; then | |
| echo "No manifest changes — skip commit" | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add k8s/overlays/oci-arm/kustomization.yaml | |
| UPDATED_COUNT=$(echo "${{ steps.changes.outputs.kustomize_services }}" | tr ' ' '\n' | wc -l) | |
| SVCS="${{ steps.changes.outputs.kustomize_services }}" | |
| git commit -m "ci: bump ${UPDATED_COUNT} service image tag(s) to ${{ steps.meta.outputs.tag }} [skip ci]" -m "Services: ${SVCS}" | |
| git push | |
| - name: Summary | |
| if: ${{ steps.changes.outputs.kustomize_services != '' }} | |
| run: | | |
| { | |
| echo "## Image publish (OCIR / arm64) — incremental" | |
| echo "Registry: \`${{ steps.meta.outputs.registry }}\`" | |
| echo "Tag : \`${{ steps.meta.outputs.tag }}\`" | |
| echo | |
| echo "### Rebuilt" | |
| echo "- JVM : \`${{ steps.changes.outputs.jvm_services }}\`" | |
| echo "- Docker: \`${{ steps.changes.outputs.docker_services }}\`" | |
| echo | |
| echo "→ 변경된 서비스만 이미지 push + kustomization.yaml 갱신." | |
| echo "→ Argo CD 가 변경된 deployment 만 rolling update." | |
| } >> "$GITHUB_STEP_SUMMARY" |