20260223 26 프로필 조회 수정 설정 화면 필요 #48
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
| # =================================================================== | |
| # Flutter CI (코드 분석 및 빌드 검증) 워크플로우 | |
| # =================================================================== | |
| # | |
| # 이 워크플로우는 PR 생성 또는 main 브랜치 푸시 시 | |
| # 코드 정적 분석(flutter analyze)과 Android/iOS 앱 빌드를 검증합니다. | |
| # | |
| # 주요 특징: | |
| # - PR 생성/업데이트 시 자동 실행 | |
| # - main 브랜치 푸시 시 자동 실행 | |
| # - flutter analyze로 코드 품질 검사 | |
| # - Android/iOS 빌드 개별 활성화/비활성화 가능 | |
| # - Analyze Only 모드: 빌드 없이 코드 분석만 실행 가능 | |
| # - PR인 경우 진행 상황을 댓글로 실시간 업데이트 | |
| # - 빌드 실패 시 상세 에러 정보 제공 | |
| # - Analyze와 빌드 병렬 실행으로 시간 단축 | |
| # | |
| # 사용 방법: | |
| # 1. 프로젝트에 맞게 상단 env 섹션의 값들을 수정하세요 | |
| # 2. ANALYZE_ONLY를 true로 설정하면 빌드 없이 코드 분석만 실행 | |
| # 3. ENABLE_ANDROID, ENABLE_IOS로 빌드 대상 플랫폼 선택 | |
| # 4. ENV_FILE_PATH로 환경변수 파일 경로 커스터마이징 | |
| # | |
| # =================================================================== | |
| # 🔧 프로젝트별 설정 | |
| # =================================================================== | |
| # | |
| # ANALYZE_ONLY : true면 analyze만 실행, 빌드 스킵 (기본: false) | |
| # ENABLE_ANDROID : Android 빌드 활성화 여부 (ANALYZE_ONLY=false일 때) | |
| # ENABLE_IOS : iOS 빌드 활성화 여부 (ANALYZE_ONLY=false일 때) | |
| # FLUTTER_VERSION : Flutter SDK 버전 | |
| # JAVA_VERSION : Java 버전 (Android 빌드용) | |
| # XCODE_VERSION : Xcode 버전 (iOS 빌드용) | |
| # ENV_FILE_PATH : .env 파일 경로 (기본값: 루트의 .env) | |
| # | |
| # =================================================================== | |
| # 📋 필요한 GitHub Secrets (선택) | |
| # =================================================================== | |
| # | |
| # 📝 환경 설정: | |
| # - ENV_FILE (또는 ENV) : .env 파일 내용 (앱에서 사용하는 환경변수) | |
| # | |
| # ※ 참고: CI는 빌드 검증 목적이므로 서명/배포 관련 Secrets 불필요 | |
| # 배포용 빌드는 별도 CD 워크플로우 사용 | |
| # | |
| # =================================================================== | |
| name: PROJECT-Flutter-CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| analyze_only: | |
| description: "Analyze Only 모드 (빌드 없이 코드 분석만)" | |
| type: boolean | |
| default: false | |
| enable_android: | |
| description: "Android 빌드 활성화 (Analyze Only=false일 때)" | |
| type: boolean | |
| default: true | |
| enable_ios: | |
| description: "iOS 빌드 활성화 (Analyze Only=false일 때)" | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| # ============================================ | |
| # 🔧 프로젝트별 설정 (아래 값들을 수정하세요) | |
| # ============================================ | |
| env: | |
| # 🎯 CI 모드 설정 | |
| ANALYZE_ONLY: "false" # true: analyze만 실행 (빌드 스킵), false: analyze + 빌드 | |
| # 🎯 빌드 대상 플랫폼 설정 (ANALYZE_ONLY=false일 때만 적용) | |
| ENABLE_ANDROID: "true" # Android 빌드 활성화 (true/false) | |
| ENABLE_IOS: "true" # iOS 빌드 활성화 (true/false) | |
| # 🔧 프로젝트별 설정 | |
| FLUTTER_VERSION: "3.35.5" | |
| JAVA_VERSION: "17" | |
| XCODE_VERSION: "26.0" | |
| ENV_FILE_PATH: ".env" # 환경변수 파일 경로 (커스터마이징 가능) | |
| concurrency: | |
| group: flutter-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ============================================ | |
| # Job 1: 준비 단계 | |
| # ============================================ | |
| prepare: | |
| name: CI 준비 | |
| runs-on: ubuntu-latest | |
| outputs: | |
| comment_id: ${{ steps.comment.outputs.comment_id }} | |
| start_time: ${{ steps.comment.outputs.start_time }} | |
| branch_name: ${{ steps.info.outputs.branch_name }} | |
| commit_hash: ${{ steps.info.outputs.commit_hash }} | |
| analyze_only: ${{ steps.config.outputs.analyze_only }} | |
| enable_android: ${{ steps.config.outputs.enable_android }} | |
| enable_ios: ${{ steps.config.outputs.enable_ios }} | |
| steps: | |
| # 설정 값 outputs으로 전달 | |
| # workflow_dispatch 입력값이 있으면 우선 사용, 없으면 env 값 사용 | |
| - name: 설정 값 전달 | |
| id: config | |
| run: | | |
| # workflow_dispatch 입력값 확인 | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| ANALYZE_ONLY="${{ github.event.inputs.analyze_only }}" | |
| ENABLE_ANDROID="${{ github.event.inputs.enable_android }}" | |
| ENABLE_IOS="${{ github.event.inputs.enable_ios }}" | |
| else | |
| ANALYZE_ONLY="${{ env.ANALYZE_ONLY }}" | |
| ENABLE_ANDROID="${{ env.ENABLE_ANDROID }}" | |
| ENABLE_IOS="${{ env.ENABLE_IOS }}" | |
| fi | |
| echo "analyze_only=$ANALYZE_ONLY" >> $GITHUB_OUTPUT | |
| echo "enable_android=$ENABLE_ANDROID" >> $GITHUB_OUTPUT | |
| echo "enable_ios=$ENABLE_IOS" >> $GITHUB_OUTPUT | |
| echo "📋 CI 설정:" | |
| echo " Analyze Only: $ANALYZE_ONLY" | |
| echo " Android: $ENABLE_ANDROID" | |
| echo " iOS: $ENABLE_IOS" | |
| echo " 트리거: ${{ github.event_name }}" | |
| # 빌드 정보 수집 | |
| - name: 빌드 정보 수집 | |
| id: info | |
| run: | | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| BRANCH_NAME="${{ github.head_ref }}" | |
| else | |
| BRANCH_NAME="${{ github.ref_name }}" | |
| fi | |
| COMMIT_HASH=$(echo "${{ github.sha }}" | cut -c1-7) | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT | |
| echo "📋 빌드 정보:" | |
| echo " 브랜치: $BRANCH_NAME" | |
| echo " 커밋: $COMMIT_HASH" | |
| # PR인 경우 진행 상황 댓글 생성 | |
| - name: 진행 상황 댓글 생성 | |
| id: comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const branchName = '${{ steps.info.outputs.branch_name }}'; | |
| const commitHash = '${{ steps.info.outputs.commit_hash }}'; | |
| const analyzeOnly = '${{ steps.config.outputs.analyze_only }}' === 'true'; | |
| const enableAndroid = '${{ steps.config.outputs.enable_android }}' === 'true'; | |
| const enableIos = '${{ steps.config.outputs.enable_ios }}' === 'true'; | |
| const runId = '${{ github.run_id }}'; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; | |
| const startTime = Date.now(); | |
| // 헤더 결정 | |
| const header = analyzeOnly | |
| ? '## 🔍 Flutter CI (Analyze Only) 진행 중...' | |
| : '## 🔨 Flutter CI 진행 중...'; | |
| // 플랫폼 상태 결정 | |
| let androidStatus, iosStatus; | |
| if (analyzeOnly) { | |
| androidStatus = '⏸️ 스킵'; | |
| iosStatus = '⏸️ 스킵'; | |
| } else { | |
| androidStatus = enableAndroid ? '⏳ 진행 중...' : '⏸️ 비활성화'; | |
| iosStatus = enableIos ? '⏳ 진행 중...' : '⏸️ 비활성화'; | |
| } | |
| const bodyParts = [ | |
| header, | |
| '', | |
| '| 검사 항목 | 상태 | 소요 시간 |', | |
| '|----------|------|----------|', | |
| '| 🔍 Analyze | ⏳ 진행 중... | - |', | |
| `| 🤖 Android 빌드 | ${androidStatus} | - |`, | |
| `| 🍎 iOS 빌드 | ${iosStatus} | - |`, | |
| '', | |
| '| 항목 | 값 |', | |
| '|------|-----|', | |
| `| **브랜치** | \`${branchName}\` |`, | |
| `| **커밋** | \`${commitHash}\` |` | |
| ]; | |
| if (analyzeOnly) { | |
| bodyParts.push(''); | |
| bodyParts.push('ℹ️ **Analyze Only 모드**: 빌드 없이 코드 분석만 실행됩니다.'); | |
| } | |
| bodyParts.push(''); | |
| bodyParts.push(`**[📋 실시간 로그 보기](${runUrl})**`); | |
| bodyParts.push(''); | |
| bodyParts.push('---'); | |
| bodyParts.push('*🤖 이 댓글은 자동으로 업데이트됩니다.*'); | |
| const body = bodyParts.join('\n'); | |
| const { data: comment } = await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| console.log(`✅ 진행 상황 댓글 생성 완료: #${comment.id}`); | |
| core.setOutput('comment_id', comment.id); | |
| core.setOutput('start_time', startTime); | |
| # ============================================ | |
| # Job 2: 코드 분석 (병렬) | |
| # ============================================ | |
| analyze: | |
| name: 코드 분석 | |
| runs-on: ubuntu-latest | |
| needs: prepare | |
| outputs: | |
| status: ${{ steps.result.outputs.status }} | |
| duration: ${{ steps.result.outputs.duration }} | |
| steps: | |
| - name: 분석 시작 시간 기록 | |
| id: analyze_start | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| core.setOutput('time', Date.now().toString()); | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| # .env 파일 생성 | |
| - name: Create .env file | |
| run: | | |
| cat << 'EOF' > ${{ env.ENV_FILE_PATH }} | |
| ${{ secrets.ENV_FILE || secrets.ENV }} | |
| EOF | |
| echo "✅ ${{ env.ENV_FILE_PATH }} file created" | |
| # Flutter 설정 | |
| - name: Set up Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Verify Flutter version | |
| run: | | |
| echo "✅ Flutter setup completed" | |
| flutter --version | |
| # Flutter 캐시 | |
| - name: Cache Flutter dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pub-cache | |
| key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }} | |
| restore-keys: ${{ runner.os }}-flutter-pub- | |
| # 프로젝트 의존성 설치 | |
| - name: Install dependencies | |
| run: | | |
| flutter pub get | |
| echo "✅ Dependencies installed" | |
| # 코드 생성 (Riverpod, Freezed, Retrofit 등) | |
| - name: Run build_runner | |
| run: | | |
| echo "🔧 코드 생성 시작 (build_runner)..." | |
| dart run build_runner build --delete-conflicting-outputs | |
| echo "✅ 코드 생성 완료" | |
| # 생성된 코드 파일을 현재 브랜치에 커밋 | |
| # - PR 이벤트: PR 소스 브랜치에 커밋 (로컬에서 pull 가능) | |
| # - Push 이벤트: 푸시된 브랜치에 커밋 | |
| # - [skip ci]로 무한 루프 방지, 최대 5회 재시도로 race condition 방지 | |
| - name: Commit generated files | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # 현재 이벤트에 따라 대상 브랜치 결정 | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| TARGET_BRANCH="${{ github.head_ref }}" | |
| echo "📌 PR 이벤트: 소스 브랜치 '$TARGET_BRANCH'에 커밋" | |
| # PR 이벤트에서는 merge ref(refs/pull/N/merge)를 checkout하므로 | |
| # 실제 소스 브랜치로 전환 후 build_runner 재실행 필요 | |
| git fetch origin "$TARGET_BRANCH" | |
| git checkout "$TARGET_BRANCH" | |
| flutter pub get | |
| dart run build_runner build --delete-conflicting-outputs | |
| else | |
| TARGET_BRANCH="${{ github.ref_name }}" | |
| echo "📌 Push 이벤트: '$TARGET_BRANCH' 브랜치에 커밋" | |
| fi | |
| # 생성 파일 스테이징 | |
| git add "*.g.dart" "*.freezed.dart" | |
| if git diff --cached --quiet; then | |
| echo "ℹ️ 생성 파일 변경 없음, 커밋 스킵" | |
| exit 0 | |
| fi | |
| git commit -m "chore: update generated files (build_runner) [skip ci]" | |
| # Race Condition 방지: pull-rebase 후 push (최대 5회 재시도) | |
| MAX_RETRIES=5 | |
| RETRY_COUNT=0 | |
| PUSH_SUCCESS=false | |
| while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "🔄 Push 시도 $RETRY_COUNT/$MAX_RETRIES..." | |
| if git push origin HEAD:"$TARGET_BRANCH"; then | |
| PUSH_SUCCESS=true | |
| echo "✅ 생성 파일 커밋 완료 ($TARGET_BRANCH)" | |
| break | |
| else | |
| echo "⚠️ Push 실패, remote 변경사항 동기화 중..." | |
| if git pull --rebase origin "$TARGET_BRANCH"; then | |
| echo "✅ Rebase 성공, 다시 push 시도..." | |
| else | |
| echo "❌ Rebase 실패, 충돌 해결 필요" | |
| git rebase --abort 2>/dev/null || true | |
| echo "⚠️ 생성 파일 커밋을 건너뜁니다 (수동 실행 필요)" | |
| exit 0 | |
| fi | |
| fi | |
| done | |
| if [ "$PUSH_SUCCESS" = false ]; then | |
| echo "⚠️ $MAX_RETRIES회 시도 후에도 push 실패, 생성 파일 커밋을 건너뜁니다" | |
| fi | |
| # Flutter Analyze 실행 | |
| - name: Run Flutter Analyze | |
| id: analyze | |
| run: | | |
| echo "🔍 Flutter 코드 분석 시작..." | |
| flutter analyze --no-fatal-infos | |
| echo "✅ 코드 분석 완료" | |
| # 분석 결과 기록 | |
| - name: 분석 결과 기록 | |
| id: result | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const analyzeStart = parseInt('${{ steps.analyze_start.outputs.time }}'); | |
| const now = Date.now(); | |
| const elapsed = now - analyzeStart; | |
| const minutes = Math.floor(elapsed / 60000); | |
| const seconds = Math.floor((elapsed % 60000) / 1000); | |
| const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; | |
| const status = '${{ steps.analyze.outcome }}' === 'success' ? 'success' : 'failure'; | |
| core.setOutput('status', status); | |
| core.setOutput('duration', duration); | |
| console.log(`📋 코드 분석 결과: ${status} (${duration})`); | |
| # ============================================ | |
| # Job 3: Android 빌드 (조건부, 병렬) | |
| # ============================================ | |
| build-android: | |
| name: Android 빌드 | |
| if: | | |
| needs.prepare.outputs.analyze_only != 'true' && | |
| needs.prepare.outputs.enable_android == 'true' | |
| runs-on: ubuntu-latest | |
| needs: prepare | |
| outputs: | |
| status: ${{ steps.result.outputs.status }} | |
| duration: ${{ steps.result.outputs.duration }} | |
| steps: | |
| - name: 빌드 시작 시간 기록 | |
| id: build_start | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| core.setOutput('time', Date.now().toString()); | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| # .env 파일 생성 | |
| - name: Create .env file | |
| run: | | |
| cat << 'EOF' > ${{ env.ENV_FILE_PATH }} | |
| ${{ secrets.ENV_FILE || secrets.ENV }} | |
| EOF | |
| echo "✅ ${{ env.ENV_FILE_PATH }} file created" | |
| # Flutter 설정 | |
| - name: Set up Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Verify Flutter version | |
| run: | | |
| echo "✅ Flutter setup completed" | |
| flutter --version | |
| # Flutter 캐시 | |
| - name: Cache Flutter dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pub-cache | |
| key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }} | |
| restore-keys: ${{ runner.os }}-flutter-pub- | |
| # Gradle 캐시 | |
| - name: Cache Gradle dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle', '**/gradle-wrapper.properties') }} | |
| restore-keys: | | |
| ${{ runner.os }}-gradle- | |
| # 프로젝트 의존성 설치 | |
| - name: Install dependencies | |
| run: | | |
| flutter pub get | |
| echo "✅ Dependencies installed" | |
| # 코드 생성 (Riverpod, Freezed, Retrofit 등) | |
| - name: Run build_runner | |
| run: | | |
| echo "🔧 코드 생성 시작 (build_runner)..." | |
| dart run build_runner build --delete-conflicting-outputs | |
| echo "✅ 코드 생성 완료" | |
| # Java 설정 | |
| - name: Set up Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: ${{ env.JAVA_VERSION }} | |
| - name: Verify Java version | |
| run: | | |
| echo "✅ Java setup completed" | |
| java -version | |
| # Gradle 셋업 | |
| - name: Setup Gradle | |
| working-directory: android | |
| run: | | |
| chmod +x gradlew | |
| echo "✅ Gradle wrapper permissions set" | |
| # APK 빌드 | |
| - name: Build APK | |
| id: build | |
| run: | | |
| echo "📦 Flutter APK 빌드 시작..." | |
| flutter build apk --release | |
| echo "✅ APK 빌드 완료" | |
| ls -la ./build/app/outputs/flutter-apk/ || true | |
| # 빌드 결과 기록 | |
| - name: 빌드 결과 기록 | |
| id: result | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); | |
| const now = Date.now(); | |
| const elapsed = now - buildStart; | |
| const minutes = Math.floor(elapsed / 60000); | |
| const seconds = Math.floor((elapsed % 60000) / 1000); | |
| const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; | |
| const status = '${{ steps.build.outcome }}' === 'success' ? 'success' : 'failure'; | |
| core.setOutput('status', status); | |
| core.setOutput('duration', duration); | |
| console.log(`📋 Android 빌드 결과: ${status} (${duration})`); | |
| # ============================================ | |
| # Job 4: iOS 빌드 (조건부, 병렬) | |
| # ============================================ | |
| build-ios: | |
| name: iOS 빌드 | |
| if: | | |
| needs.prepare.outputs.analyze_only != 'true' && | |
| needs.prepare.outputs.enable_ios == 'true' | |
| runs-on: macos-15 | |
| needs: prepare | |
| outputs: | |
| status: ${{ steps.result.outputs.status }} | |
| duration: ${{ steps.result.outputs.duration }} | |
| steps: | |
| - name: 빌드 시작 시간 기록 | |
| id: build_start | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| core.setOutput('time', Date.now().toString()); | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Select Xcode version | |
| run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer | |
| # .env 파일 생성 | |
| - name: Create .env file | |
| run: | | |
| cat << 'EOF' > ${{ env.ENV_FILE_PATH }} | |
| ${{ secrets.ENV_FILE || secrets.ENV }} | |
| EOF | |
| echo "✅ ${{ env.ENV_FILE_PATH }} file created" | |
| # Flutter 설정 | |
| - name: Set up Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Verify Flutter version | |
| run: | | |
| echo "✅ Flutter setup completed" | |
| flutter --version | |
| # Flutter 캐시 | |
| - name: Cache Flutter dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pub-cache | |
| key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }} | |
| restore-keys: ${{ runner.os }}-flutter-pub- | |
| # 프로젝트 의존성 설치 | |
| - name: Install dependencies | |
| run: | | |
| flutter pub get | |
| echo "✅ Dependencies installed" | |
| # 코드 생성 (Riverpod, Freezed, Retrofit 등) | |
| - name: Run build_runner | |
| run: | | |
| echo "🔧 코드 생성 시작 (build_runner)..." | |
| dart run build_runner build --delete-conflicting-outputs | |
| echo "✅ 코드 생성 완료" | |
| # Ruby 및 CocoaPods 설정 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.1" | |
| bundler-cache: true | |
| - name: Install CocoaPods | |
| run: | | |
| gem install cocoapods | |
| cd ios && pod install --repo-update || pod install | |
| echo "✅ CocoaPods installed" | |
| # iOS 빌드 (서명 없이) | |
| - name: Build iOS | |
| id: build | |
| run: | | |
| echo "📦 Flutter iOS 빌드 시작..." | |
| flutter build ios --release --no-codesign | |
| echo "✅ iOS 빌드 완료" | |
| # 빌드 결과 기록 | |
| - name: 빌드 결과 기록 | |
| id: result | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); | |
| const now = Date.now(); | |
| const elapsed = now - buildStart; | |
| const minutes = Math.floor(elapsed / 60000); | |
| const seconds = Math.floor((elapsed % 60000) / 1000); | |
| const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; | |
| const status = '${{ steps.build.outcome }}' === 'success' ? 'success' : 'failure'; | |
| core.setOutput('status', status); | |
| core.setOutput('duration', duration); | |
| console.log(`📋 iOS 빌드 결과: ${status} (${duration})`); | |
| # ============================================ | |
| # Job 5: 결과 보고 | |
| # ============================================ | |
| report: | |
| name: 결과 보고 | |
| if: always() && github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [prepare, analyze, build-android, build-ios] | |
| steps: | |
| - name: 최종 결과 댓글 업데이트 | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const commentId = parseInt('${{ needs.prepare.outputs.comment_id }}'); | |
| const startTime = parseInt('${{ needs.prepare.outputs.start_time }}'); | |
| const branchName = '${{ needs.prepare.outputs.branch_name }}'; | |
| const commitHash = '${{ needs.prepare.outputs.commit_hash }}'; | |
| const analyzeOnly = '${{ needs.prepare.outputs.analyze_only }}' === 'true'; | |
| const enableAndroid = '${{ needs.prepare.outputs.enable_android }}' === 'true'; | |
| const enableIos = '${{ needs.prepare.outputs.enable_ios }}' === 'true'; | |
| const runId = '${{ github.run_id }}'; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; | |
| // 결과 수집 | |
| const analyzeStatus = '${{ needs.analyze.outputs.status }}'; | |
| const analyzeDuration = '${{ needs.analyze.outputs.duration }}' || '-'; | |
| const androidStatus = '${{ needs.build-android.outputs.status }}'; | |
| const androidDuration = '${{ needs.build-android.outputs.duration }}' || '-'; | |
| const iosStatus = '${{ needs.build-ios.outputs.status }}'; | |
| const iosDuration = '${{ needs.build-ios.outputs.duration }}' || '-'; | |
| // 전체 소요 시간 계산 | |
| const now = Date.now(); | |
| const totalElapsed = now - startTime; | |
| const totalMin = Math.floor(totalElapsed / 60000); | |
| const totalSec = Math.floor((totalElapsed % 60000) / 1000); | |
| const totalDuration = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`; | |
| // 상태 결정 | |
| let overallSuccess = true; | |
| let analyzeDisplay, androidDisplay, iosDisplay; | |
| // Analyze 상태 | |
| if (analyzeStatus === 'success') { | |
| analyzeDisplay = '✅ 성공'; | |
| } else { | |
| analyzeDisplay = '❌ 실패'; | |
| overallSuccess = false; | |
| } | |
| // Android 상태 | |
| if (analyzeOnly) { | |
| androidDisplay = '⏸️ 스킵'; | |
| } else if (!enableAndroid) { | |
| androidDisplay = '⏸️ 비활성화'; | |
| } else if (androidStatus === 'success') { | |
| androidDisplay = '✅ 성공'; | |
| } else { | |
| androidDisplay = '❌ 실패'; | |
| overallSuccess = false; | |
| } | |
| // iOS 상태 | |
| if (analyzeOnly) { | |
| iosDisplay = '⏸️ 스킵'; | |
| } else if (!enableIos) { | |
| iosDisplay = '⏸️ 비활성화'; | |
| } else if (iosStatus === 'success') { | |
| iosDisplay = '✅ 성공'; | |
| } else { | |
| iosDisplay = '❌ 실패'; | |
| overallSuccess = false; | |
| } | |
| // 헤더 결정 | |
| let header; | |
| if (analyzeOnly) { | |
| header = overallSuccess | |
| ? '## ✅ Flutter CI (Analyze Only) 성공!' | |
| : '## ❌ Flutter CI (Analyze Only) 실패'; | |
| } else { | |
| header = overallSuccess | |
| ? '## ✅ Flutter CI 성공!' | |
| : '## ❌ Flutter CI 실패'; | |
| } | |
| // 댓글 본문 생성 | |
| const bodyParts = [ | |
| header, | |
| '', | |
| '| 검사 항목 | 상태 | 소요 시간 |', | |
| '|----------|------|----------|', | |
| `| 🔍 Analyze | ${analyzeDisplay} | ${analyzeDuration} |`, | |
| `| 🤖 Android 빌드 | ${androidDisplay} | ${analyzeOnly || !enableAndroid ? '-' : androidDuration} |`, | |
| `| 🍎 iOS 빌드 | ${iosDisplay} | ${analyzeOnly || !enableIos ? '-' : iosDuration} |`, | |
| '', | |
| '| 항목 | 값 |', | |
| '|------|-----|', | |
| `| **브랜치** | \`${branchName}\` |`, | |
| `| **커밋** | \`${commitHash}\` |`, | |
| `| **총 소요 시간** | ${totalDuration} |`, | |
| '' | |
| ]; | |
| // Analyze Only 모드 안내 | |
| if (analyzeOnly && overallSuccess) { | |
| bodyParts.push('ℹ️ **Analyze Only 모드**: 빌드 없이 코드 분석만 실행되었습니다.'); | |
| bodyParts.push(''); | |
| } | |
| // 실패 시 추가 안내 | |
| if (!overallSuccess) { | |
| bodyParts.push( | |
| '### 💡 확인 사항', | |
| '' | |
| ); | |
| if (analyzeStatus !== 'success') { | |
| bodyParts.push('**Analyze 실패 시:**'); | |
| bodyParts.push('- `flutter analyze` 로컬에서 실행하여 lint 오류 확인'); | |
| bodyParts.push('- 코드 스타일 및 타입 오류 수정'); | |
| bodyParts.push(''); | |
| } | |
| if (!analyzeOnly && enableAndroid && androidStatus !== 'success') { | |
| bodyParts.push('**Android 빌드 실패 시:**'); | |
| bodyParts.push('- Gradle 빌드 로그에서 에러 확인'); | |
| bodyParts.push('- 의존성 버전 호환성 확인'); | |
| bodyParts.push(''); | |
| } | |
| if (!analyzeOnly && enableIos && iosStatus !== 'success') { | |
| bodyParts.push('**iOS 빌드 실패 시:**'); | |
| bodyParts.push('- Xcode 빌드 로그에서 에러 확인'); | |
| bodyParts.push('- CocoaPods 의존성 확인'); | |
| bodyParts.push(''); | |
| } | |
| } | |
| bodyParts.push(`**[📋 워크플로우 로그](${runUrl})**`); | |
| const body = bodyParts.join('\n'); | |
| // 댓글 업데이트 | |
| if (commentId) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: commentId, | |
| body: body | |
| }); | |
| console.log(`✅ 결과 댓글 업데이트 완료`); | |
| } else { | |
| console.log('⚠️ 댓글 ID가 없어 업데이트를 건너뜁니다.'); | |
| } |