From 5abe92ad7a91a50cb7e091b78b3d566282ce6ad1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 Mar 2026 00:22:34 +0000 Subject: [PATCH 01/20] =?UTF-8?q?SpaceStudyShip-FE=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20:=20docs=20:=20v1.1.31=20README=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6671f0..7d63c0b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ samples, guidance on mobile development, and a full API reference. -## 최신 버전 : v1.1.27 (2026-02-21) +## 최신 버전 : v1.1.31 (2026-03-05) [전체 버전 기록 보기](CHANGELOG.md) From e91ef16677694693acb79794234c2a01a07ad827 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Thu, 5 Mar 2026 13:53:16 +0900 Subject: [PATCH 02/20] =?UTF-8?q?@suh-lab=20=EB=8C=93=EA=B8=80=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=EC=9E=91=EB=8F=99=20:=20fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=ED=99=94=20https://github.com/SpaceStudyShip/SpaceStudyShip-FE?= =?UTF-8?q?/issues/49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PROJECT-FLUTTER-ANDROID-TEST-APK.yaml | 509 +++++++++----- .../PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml | 638 ++++++++++++++---- ...ECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml | 235 +++++-- 3 files changed, 1045 insertions(+), 337 deletions(-) diff --git a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml index 6279a24..eee4070 100644 --- a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +++ b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml @@ -13,6 +13,7 @@ # - APK 파일만 생성 (Play Store 배포 제거) # - 아티팩트로 업로드하여 다운로드 가능 (최대 500MB/아티팩트) # - 아티팩트 이름에 브랜치명 포함하여 구분 용이 +# - 진행 상황 댓글 시스템: 준비→빌드 단계별 실시간 업데이트 # # 사용 방법: # 1. 기능 브랜치로 체크아웃 @@ -21,13 +22,18 @@ # 4. 빌드 완료 후 아티팩트에서 APK 다운로드 # # =================================================================== -# 📋 필요한 GitHub Secrets +# 🔑 필수 GitHub Secrets # =================================================================== # +# 🔐 Release Keystore (Play Store 배포와 동일): +# RELEASE_KEYSTORE_BASE64: 릴리즈 키스토어 파일 (base64) +# RELEASE_KEYSTORE_PASSWORD: 키스토어 비밀번호 +# RELEASE_KEY_ALIAS: 키 별칭 +# RELEASE_KEY_PASSWORD: 키 비밀번호 +# # 📝 환경 설정 (선택): -# - ENV_FILE : .env 파일 내용 (앱에서 사용하는 환경변수) -# - DEBUG_KEYSTORE : debug.keystore 파일 (base64 인코딩) -# - GOOGLE_SERVICES_JSON : google-services.json 내용 (Firebase 사용 시) +# ENV_FILE (선택): .env 파일 내용 +# GOOGLE_SERVICES_JSON (선택): google-services.json 내용 (Firebase 사용 시) # # =================================================================== @@ -38,9 +44,9 @@ on: repository_dispatch: types: [build-android-app] -# ============================================ +# =================================================================== # 🔧 프로젝트별 설정 (아래 값들을 수정하세요) -# ============================================ +# =================================================================== env: FLUTTER_VERSION: "3.35.5" JAVA_VERSION: "17" @@ -60,8 +66,111 @@ jobs: commit_hash: ${{ steps.build_info.outputs.commit_hash }} pr_number: ${{ steps.build_info.outputs.pr_number }} build_number: ${{ steps.build_info.outputs.build_number }} + progress_comment_id: ${{ steps.progress.outputs.comment_id }} + prepare_start: ${{ steps.progress.outputs.start_time }} steps: + - name: 브랜치 존재 여부 사전 확인 + id: verify_branch + if: github.event_name == 'repository_dispatch' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branchName = '${{ github.event.client_payload.branch_name }}'; + const prNumber = '${{ github.event.client_payload.pr_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`); + + try { + await github.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: branchName + }); + console.log(`✅ 브랜치 존재 확인됨: ${branchName}`); + } catch (error) { + if (error.status === 404) { + console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`); + + const body = [ + '🤖 ❌ **Android 테스트 APK 빌드 실패 - 브랜치를 찾을 수 없습니다**', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **요청된 브랜치** | \`${branchName}\` |`, + `| **이슈/PR** | #${prNumber} |`, + '', + '### 💡 확인 사항', + '1. 브랜치가 원격 저장소에 push되었는지 확인하세요', + '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요', + '3. 브랜치명에 오타가 없는지 확인하세요', + '', + `🔗 [워크플로우 로그](${runUrl})` + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(prNumber), + body: body + }); + + core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`); + } else { + core.setFailed(`브랜치 확인 중 오류: ${error.message}`); + } + } + + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # 진행 상황 댓글 시스템 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + - name: 진행 상황 댓글 생성 + id: progress + if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + const startTime = Date.now(); + + const body = [ + '## 🤖 Android 테스트 APK 빌드 진행 중...', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + '| 🔧 준비 | ⏳ 진행 중... | - |', + '| 🔨 APK 빌드 | ⏸️ 대기 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + `**[📋 실시간 로그 보기](${runUrl})**`, + '', + '---', + '*🤖 이 댓글은 자동으로 업데이트됩니다.*' + ].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); + - name: Checkout repository uses: actions/checkout@v4 with: @@ -160,12 +269,115 @@ jobs: echo " 커밋 해시: $COMMIT_SHORT" echo " 빌드 날짜: $BUILD_DATE" + # 진행 상황 업데이트 - 준비 완료 + - name: 진행 상황 업데이트 - 준비 완료 + if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && steps.progress.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ steps.progress.outputs.comment_id }}'); + const startTime = parseInt('${{ steps.progress.outputs.start_time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + const elapsed = now - startTime; + const minutes = Math.floor(elapsed / 60000); + const seconds = Math.floor((elapsed % 60000) / 1000); + const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; + + const body = [ + '## 🤖 Android 테스트 APK 빌드 진행 중...', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${duration} |`, + '| 🔨 APK 빌드 | ⏳ 진행 중... | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + `**[📋 실시간 로그 보기](${runUrl})**`, + '', + '---', + '*🤖 이 댓글은 자동으로 업데이트됩니다.*' + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`✅ 진행 상황 업데이트 완료: 준비 완료 (${duration})`); + + # 진행 상황 업데이트 - 준비 실패 + - name: 진행 상황 업데이트 - 준비 실패 + if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && steps.progress.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ steps.progress.outputs.comment_id }}'); + const startTime = parseInt('${{ steps.progress.outputs.start_time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + const elapsed = now - startTime; + const minutes = Math.floor(elapsed / 60000); + const seconds = Math.floor((elapsed % 60000) / 1000); + const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; + + const body = [ + '## 🤖 ❌ Android 테스트 APK 빌드 실패', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ❌ 실패 | ${duration} |`, + '| 🔨 APK 빌드 | ⏸️ 취소됨 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + '❌ **환경 준비 중 오류가 발생했습니다.**', + '', + `🔗 [워크플로우 실행 로그 확인](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`❌ 진행 상황 업데이트 완료: 준비 실패`); + build-android-test: name: Android 테스트 APK 빌드 runs-on: ubuntu-latest needs: prepare-test-build 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 with: @@ -175,55 +387,36 @@ jobs: - name: Pull latest changes run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }} - # Debug Keystore 설정 (선택적) - - name: Setup Debug Keystore - run: | - mkdir -p ~/.android - if [ -n "${{ secrets.DEBUG_KEYSTORE }}" ]; then - echo "${{ secrets.DEBUG_KEYSTORE }}" | base64 -d > ~/.android/debug.keystore - echo "✅ Debug Keystore created from secrets" - else - echo "ℹ️ DEBUG_KEYSTORE secret not provided, using default" - fi - ls -la ~/.android/ || true - - # .env 파일 생성 - name: Create .env file run: | - cat << 'EOF' > ${{ env.ENV_FILE_PATH }} - ${{ secrets.ENV_FILE }} - EOF + echo "${{ secrets.ENV_FILE || secrets.ENV }}" > ${{ env.ENV_FILE_PATH }} echo "✅ ${{ env.ENV_FILE_PATH }} file created" - # Keystore와 key.properties 설정 (선택적) - - name: Setup Keystore and key.properties + # Release Keystore 설정 (PLAYSTORE-CICD와 동일한 경로) + - name: Setup Release Keystore run: | mkdir -p android/app/keystore - if [ -n "${{ secrets.DEBUG_KEYSTORE }}" ]; then - echo "${{ secrets.DEBUG_KEYSTORE }}" | base64 -d > android/app/keystore/key.jks - echo "✅ Keystore created from DEBUG_KEYSTORE" - echo "storeFile=keystore/key.jks" > android/key.properties - echo "storePassword=android" >> android/key.properties - echo "keyAlias=androiddebugkey" >> android/key.properties - echo "keyPassword=android" >> android/key.properties - echo "✅ key.properties created" - else - echo "ℹ️ DEBUG_KEYSTORE not provided, skipping keystore setup" - fi - ls -la android/ || true + echo "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore/key.jks + echo "✅ Release Keystore 생성 완료" + ls -la android/app/keystore/ + + - name: Create key.properties + run: | + cat > android/key.properties << EOF + storeFile=keystore/key.jks + storePassword=${{ secrets.RELEASE_KEYSTORE_PASSWORD }} + keyAlias=${{ secrets.RELEASE_KEY_ALIAS }} + keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }} + EOF + echo "✅ key.properties 생성 완료" - # google-services.json 생성 (선택적) - - name: Create google-services.json + # Google-services.json 생성 (PLAYSTORE-CICD와 동일한 방식) + - name: Create Google-services.json run: | - if [ -n "${{ secrets.GOOGLE_SERVICES_JSON }}" ]; then - mkdir -p android/app - cat << 'EOF' > android/app/google-services.json + cat << 'EOF' > android/app/google-services.json ${{ secrets.GOOGLE_SERVICES_JSON }} EOF - echo "✅ google-services.json created" - else - echo "ℹ️ GOOGLE_SERVICES_JSON secret not provided, skipping" - fi + echo "✅ Google-services.json 생성 완료" # Flutter 설정 - name: Set up Flutter @@ -280,32 +473,24 @@ jobs: echo "✅ Java setup completed" java -version - # Android SDK 설정 - - name: Set up Android SDK - uses: android-actions/setup-android@v3 - - - name: Verify Android SDK setup - run: echo "✅ Android SDK setup completed" - # Ruby 설정 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.3" - bundler-cache: false + ruby-version: "3.4.1" + bundler-cache: true - name: Verify Ruby version run: | echo "✅ Ruby setup completed" ruby -v - # Fastlane 설치 (Bundler 방식으로 CFPropertyList 의존성 충돌 방지) + # Fastlane 설치 - name: Install Fastlane run: | - printf 'source "https://rubygems.org"\ngem "fastlane"\n' > Gemfile - bundle install - echo "✅ Fastlane 설치 완료" - bundle exec fastlane --version + gem install fastlane --force + echo "✅ Fastlane installed" + fastlane --version # APK 파일명 생성 - name: Generate APK filename @@ -324,7 +509,7 @@ jobs: if [ -f "android/fastlane/Fastfile" ]; then echo "📦 Fastlane을 사용하여 빌드..." cd android - bundle exec fastlane build --verbose || flutter build apk --release + fastlane build --verbose || flutter build apk --release else echo "📦 Flutter 직접 빌드..." flutter build apk --release @@ -448,115 +633,127 @@ jobs: echo "❌ Android 테스트 APK 빌드 실패!" echo "로그를 확인해주세요." - # PR/ISSUE에 빌드 완료 댓글 작성 (repository_dispatch로 트리거된 경우) - - name: 빌드 성공 댓글 작성 - if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' + # 진행 상황 최종 업데이트 - 성공 + - name: 진행 상황 최종 업데이트 - 성공 + if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != '' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const sourceType = '${{ github.event.client_payload.source_type }}' || 'PR'; - const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); - const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}'; - const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}'; + const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}'); + const prepareStart = parseInt('${{ needs.prepare-test-build.outputs.prepare_start }}'); + const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}'; - const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}'; const runId = '${{ github.run_id }}'; const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; const artifactUrl = `${runUrl}#artifacts`; - const body = `🤖 ✅ **Android 테스트 APK 빌드 완료** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 브랜치: \`${branchName}\` - - 커밋: \`${commitHash}\` - - 📦 **[아티팩트 다운로드](${artifactUrl})** - - 🔗 [워크플로우 실행 로그](${runUrl})`; - - if (sourceType === 'PR') { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ PR #${prNumber}에 빌드 성공 댓글 작성 완료`); - - if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) { - const issueBody = `🤖 ✅ **Android 테스트 APK 빌드 완료** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 커밋: \`${commitHash}\` - - 📦 **[아티팩트 다운로드](${artifactUrl})**`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(issueNumber), - body: issueBody - }); - console.log(`✅ 이슈 #${issueNumber}에도 빌드 성공 댓글 작성 완료`); - } - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ ${sourceType} #${prNumber}에 빌드 성공 댓글 작성 완료`); - } - - - name: 빌드 실패 댓글 작성 - if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = now - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + // 전체 소요 시간 + const totalElapsed = now - prepareStart; + const totalMin = Math.floor(totalElapsed / 60000); + const totalSec = Math.floor((totalElapsed % 60000) / 1000); + const totalDuration = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`; + + const body = [ + '## 🤖 ✅ Android 테스트 APK 빌드 완료', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 APK 빌드 | ✅ 완료 | ${buildDuration} |`, + '', + `**총 소요 시간: ${totalDuration}**`, + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + `| **커밋** | \`${commitHash}\` |`, + '', + `📦 **[아티팩트 다운로드](${artifactUrl})**`, + '', + `🔗 [워크플로우 실행 로그](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`✅ 진행 상황 최종 업데이트 완료: 전체 성공 (${totalDuration})`); + + # 진행 상황 최종 업데이트 - 실패 + - name: 진행 상황 최종 업데이트 - 실패 + if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != '' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const sourceType = '${{ github.event.client_payload.source_type }}' || 'PR'; - const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); - const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}'; - const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}'; - const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}'; + const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}'); + const prepareStart = parseInt('${{ needs.prepare-test-build.outputs.prepare_start }}'); + const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}'; const runId = '${{ github.run_id }}'; const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - const body = `🤖 ❌ **Android 테스트 APK 빌드 실패** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 브랜치: \`${branchName}\` - - ❌ 빌드 중 오류가 발생했습니다. - - 🔗 [워크플로우 실행 로그 확인](${runUrl})`; - - if (sourceType === 'PR') { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ PR #${prNumber}에 빌드 실패 댓글 작성 완료`); - - if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(issueNumber), - body: body - }); - console.log(`✅ 이슈 #${issueNumber}에도 빌드 실패 댓글 작성 완료`); - } - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ ${sourceType} #${prNumber}에 빌드 실패 댓글 작성 완료`); - } \ No newline at end of file + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = now - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + const body = [ + '## 🤖 ❌ Android 테스트 APK 빌드 실패', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 APK 빌드 | ❌ 실패 | ${buildDuration} |`, + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + `| **커밋** | \`${commitHash}\` |`, + '', + '❌ **APK 빌드 중 오류가 발생했습니다.**', + '', + `🔗 [워크플로우 실행 로그 확인](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`❌ 진행 상황 최종 업데이트 완료: APK 빌드 실패`); diff --git a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml index fd8f94e..a76f8e5 100644 --- a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +++ b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml @@ -16,6 +16,7 @@ # - 빌드 번호는 PR번호 + 카운트 또는 GITHUB_RUN_NUMBER 사용 # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식) # - GitHub API로 이슈 정보 가져와서 릴리즈 노트에 포함 +# - 진행 상황 댓글 시스템: 준비→빌드→배포 단계별 실시간 업데이트 # # 빌드 파이프라인: # 1. flutter build ios --no-codesign (Flutter 빌드) @@ -24,23 +25,24 @@ # 4. fastlane upload_testflight (마법사 생성 Fastfile 사용) # # =================================================================== -# 📋 필요한 GitHub Secrets +# 🔑 필수 GitHub Secrets # =================================================================== # -# 🔐 Apple 인증서 & 프로비저닝 프로파일 (필수): -# - APPLE_CERTIFICATE_BASE64 : .p12 인증서 (base64 인코딩) -# - APPLE_CERTIFICATE_PASSWORD : .p12 인증서 비밀번호 -# - APPLE_PROVISIONING_PROFILE_BASE64 : .mobileprovision 파일 (base64 인코딩) -# - IOS_PROVISIONING_PROFILE_NAME : 프로비저닝 프로파일 이름 (예: "MyApp Distribution") +# 🔐 Apple 인증서 & 프로비저닝 프로파일: +# APPLE_CERTIFICATE_BASE64: .p12 인증서 (base64 인코딩) +# APPLE_CERTIFICATE_PASSWORD: .p12 인증서 비밀번호 +# APPLE_PROVISIONING_PROFILE_BASE64: .mobileprovision 파일 (base64 인코딩) +# IOS_PROVISIONING_PROFILE_NAME: 프로비저닝 프로파일 이름 # -# 🔑 App Store Connect API (필수): -# - APP_STORE_CONNECT_API_KEY_ID : API Key ID (10자리, 예: ABC123DEF4) -# - APP_STORE_CONNECT_ISSUER_ID : Issuer ID (UUID 형식) -# - APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 파일 (base64 인코딩) +# 🔑 App Store Connect API: +# APP_STORE_CONNECT_API_KEY_ID: API Key ID (10자리) +# APP_STORE_CONNECT_ISSUER_ID: Issuer ID (UUID 형식) +# APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 파일 (base64 인코딩) # # 📝 환경 설정 (선택): -# - ENV_FILE : .env 파일 내용 (앱에서 사용하는 환경변수) -# - SECRETS_XCCONFIG : ios/Flutter/Secrets.xcconfig 내용 (선택) +# ENV_FILE (선택): .env 파일 내용 +# SECRETS_XCCONFIG (선택): ios/Flutter/Secrets.xcconfig 내용 +# GOOGLE_MAPS_API_KEY (선택): Google Maps API 키 (SECRETS_XCCONFIG 없을 때 사용) # # =================================================================== @@ -51,9 +53,9 @@ on: repository_dispatch: types: [build-ios-app] -# ============================================ +# =================================================================== # 🔧 프로젝트별 설정 (아래 값들을 수정하세요) -# ============================================ +# =================================================================== env: FLUTTER_VERSION: "3.35.5" XCODE_VERSION: "26.0" # macos-26 runner + iOS 26 SDK (TestFlight 경고 90725 대응 - 2026년 4월 필수) @@ -61,9 +63,130 @@ env: ENV_FILE_PATH: ".env" jobs: + # ============================================ + # 즉시 댓글 생성 (ubuntu-latest로 빠르게 실행) + # ============================================ + notify-start: + name: 진행 상황 알림 + runs-on: ubuntu-latest + if: github.event_name == 'repository_dispatch' + + outputs: + comment_id: ${{ steps.progress.outputs.comment_id }} + start_time: ${{ steps.progress.outputs.start_time }} + branch_valid: ${{ steps.verify_branch.outputs.valid }} + + steps: + - name: 브랜치 존재 여부 사전 확인 + id: verify_branch + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branchName = '${{ github.event.client_payload.branch_name }}'; + const prNumber = '${{ github.event.client_payload.pr_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`); + + try { + await github.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: branchName + }); + console.log(`✅ 브랜치 존재 확인됨: ${branchName}`); + core.setOutput('valid', 'true'); + } catch (error) { + if (error.status === 404) { + console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`); + + const body = [ + '🍎 ❌ **iOS TestFlight 빌드 실패 - 브랜치를 찾을 수 없습니다**', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **요청된 브랜치** | \`${branchName}\` |`, + `| **이슈/PR** | #${prNumber} |`, + '', + '### 💡 확인 사항', + '1. 브랜치가 원격 저장소에 push되었는지 확인하세요', + '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요', + '3. 브랜치명에 오타가 없는지 확인하세요', + '', + `🔗 [워크플로우 로그](${runUrl})` + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(prNumber), + body: body + }); + + core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`); + } else { + core.setFailed(`브랜치 확인 중 오류: ${error.message}`); + } + } + + - name: 진행 상황 댓글 생성 + id: progress + if: github.event.client_payload.pr_number != '' + continue-on-error: true + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + const startTime = Date.now(); + + const body = [ + '## 🍎 iOS TestFlight 빌드 진행 중...', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + '| 🔧 준비 | ⏳ 진행 중... | - |', + '| 🔨 IPA 빌드 | ⏸️ 대기 | - |', + '| 📤 TestFlight 배포 | ⏸️ 대기 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + `**[📋 실시간 로그 보기](${runUrl})**`, + '', + '---', + '*🤖 이 댓글은 자동으로 업데이트됩니다.*' + ].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); + + # ============================================ + # 테스트 빌드 준비 (macOS 환경 설정) + # ============================================ prepare-test-build: name: 테스트 빌드 준비 - runs-on: macos-26 # iOS 26 SDK 지원 (2026년 4월 App Store 정책 대응) + runs-on: macos-26 + needs: notify-start + if: | + always() && + (needs.notify-start.result == 'skipped' || needs.notify-start.outputs.branch_valid == 'true') outputs: version: ${{ steps.test_version.outputs.version }} @@ -87,24 +210,19 @@ jobs: - name: Select Xcode 26 and verify SDK run: | - # Xcode 26 명시적 선택 sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer || true - # 현재 Xcode 버전 확인 echo "📱 Current Xcode version:" xcodebuild -version - # 사용 가능한 iOS SDK 목록 echo "" echo "📦 Available iOS SDKs:" xcodebuild -showsdks | grep "iOS" - # xcode-select 경로 확인 echo "" echo "🎯 xcode-select path:" xcode-select -p - # iOS 26 SDK 존재 여부 확인 (필수) echo "" if xcodebuild -showsdks | grep -q "iOS 26"; then echo "✅ iOS 26 SDK available - TestFlight warning 90725 will be resolved" @@ -118,10 +236,11 @@ jobs: - name: Create .env file run: | cat << 'EOF' > ${{ env.ENV_FILE_PATH }} - ${{ secrets.ENV_FILE }} + ${{ secrets.ENV_FILE || secrets.ENV }} EOF echo "✅ ${{ env.ENV_FILE_PATH }} file created" + # Secrets.xcconfig 생성 (IOS-TESTFLIGHT.yaml과 동일한 로직) - name: Create Secrets.xcconfig file run: | mkdir -p ios/Flutter @@ -146,7 +265,6 @@ jobs: run: | echo "🔒 보안 검증: API 키가 노출되지 않았는지 확인" - # iOS Secrets.xcconfig 확인 if [ -f ios/Flutter/Secrets.xcconfig ]; then echo "✅ ios/Flutter/Secrets.xcconfig exists" if grep -q "GOOGLE_MAPS_API_KEY" ios/Flutter/Secrets.xcconfig; then @@ -290,15 +408,126 @@ jobs: assets/ retention-days: 1 + # 진행 상황 업데이트 - 준비 완료 + - name: 진행 상황 업데이트 - 준비 완료 + if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + const elapsed = now - startTime; + const minutes = Math.floor(elapsed / 60000); + const seconds = Math.floor((elapsed % 60000) / 1000); + const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; + + const body = [ + '## 🍎 iOS TestFlight 빌드 진행 중...', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${duration} |`, + '| 🔨 IPA 빌드 | ⏳ 진행 중... | - |', + '| 📤 TestFlight 배포 | ⏸️ 대기 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + `**[📋 실시간 로그 보기](${runUrl})**`, + '', + '---', + '*🤖 이 댓글은 자동으로 업데이트됩니다.*' + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`✅ 진행 상황 업데이트 완료: 준비 완료 (${duration})`); + + # 진행 상황 업데이트 - 준비 실패 + - name: 진행 상황 업데이트 - 준비 실패 + if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + const elapsed = now - startTime; + const minutes = Math.floor(elapsed / 60000); + const seconds = Math.floor((elapsed % 60000) / 1000); + const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`; + + const body = [ + '## 🍎 ❌ iOS TestFlight 빌드 실패', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ❌ 실패 | ${duration} |`, + '| 🔨 IPA 빌드 | ⏸️ 취소됨 | - |', + '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + '❌ **환경 준비 중 오류가 발생했습니다.**', + '', + `🔗 [워크플로우 실행 로그 확인](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`❌ 진행 상황 업데이트 완료: 준비 실패`); + # ============================================ # iOS 빌드 (xcodebuild 직접 사용) # ============================================ build-ios-test: name: iOS 테스트 빌드 - runs-on: macos-26 # iOS 26 SDK 지원 (2026년 4월 App Store 정책 대응) - needs: prepare-test-build + runs-on: macos-26 + needs: [notify-start, prepare-test-build] + if: | + !cancelled() && + needs.prepare-test-build.result == 'success' + + outputs: + build_start: ${{ steps.build_start.outputs.time }} 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 with: @@ -310,24 +539,19 @@ jobs: - name: Select Xcode 26 and verify SDK run: | - # Xcode 26 명시적 선택 sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer || true - # 현재 Xcode 버전 확인 echo "📱 Current Xcode version:" xcodebuild -version - # 사용 가능한 iOS SDK 목록 echo "" echo "📦 Available iOS SDKs:" xcodebuild -showsdks | grep "iOS" - # xcode-select 경로 확인 echo "" echo "🎯 xcode-select path:" xcode-select -p - # iOS 26 SDK 존재 여부 확인 (필수) echo "" if xcodebuild -showsdks | grep -q "iOS 26"; then echo "✅ iOS 26 SDK available - TestFlight warning 90725 will be resolved" @@ -348,7 +572,7 @@ jobs: run: | if [ ! -f "${{ env.ENV_FILE_PATH }}" ]; then cat << 'EOF' > ${{ env.ENV_FILE_PATH }} - ${{ secrets.ENV_FILE }} + ${{ secrets.ENV_FILE || secrets.ENV }} EOF echo "✅ ${{ env.ENV_FILE_PATH }} file created (fallback)" fi @@ -454,15 +678,143 @@ jobs: build-metadata.json retention-days: 1 + # 진행 상황 업데이트 - IPA 빌드 완료 + - name: 진행 상황 업데이트 - IPA 빌드 완료 + if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const prepareStart = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = now - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + const body = [ + '## 🍎 ✅ iOS 빌드 완료', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 IPA 빌드 | ✅ 완료 | ${buildDuration} |`, + '| 📤 TestFlight 배포 | ⏳ 진행 중... | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + '', + `**[📋 실시간 로그 보기](${runUrl})**`, + '', + '---', + '*🤖 이 댓글은 자동으로 업데이트됩니다.*' + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`✅ 진행 상황 업데이트 완료: IPA 빌드 완료 (${buildDuration})`); + + # 빌드 실패 시 progress 댓글 업데이트 + - name: 빌드 실패 시 progress 댓글 업데이트 + if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const prepareStart = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const buildStart = parseInt('${{ steps.build_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = now - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + const body = [ + '## 🍎 ❌ iOS TestFlight 빌드 실패', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 IPA 빌드 | ❌ 실패 | ${buildDuration} |`, + '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + `| **커밋** | \`${commitHash}\` |`, + '', + '❌ **IPA 빌드 중 오류가 발생했습니다.**', + '', + `🔗 [워크플로우 실행 로그 확인](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`❌ 진행 상황 업데이트 완료: IPA 빌드 실패`); + # ============================================ # TestFlight 배포 (마법사 생성 Fastfile 사용) # ============================================ deploy-testflight-test: name: TestFlight 테스트 배포 - runs-on: macos-15 - needs: [prepare-test-build, build-ios-test] + runs-on: macos-26 + needs: [notify-start, prepare-test-build, build-ios-test] + if: | + !cancelled() && + needs.build-ios-test.result == 'success' steps: + # 배포 시작 시간 기록 + - name: 배포 시작 시간 기록 + id: deploy_start + uses: actions/github-script@v7 + with: + script: | + core.setOutput('time', Date.now().toString()); + - name: Checkout repository uses: actions/checkout@v4 with: @@ -496,7 +848,7 @@ jobs: fi echo "✅ Fastfile 확인됨: ios/fastlane/Fastfile" - # Fastlane 설치 (Bundler 방식으로 CFPropertyList 의존성 충돌 방지) + # Fastlane 설치 (IOS-TESTFLIGHT.yaml과 동일한 Bundler 방식) - name: Install Fastlane run: | printf 'source "https://rubygems.org"\ngem "fastlane"\n' > Gemfile @@ -563,114 +915,142 @@ jobs: echo "❌ TestFlight 테스트 빌드 업로드 실패!" echo "로그를 확인해주세요." - # PR/ISSUE에 빌드 완료 댓글 작성 (repository_dispatch로 트리거된 경우) - - name: 빌드 성공 댓글 작성 - if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' + # 진행 상황 최종 업데이트 - 성공 + - name: 진행 상황 최종 업데이트 - 성공 + if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const sourceType = '${{ github.event.client_payload.source_type }}' || 'PR'; - const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); - const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}'; - const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}'; + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const prepareStart = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const buildStart = parseInt('${{ needs.build-ios-test.outputs.build_start }}'); + const deployStart = parseInt('${{ steps.deploy_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}'; - const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}'; const runId = '${{ github.run_id }}'; const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - const body = `🍎 ✅ **iOS TestFlight 빌드 완료** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 브랜치: \`${branchName}\` - - 커밋: \`${commitHash}\` - - 📱 **TestFlight에서 테스트 앱을 다운로드할 수 있습니다.** - - 🔗 [워크플로우 실행 로그](${runUrl})`; - - if (sourceType === 'PR') { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ PR #${prNumber}에 빌드 성공 댓글 작성 완료`); - - if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) { - const issueBody = `🍎 ✅ **iOS TestFlight 빌드 완료** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 커밋: \`${commitHash}\` - - 📱 **TestFlight에서 테스트 앱을 다운로드할 수 있습니다.**`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(issueNumber), - body: issueBody - }); - console.log(`✅ 이슈 #${issueNumber}에도 빌드 성공 댓글 작성 완료`); - } - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ ${sourceType} #${prNumber}에 빌드 성공 댓글 작성 완료`); - } - - - name: 빌드 실패 댓글 작성 - if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = deployStart - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + // 배포 단계 소요 시간 + const deployElapsed = now - deployStart; + const deployMin = Math.floor(deployElapsed / 60000); + const deploySec = Math.floor((deployElapsed % 60000) / 1000); + const deployDuration = deployMin > 0 ? `${deployMin}분 ${deploySec}초` : `${deploySec}초`; + + // 전체 소요 시간 + const totalElapsed = now - prepareStart; + const totalMin = Math.floor(totalElapsed / 60000); + const totalSec = Math.floor((totalElapsed % 60000) / 1000); + const totalDuration = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`; + + const body = [ + '## 🍎 ✅ iOS TestFlight 배포 완료', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 IPA 빌드 | ✅ 완료 | ${buildDuration} |`, + `| 📤 TestFlight 배포 | ✅ 완료 | ${deployDuration} |`, + '', + `**총 소요 시간: ${totalDuration}**`, + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + `| **커밋** | \`${commitHash}\` |`, + '', + '📱 **TestFlight에서 테스트 앱을 다운로드할 수 있습니다.**', + '', + `🔗 [워크플로우 실행 로그](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`✅ 진행 상황 최종 업데이트 완료: 전체 성공 (${totalDuration})`); + + # 진행 상황 최종 업데이트 - 실패 + - name: 진행 상황 최종 업데이트 - 실패 + if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != '' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const sourceType = '${{ github.event.client_payload.source_type }}' || 'PR'; - const prNumber = parseInt('${{ github.event.client_payload.pr_number }}'); - const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}'; - const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}'; - const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}'; + const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}'); + const prepareStart = parseInt('${{ needs.notify-start.outputs.start_time }}'); + const buildStart = parseInt('${{ needs.build-ios-test.outputs.build_start }}'); + const deployStart = parseInt('${{ steps.deploy_start.outputs.time }}'); + const branchName = '${{ github.event.client_payload.branch_name }}'; + const buildNumber = '${{ github.event.client_payload.build_number }}'; + const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}'; const runId = '${{ github.run_id }}'; const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - const body = `🍎 ❌ **iOS TestFlight 빌드 실패** - - - 앱 버전: \`0.0.0(${buildNumber})\` - - 브랜치: \`${branchName}\` - - ❌ 빌드 중 오류가 발생했습니다. - - 🔗 [워크플로우 실행 로그 확인](${runUrl})`; - - if (sourceType === 'PR') { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ PR #${prNumber}에 빌드 실패 댓글 작성 완료`); - - if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(issueNumber), - body: body - }); - console.log(`✅ 이슈 #${issueNumber}에도 빌드 실패 댓글 작성 완료`); - } - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - console.log(`✅ ${sourceType} #${prNumber}에 빌드 실패 댓글 작성 완료`); - } + const now = Date.now(); + + // 준비 단계 소요 시간 + const prepareElapsed = buildStart - prepareStart; + const prepareMin = Math.floor(prepareElapsed / 60000); + const prepareSec = Math.floor((prepareElapsed % 60000) / 1000); + const prepareDuration = prepareMin > 0 ? `${prepareMin}분 ${prepareSec}초` : `${prepareSec}초`; + + // 빌드 단계 소요 시간 + const buildElapsed = deployStart - buildStart; + const buildMin = Math.floor(buildElapsed / 60000); + const buildSec = Math.floor((buildElapsed % 60000) / 1000); + const buildDuration = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`; + + // 배포 단계 소요 시간 + const deployElapsed = now - deployStart; + const deployMin = Math.floor(deployElapsed / 60000); + const deploySec = Math.floor((deployElapsed % 60000) / 1000); + const deployDuration = deployMin > 0 ? `${deployMin}분 ${deploySec}초` : `${deploySec}초`; + + const body = [ + '## 🍎 ❌ iOS TestFlight 배포 실패', + '', + '| 단계 | 상태 | 소요 시간 |', + '|------|------|----------|', + `| 🔧 준비 | ✅ 완료 | ${prepareDuration} |`, + `| 🔨 IPA 빌드 | ✅ 완료 | ${buildDuration} |`, + `| 📤 TestFlight 배포 | ❌ 실패 | ${deployDuration} |`, + '', + '| 항목 | 값 |', + '|------|-----|', + `| **앱 버전** | \`0.0.0(${buildNumber})\` |`, + `| **브랜치** | \`${branchName}\` |`, + `| **커밋** | \`${commitHash}\` |`, + '', + '❌ **TestFlight 배포 중 오류가 발생했습니다.**', + '', + `🔗 [워크플로우 실행 로그 확인](${runUrl})` + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: body + }); + + console.log(`❌ 진행 상황 최종 업데이트 완료: TestFlight 배포 실패`); diff --git a/.github/workflows/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml b/.github/workflows/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml index c693b4f..837b2ae 100644 --- a/.github/workflows/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +++ b/.github/workflows/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml @@ -2,19 +2,21 @@ # Flutter 테스트 앱 빌드 트리거 워크플로우 (SUH-LAB) # =================================================================== # -# 이 워크플로우는 PR 또는 이슈에 댓글로 "@suh-lab build app"을 작성하면 +# 이 워크플로우는 PR 또는 이슈에 댓글로 빌드 명령어를 작성하면 # Android와 iOS 테스트 앱 빌드를 자동으로 트리거합니다. # # 주요 특징: -# - PR/이슈 댓글 감지: "@suh-lab build app" 키워드 감지 +# - PR/이슈 댓글 감지: "@suh-lab build app/apk build/ios build" 키워드 감지 # - repository_dispatch 이벤트로 빌드 워크플로우 트리거 # - PR/이슈 번호 + 빌드 횟수로 고유한 빌드 번호 생성 (예: 38700, 38701...) # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식) # - 테스트 버전 0.0.0 고정으로 운영 버전과 분리 # # 사용 방법: -# - PR에 댓글 작성: "@suh-lab build app" -# - 이슈에 댓글 작성: "@suh-lab build app" (Guide by SUH-LAB 댓글 필요) +# - PR에 댓글 작성: "@suh-lab build app" (Android + iOS 모두 빌드) +# - PR에 댓글 작성: "@suh-lab apk build" (Android만 빌드) +# - PR에 댓글 작성: "@suh-lab ios build" (iOS만 빌드) +# - 이슈에 댓글 작성: 위 명령어 동일 (Guide by SUH-LAB 댓글 필요) # # 트리거되는 워크플로우: # - PROJECT-FLUTTER-ANDROID-TEST-APK.yaml (event_type: build-android-app) @@ -36,11 +38,14 @@ permissions: jobs: trigger-builds: name: 빌드 워크플로우 트리거 - # @suh-lab, build, app 키워드가 모두 포함되어 있으면 실행 + # @suh-lab build app / @suh-lab apk build / @suh-lab ios build 키워드 감지 if: | contains(github.event.comment.body, '@suh-lab') && - contains(github.event.comment.body, 'build') && - contains(github.event.comment.body, 'app') + ( + (contains(github.event.comment.body, 'build') && contains(github.event.comment.body, 'app')) || + (contains(github.event.comment.body, 'apk') && contains(github.event.comment.body, 'build')) || + (contains(github.event.comment.body, 'ios') && contains(github.event.comment.body, 'build')) + ) runs-on: ubuntu-latest steps: @@ -60,6 +65,39 @@ jobs: console.log('👀 댓글에 확인 리액션 추가 완료'); + - name: 빌드 타입 판별 + id: build_type + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const comment = context.payload.comment.body.toLowerCase(); + + let buildAndroid = false; + let buildIos = false; + let buildType = ''; + + if (comment.includes('build') && comment.includes('app')) { + // @suh-lab build app → 양쪽 모두 + buildAndroid = true; + buildIos = true; + buildType = 'app'; + } else if (comment.includes('apk') && comment.includes('build')) { + // @suh-lab apk build → Android만 + buildAndroid = true; + buildType = 'apk'; + } else if (comment.includes('ios') && comment.includes('build')) { + // @suh-lab ios build → iOS만 + buildIos = true; + buildType = 'ios'; + } + + core.setOutput('buildAndroid', buildAndroid.toString()); + core.setOutput('buildIos', buildIos.toString()); + core.setOutput('buildType', buildType); + + console.log(`📱 빌드 타입: ${buildType} (Android=${buildAndroid}, iOS=${buildIos})`); + - name: PR/이슈 정보 확인 및 추출 id: source_info uses: actions/github-script@v7 @@ -167,14 +205,85 @@ jobs: const issueNumber = context.payload.issue.number; const errorMessage = '${{ steps.source_info.outputs.errorMessage }}'; - const body = `❌ **앱 빌드 트리거 실패** + const body = [ + '❌ **앱 빌드 트리거 실패**', + '', + errorMessage, + '', + '**해결 방법:**', + '- PR에서 빌드하는 경우: PR 페이지에서 댓글을 작성해주세요.', + '- 이슈에서 빌드하는 경우: "Guide by SUH-LAB" 댓글이 있어야 합니다.', + ' - 댓글에 `### 브랜치` 섹션과 브랜치명이 포함되어 있어야 합니다.' + ].join('\n'); - ${errorMessage} + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body + }); + + core.setFailed(errorMessage); - **해결 방법:** - - PR에서 빌드하는 경우: PR 페이지에서 댓글을 작성해주세요. - - 이슈에서 빌드하는 경우: "Guide by SUH-LAB" 댓글이 있어야 합니다. - - 댓글에 \`### 브랜치\` 섹션과 브랜치명이 포함되어 있어야 합니다.`; + - name: 브랜치 존재 여부 확인 + id: check_branch + if: steps.source_info.outputs.found == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branchName = '${{ steps.source_info.outputs.branchName }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`); + + try { + await github.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: branchName + }); + console.log(`✅ 브랜치 존재 확인됨: ${branchName}`); + core.setOutput('exists', 'true'); + } catch (error) { + if (error.status === 404) { + console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`); + core.setOutput('exists', 'false'); + core.setOutput('errorMessage', `브랜치 \`${branchName}\`를 찾을 수 없습니다.`); + } else { + console.log(`❌ 브랜치 확인 중 오류: ${error.message}`); + core.setOutput('exists', 'false'); + core.setOutput('errorMessage', `브랜치 확인 중 오류가 발생했습니다: ${error.message}`); + } + } + + - name: 브랜치가 존재하지 않는 경우 에러 댓글 작성 + if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'false' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueNumber = context.payload.issue.number; + const branchName = '${{ steps.source_info.outputs.branchName }}'; + const runId = '${{ github.run_id }}'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const body = [ + '❌ **앱 빌드 트리거 실패 - 브랜치를 찾을 수 없습니다**', + '', + '| 항목 | 값 |', + '|------|-----|', + `| **요청된 브랜치** | \`${branchName}\` |`, + `| **이슈/PR** | #${issueNumber} |`, + '', + '### 💡 확인 사항', + '1. 브랜치가 원격 저장소에 push되었는지 확인하세요', + '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요', + '3. 브랜치명에 오타가 없는지 확인하세요', + '', + `🔗 [워크플로우 로그](${runUrl})` + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, @@ -183,19 +292,21 @@ jobs: body: body }); - core.setFailed(errorMessage); + core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`); - name: 빌드 횟수 조회 및 빌드 번호 생성 id: build_number_calc - if: steps.source_info.outputs.found == 'true' + if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const sourceNumber = parseInt('${{ steps.source_info.outputs.sourceNumber }}'); const sourceType = '${{ steps.source_info.outputs.sourceType }}'; + const buildType = '${{ steps.build_type.outputs.buildType }}'; console.log(`📊 ${sourceType} #${sourceNumber}의 빌드 횟수 조회 중...`); + console.log(`📱 빌드 타입: ${buildType}`); // PR/이슈의 모든 댓글 조회 const comments = await github.rest.issues.listComments({ @@ -205,22 +316,61 @@ jobs: per_page: 100 }); - // "앱 빌드 트리거 완료" 댓글 개수 = 이전 빌드 횟수 - const buildCount = comments.data.filter(c => - c.body.includes('앱 빌드 트리거 완료') - ).length; + // 빌드 타입에 따라 해당 플랫폼의 빌드 횟수 카운트 + // iOS: 'ios build' + 'build app' 통합 카운트 (App Store Connect는 앱 전체에서 빌드 번호 unique) + // Android: 'apk build' + 'build app' 통합 카운트 (Play Store도 동일) + // App: 모든 빌드 명령어 통합 카운트 + let buildCount = 0; + + if (buildType === 'ios') { + // iOS 빌드: 'ios build' + 'build app' 둘 다 카운트 + buildCount = comments.data.filter(c => { + const body = c.body.toLowerCase(); + return body.includes('@suh-lab') && ( + body.includes('ios build') || + (body.includes('build') && body.includes('app')) + ); + }).length; + console.log(`📊 iOS 빌드 카운트: 'ios build' + 'build app' 통합`); + + } else if (buildType === 'apk') { + // Android 빌드: 'apk build' + 'build app' 둘 다 카운트 + buildCount = comments.data.filter(c => { + const body = c.body.toLowerCase(); + return body.includes('@suh-lab') && ( + body.includes('apk build') || + (body.includes('build') && body.includes('app')) + ); + }).length; + console.log(`📊 Android 빌드 카운트: 'apk build' + 'build app' 통합`); + + } else if (buildType === 'app') { + // 전체 앱 빌드: 모든 빌드 명령어 카운트 + buildCount = comments.data.filter(c => { + const body = c.body.toLowerCase(); + return body.includes('@suh-lab') && ( + body.includes('ios build') || + body.includes('apk build') || + (body.includes('build') && body.includes('app')) + ); + }).length; + console.log(`📊 전체 앱 빌드 카운트: 모든 빌드 명령어 통합`); + } // 빌드 번호: 소스번호 + 2자리 카운트 (38800, 38801, 38802...) const buildNumber = `${sourceNumber}${String(buildCount).padStart(2, '0')}`; - console.log(`📊 ${sourceType} #${sourceNumber}: ${buildCount}번째 빌드 (이전 빌드 ${buildCount}회)`); + console.log(`📊 ${sourceType} #${sourceNumber}: ${buildType} ${buildCount}번째 빌드`); console.log(`📦 생성된 빌드 번호: ${buildNumber}`); core.setOutput('buildCount', buildCount.toString()); core.setOutput('buildNumber', buildNumber); - name: Android 빌드 워크플로우 트리거 - if: steps.source_info.outputs.found == 'true' + if: | + steps.source_info.outputs.found == 'true' && + steps.check_branch.outputs.exists == 'true' && + steps.build_type.outputs.buildAndroid == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -255,7 +405,10 @@ jobs: console.log('✅ Android 빌드 워크플로우 트리거 완료'); - name: iOS 빌드 워크플로우 트리거 - if: steps.source_info.outputs.found == 'true' + if: | + steps.source_info.outputs.found == 'true' && + steps.check_branch.outputs.exists == 'true' && + steps.build_type.outputs.buildIos == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -289,33 +442,11 @@ jobs: console.log('✅ iOS 빌드 워크플로우 트리거 완료'); - - name: 트리거 완료 댓글 작성 - if: steps.source_info.outputs.found == 'true' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const sourceType = '${{ steps.source_info.outputs.sourceType }}'; - const sourceNumber = '${{ steps.source_info.outputs.sourceNumber }}'; - const branchName = '${{ steps.source_info.outputs.branchName }}'; - const buildNumber = '${{ steps.build_number_calc.outputs.buildNumber }}'; - const buildCount = '${{ steps.build_number_calc.outputs.buildCount }}'; - - const body = `🚀 **앱 빌드 트리거 완료** - - Android와 iOS 빌드 워크플로우를 시작했습니다. - - - 앱 버전: \`0.0.0(${buildNumber})\` - - PR/ISSUE: **#${sourceNumber}** (${buildCount}번째 빌드) - - 브랜치: \`${branchName}\` - - ⏳ 빌드가 완료되면 자동으로 결과 댓글이 작성됩니다.`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(sourceNumber), - body: body - }); - - console.log('✅ 트리거 완료 댓글 작성 완료'); + # 트리거 완료 댓글은 제거됨 - Android/iOS 각각의 진행상황 댓글이 이 역할을 대체 + - name: 트리거 완료 로그 + if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'true' + run: | + echo "✅ Android 및 iOS 빌드 워크플로우 트리거 완료" + echo "📦 빌드 번호: ${{ steps.build_number_calc.outputs.buildNumber }}" + echo "🌿 브랜치: ${{ steps.source_info.outputs.branchName }}" + echo "📋 각 플랫폼별 진행상황 댓글이 자동으로 생성됩니다." From 7a71cfbe36c5370a3b19c7067983594797e35f09 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 Mar 2026 05:44:05 +0000 Subject: [PATCH 03/20] =?UTF-8?q?SpaceStudyShip-FE=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=201.1.32=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 80aa84e..9c73162 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: space_study_ship description: "Space Study Ship - 우주 테마 게이미피케이션 학습 관리 앱" publish_to: "none" -version: 1.1.31+75 +version: 1.1.32+76 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 85afcf6..8aa23d6 100644 --- a/version.yml +++ b/version.yml @@ -34,12 +34,12 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.1.31" # 사용자에게 표시되는 버전 -version_code: 76 # app build number +version: "1.1.32" # 사용자에게 표시되는 버전 +version_code: 77 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-03-05 00:17:48" - last_updated_by: "EM-H20" + last_updated: "2026-03-05 05:44:05" + last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" integration_date: "2026-02-06" From de4feda0c0ccfa544a9d06573d7a862537a8421d Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Thu, 5 Mar 2026 14:48:46 +0900 Subject: [PATCH 04/20] =?UTF-8?q?@suh-lab=20=EB=8C=93=EA=B8=80=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=EC=9E=91=EB=8F=99=20:=20fix=20:=20=EB=8C=93=EA=B8=80=20write?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80=20https://github.com?= =?UTF-8?q?/SpaceStudyShip/SpaceStudyShip-FE/issues/49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml | 4 ++++ .github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml index 6279a24..21d7d84 100644 --- a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +++ b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml @@ -38,6 +38,10 @@ on: repository_dispatch: types: [build-android-app] +permissions: + issues: write + pull-requests: write + # ============================================ # 🔧 프로젝트별 설정 (아래 값들을 수정하세요) # ============================================ diff --git a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml index fd8f94e..675f14b 100644 --- a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +++ b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml @@ -51,6 +51,10 @@ on: repository_dispatch: types: [build-ios-app] +permissions: + issues: write + pull-requests: write + # ============================================ # 🔧 프로젝트별 설정 (아래 값들을 수정하세요) # ============================================ From da495dd10f12f0a9a72a7864cb7d2048fcff37f4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 Mar 2026 05:49:54 +0000 Subject: [PATCH 05/20] =?UTF-8?q?SpaceStudyShip-FE=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=201.1.33=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9c73162..c8efd9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: space_study_ship description: "Space Study Ship - 우주 테마 게이미피케이션 학습 관리 앱" publish_to: "none" -version: 1.1.32+76 +version: 1.1.33+77 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 8aa23d6..3739595 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.1.32" # 사용자에게 표시되는 버전 -version_code: 77 # app build number +version: "1.1.33" # 사용자에게 표시되는 버전 +version_code: 78 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-03-05 05:44:05" + last_updated: "2026-03-05 05:49:54" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" From c8907b0ea15c2f662814a79f2155925085ead462 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Thu, 5 Mar 2026 15:06:19 +0900 Subject: [PATCH 06/20] =?UTF-8?q?@suh-lab=20=EB=8C=93=EA=B8=80=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=EC=9E=91=EB=8F=99=20:=20fix=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20step=20=EC=A0=9C=EA=B1=B0=20https://github.com/Spac?= =?UTF-8?q?eStudyShip/SpaceStudyShip-FE/issues/49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PROJECT-FLUTTER-ANDROID-TEST-APK.yaml | 56 ++++--------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml index 2a8fdb2..9858f9d 100644 --- a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +++ b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml @@ -452,49 +452,18 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - # 프로젝트 의존성 설치 - - name: Install dependencies - run: | - flutter pub get - echo "✅ Dependencies installed" - - # Gradle 셋업 - - name: Setup Gradle - working-directory: android - run: | - chmod +x gradlew - echo "✅ Gradle wrapper permissions set" - # Java 설정 - name: Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: ${{ env.JAVA_VERSION }} - - name: Verify Java version - run: | - echo "✅ Java setup completed" - java -version - - # Ruby 설정 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.4.1" - bundler-cache: true - - - name: Verify Ruby version - run: | - echo "✅ Ruby setup completed" - ruby -v - - # Fastlane 설치 - - name: Install Fastlane + # 프로젝트 의존성 설치 + - name: Install dependencies run: | - gem install fastlane --force - echo "✅ Fastlane installed" - fastlane --version + flutter pub get + echo "✅ Dependencies installed" # APK 파일명 생성 - name: Generate APK filename @@ -506,18 +475,13 @@ jobs: echo "apk_name=$APK_NAME" >> $GITHUB_OUTPUT echo "📦 APK 파일명: $APK_NAME" - # APK 빌드 (Fastlane 또는 직접 빌드) + # APK 빌드 - name: Build APK run: | - # Fastlane Fastfile이 있으면 사용, 없으면 직접 빌드 - if [ -f "android/fastlane/Fastfile" ]; then - echo "📦 Fastlane을 사용하여 빌드..." - cd android - fastlane build --verbose || flutter build apk --release - else - echo "📦 Flutter 직접 빌드..." - flutter build apk --release - fi + BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}" + flutter build apk --release \ + --build-name="0.0.0" \ + --build-number="$BUILD_NUMBER" ls -la ./build/app/outputs/flutter-apk/ || true echo "✅ APK built" From 22438f37cb96a0a7d5bb146bd4761a74429b4745 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 Mar 2026 06:06:07 +0000 Subject: [PATCH 07/20] =?UTF-8?q?SpaceStudyShip-FE=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=201.1.34=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index c8efd9f..f2f3115 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: space_study_ship description: "Space Study Ship - 우주 테마 게이미피케이션 학습 관리 앱" publish_to: "none" -version: 1.1.33+77 +version: 1.1.34+78 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 3739595..4760fbf 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.1.33" # 사용자에게 표시되는 버전 -version_code: 78 # app build number +version: "1.1.34" # 사용자에게 표시되는 버전 +version_code: 79 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-03-05 05:49:54" + last_updated: "2026-03-05 06:06:06" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" From 81a209137265b596c2d3bd0d6d79f8fa92744eea Mon Sep 17 00:00:00 2001 From: EM-H20 Date: Mon, 9 Mar 2026 13:37:01 +0900 Subject: [PATCH 08/20] =?UTF-8?q?docs=20:=20=EC=8A=A4=ED=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20CLAUDE.md=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/find-skills/SKILL.md | 133 ++++++ .agents/skills/frontend-design/LICENSE.txt | 177 ++++++++ .agents/skills/frontend-design/SKILL.md | 42 ++ .../skills/remotion-best-practices/SKILL.md | 61 +++ .../remotion-best-practices/rules/3d.md | 86 ++++ .../rules/animations.md | 27 ++ .../remotion-best-practices/rules/assets.md | 78 ++++ .../rules/assets/charts-bar-chart.tsx | 173 ++++++++ .../assets/text-animations-typewriter.tsx | 100 +++++ .../assets/text-animations-word-highlight.tsx | 103 +++++ .../rules/audio-visualization.md | 198 +++++++++ .../remotion-best-practices/rules/audio.md | 169 +++++++ .../rules/calculate-metadata.md | 134 ++++++ .../rules/can-decode.md | 75 ++++ .../remotion-best-practices/rules/charts.md | 120 +++++ .../rules/compositions.md | 154 +++++++ .../rules/display-captions.md | 184 ++++++++ .../rules/extract-frames.md | 229 ++++++++++ .../remotion-best-practices/rules/ffmpeg.md | 38 ++ .../remotion-best-practices/rules/fonts.md | 152 +++++++ .../rules/get-audio-duration.md | 58 +++ .../rules/get-video-dimensions.md | 68 +++ .../rules/get-video-duration.md | 60 +++ .../remotion-best-practices/rules/gifs.md | 141 ++++++ .../remotion-best-practices/rules/images.md | 134 ++++++ .../rules/import-srt-captions.md | 69 +++ .../rules/light-leaks.md | 73 ++++ .../remotion-best-practices/rules/lottie.md | 70 +++ .../remotion-best-practices/rules/maps.md | 412 ++++++++++++++++++ .../rules/measuring-dom-nodes.md | 34 ++ .../rules/measuring-text.md | 140 ++++++ .../rules/parameters.md | 109 +++++ .../rules/sequencing.md | 118 +++++ .../remotion-best-practices/rules/sfx.md | 26 ++ .../rules/subtitles.md | 36 ++ .../remotion-best-practices/rules/tailwind.md | 11 + .../rules/text-animations.md | 20 + .../remotion-best-practices/rules/timing.md | 179 ++++++++ .../rules/transcribe-captions.md | 70 +++ .../rules/transitions.md | 197 +++++++++ .../rules/transparent-videos.md | 106 +++++ .../remotion-best-practices/rules/trimming.md | 51 +++ .../remotion-best-practices/rules/videos.md | 171 ++++++++ .../rules/voiceover.md | 99 +++++ CLAUDE.md | 8 + skills-lock.json | 20 + 46 files changed, 4913 insertions(+) create mode 100644 .agents/skills/find-skills/SKILL.md create mode 100644 .agents/skills/frontend-design/LICENSE.txt create mode 100644 .agents/skills/frontend-design/SKILL.md create mode 100644 .agents/skills/remotion-best-practices/SKILL.md create mode 100644 .agents/skills/remotion-best-practices/rules/3d.md create mode 100644 .agents/skills/remotion-best-practices/rules/animations.md create mode 100644 .agents/skills/remotion-best-practices/rules/assets.md create mode 100644 .agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx create mode 100644 .agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx create mode 100644 .agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx create mode 100644 .agents/skills/remotion-best-practices/rules/audio-visualization.md create mode 100644 .agents/skills/remotion-best-practices/rules/audio.md create mode 100644 .agents/skills/remotion-best-practices/rules/calculate-metadata.md create mode 100644 .agents/skills/remotion-best-practices/rules/can-decode.md create mode 100644 .agents/skills/remotion-best-practices/rules/charts.md create mode 100644 .agents/skills/remotion-best-practices/rules/compositions.md create mode 100644 .agents/skills/remotion-best-practices/rules/display-captions.md create mode 100644 .agents/skills/remotion-best-practices/rules/extract-frames.md create mode 100644 .agents/skills/remotion-best-practices/rules/ffmpeg.md create mode 100644 .agents/skills/remotion-best-practices/rules/fonts.md create mode 100644 .agents/skills/remotion-best-practices/rules/get-audio-duration.md create mode 100644 .agents/skills/remotion-best-practices/rules/get-video-dimensions.md create mode 100644 .agents/skills/remotion-best-practices/rules/get-video-duration.md create mode 100644 .agents/skills/remotion-best-practices/rules/gifs.md create mode 100644 .agents/skills/remotion-best-practices/rules/images.md create mode 100644 .agents/skills/remotion-best-practices/rules/import-srt-captions.md create mode 100644 .agents/skills/remotion-best-practices/rules/light-leaks.md create mode 100644 .agents/skills/remotion-best-practices/rules/lottie.md create mode 100644 .agents/skills/remotion-best-practices/rules/maps.md create mode 100644 .agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md create mode 100644 .agents/skills/remotion-best-practices/rules/measuring-text.md create mode 100644 .agents/skills/remotion-best-practices/rules/parameters.md create mode 100644 .agents/skills/remotion-best-practices/rules/sequencing.md create mode 100644 .agents/skills/remotion-best-practices/rules/sfx.md create mode 100644 .agents/skills/remotion-best-practices/rules/subtitles.md create mode 100644 .agents/skills/remotion-best-practices/rules/tailwind.md create mode 100644 .agents/skills/remotion-best-practices/rules/text-animations.md create mode 100644 .agents/skills/remotion-best-practices/rules/timing.md create mode 100644 .agents/skills/remotion-best-practices/rules/transcribe-captions.md create mode 100644 .agents/skills/remotion-best-practices/rules/transitions.md create mode 100644 .agents/skills/remotion-best-practices/rules/transparent-videos.md create mode 100644 .agents/skills/remotion-best-practices/rules/trimming.md create mode 100644 .agents/skills/remotion-best-practices/rules/videos.md create mode 100644 .agents/skills/remotion-best-practices/rules/voiceover.md create mode 100644 skills-lock.json diff --git a/.agents/skills/find-skills/SKILL.md b/.agents/skills/find-skills/SKILL.md new file mode 100644 index 0000000..c797184 --- /dev/null +++ b/.agents/skills/find-skills/SKILL.md @@ -0,0 +1,133 @@ +--- +name: find-skills +description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. +--- + +# Find Skills + +This skill helps you discover and install skills from the open agent skills ecosystem. + +## When to Use This Skill + +Use this skill when the user: + +- Asks "how do I do X" where X might be a common task with an existing skill +- Says "find a skill for X" or "is there a skill for X" +- Asks "can you do X" where X is a specialized capability +- Expresses interest in extending agent capabilities +- Wants to search for tools, templates, or workflows +- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) + +## What is the Skills CLI? + +The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. + +**Key commands:** + +- `npx skills find [query]` - Search for skills interactively or by keyword +- `npx skills add ` - Install a skill from GitHub or other sources +- `npx skills check` - Check for skill updates +- `npx skills update` - Update all installed skills + +**Browse skills at:** https://skills.sh/ + +## How to Help Users Find Skills + +### Step 1: Understand What They Need + +When a user asks for help with something, identify: + +1. The domain (e.g., React, testing, design, deployment) +2. The specific task (e.g., writing tests, creating animations, reviewing PRs) +3. Whether this is a common enough task that a skill likely exists + +### Step 2: Search for Skills + +Run the find command with a relevant query: + +```bash +npx skills find [query] +``` + +For example: + +- User asks "how do I make my React app faster?" → `npx skills find react performance` +- User asks "can you help me with PR reviews?" → `npx skills find pr review` +- User asks "I need to create a changelog" → `npx skills find changelog` + +The command will return results like: + +``` +Install with npx skills add + +vercel-labs/agent-skills@vercel-react-best-practices +└ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices +``` + +### Step 3: Present Options to the User + +When you find relevant skills, present them to the user with: + +1. The skill name and what it does +2. The install command they can run +3. A link to learn more at skills.sh + +Example response: + +``` +I found a skill that might help! The "vercel-react-best-practices" skill provides +React and Next.js performance optimization guidelines from Vercel Engineering. + +To install it: +npx skills add vercel-labs/agent-skills@vercel-react-best-practices + +Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices +``` + +### Step 4: Offer to Install + +If the user wants to proceed, you can install the skill for them: + +```bash +npx skills add -g -y +``` + +The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. + +## Common Skill Categories + +When searching, consider these common categories: + +| Category | Example Queries | +| --------------- | ---------------------------------------- | +| Web Development | react, nextjs, typescript, css, tailwind | +| Testing | testing, jest, playwright, e2e | +| DevOps | deploy, docker, kubernetes, ci-cd | +| Documentation | docs, readme, changelog, api-docs | +| Code Quality | review, lint, refactor, best-practices | +| Design | ui, ux, design-system, accessibility | +| Productivity | workflow, automation, git | + +## Tips for Effective Searches + +1. **Use specific keywords**: "react testing" is better than just "testing" +2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" +3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` + +## When No Skills Are Found + +If no relevant skills exist: + +1. Acknowledge that no existing skill was found +2. Offer to help with the task directly using your general capabilities +3. Suggest the user could create their own skill with `npx skills init` + +Example: + +``` +I searched for skills related to "xyz" but didn't find any matches. +I can still help you with this task directly! Would you like me to proceed? + +If this is something you do often, you could create your own skill: +npx skills init my-xyz-skill +``` diff --git a/.agents/skills/frontend-design/LICENSE.txt b/.agents/skills/frontend-design/LICENSE.txt new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/.agents/skills/frontend-design/LICENSE.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/.agents/skills/frontend-design/SKILL.md b/.agents/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..5be498e --- /dev/null +++ b/.agents/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/.agents/skills/remotion-best-practices/SKILL.md b/.agents/skills/remotion-best-practices/SKILL.md new file mode 100644 index 0000000..7ddaa0f --- /dev/null +++ b/.agents/skills/remotion-best-practices/SKILL.md @@ -0,0 +1,61 @@ +--- +name: remotion-best-practices +description: Best practices for Remotion - Video creation in React +metadata: + tags: remotion, video, react, animation, composition +--- + +## When to use + +Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge. + +## Captions + +When dealing with captions or subtitles, load the [./rules/subtitles.md](./rules/subtitles.md) file for more information. + +## Using FFmpeg + +For some video operations, such as trimming videos or detecting silence, FFmpeg should be used. Load the [./rules/ffmpeg.md](./rules/ffmpeg.md) file for more information. + +## Audio visualization + +When needing to visualize audio (spectrum bars, waveforms, bass-reactive effects), load the [./rules/audio-visualization.md](./rules/audio-visualization.md) file for more information. + +## Sound effects + +When needing to use sound effects, load the [./rules/sound-effects.md](./rules/sound-effects.md) file for more information. + +## How to use + +Read individual rule files for detailed explanations and code examples: + +- [rules/3d.md](rules/3d.md) - 3D content in Remotion using Three.js and React Three Fiber +- [rules/animations.md](rules/animations.md) - Fundamental animation skills for Remotion +- [rules/assets.md](rules/assets.md) - Importing images, videos, audio, and fonts into Remotion +- [rules/audio.md](rules/audio.md) - Using audio and sound in Remotion - importing, trimming, volume, speed, pitch +- [rules/calculate-metadata.md](rules/calculate-metadata.md) - Dynamically set composition duration, dimensions, and props +- [rules/can-decode.md](rules/can-decode.md) - Check if a video can be decoded by the browser using Mediabunny +- [rules/charts.md](rules/charts.md) - Chart and data visualization patterns for Remotion (bar, pie, line, stock charts) +- [rules/compositions.md](rules/compositions.md) - Defining compositions, stills, folders, default props and dynamic metadata +- [rules/extract-frames.md](rules/extract-frames.md) - Extract frames from videos at specific timestamps using Mediabunny +- [rules/fonts.md](rules/fonts.md) - Loading Google Fonts and local fonts in Remotion +- [rules/get-audio-duration.md](rules/get-audio-duration.md) - Getting the duration of an audio file in seconds with Mediabunny +- [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - Getting the width and height of a video file with Mediabunny +- [rules/get-video-duration.md](rules/get-video-duration.md) - Getting the duration of a video file in seconds with Mediabunny +- [rules/gifs.md](rules/gifs.md) - Displaying GIFs synchronized with Remotion's timeline +- [rules/images.md](rules/images.md) - Embedding images in Remotion using the Img component +- [rules/light-leaks.md](rules/light-leaks.md) - Light leak overlay effects using @remotion/light-leaks +- [rules/lottie.md](rules/lottie.md) - Embedding Lottie animations in Remotion +- [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - Measuring DOM element dimensions in Remotion +- [rules/measuring-text.md](rules/measuring-text.md) - Measuring text dimensions, fitting text to containers, and checking overflow +- [rules/sequencing.md](rules/sequencing.md) - Sequencing patterns for Remotion - delay, trim, limit duration of items +- [rules/tailwind.md](rules/tailwind.md) - Using TailwindCSS in Remotion +- [rules/text-animations.md](rules/text-animations.md) - Typography and text animation patterns for Remotion +- [rules/timing.md](rules/timing.md) - Interpolation curves in Remotion - linear, easing, spring animations +- [rules/transitions.md](rules/transitions.md) - Scene transition patterns for Remotion +- [rules/transparent-videos.md](rules/transparent-videos.md) - Rendering out a video with transparency +- [rules/trimming.md](rules/trimming.md) - Trimming patterns for Remotion - cut the beginning or end of animations +- [rules/videos.md](rules/videos.md) - Embedding videos in Remotion - trimming, volume, speed, looping, pitch +- [rules/parameters.md](rules/parameters.md) - Make a video parametrizable by adding a Zod schema +- [rules/maps.md](rules/maps.md) - Add a map using Mapbox and animate it +- [rules/voiceover.md](rules/voiceover.md) - Adding AI-generated voiceover to Remotion compositions using ElevenLabs TTS diff --git a/.agents/skills/remotion-best-practices/rules/3d.md b/.agents/skills/remotion-best-practices/rules/3d.md new file mode 100644 index 0000000..c9d254f --- /dev/null +++ b/.agents/skills/remotion-best-practices/rules/3d.md @@ -0,0 +1,86 @@ +--- +name: 3d +description: 3D content in Remotion using Three.js and React Three Fiber. +metadata: + tags: 3d, three, threejs +--- + +# Using Three.js and React Three Fiber in Remotion + +Follow React Three Fiber and Three.js best practices. +Only the following Remotion-specific rules need to be followed: + +## Prerequisites + +First, the `@remotion/three` package needs to be installed. +If it is not, use the following command: + +```bash +npx remotion add @remotion/three # If project uses npm +bunx remotion add @remotion/three # If project uses bun +yarn remotion add @remotion/three # If project uses yarn +pnpm exec remotion add @remotion/three # If project uses pnpm +``` + +## Using ThreeCanvas + +You MUST wrap 3D content in `` and include proper lighting. +`` MUST have a `width` and `height` prop. + +```tsx +import { ThreeCanvas } from "@remotion/three"; +import { useVideoConfig } from "remotion"; + +const { width, height } = useVideoConfig(); + + + + + + + + +; +``` + +## No animations not driven by `useCurrentFrame()` + +Shaders, models etc MUST NOT animate by themselves. +No animations are allowed unless they are driven by `useCurrentFrame()`. +Otherwise, it will cause flickering during rendering. + +Using `useFrame()` from `@react-three/fiber` is forbidden. + +## Animate using `useCurrentFrame()` + +Use `useCurrentFrame()` to perform animations. + +```tsx +const frame = useCurrentFrame(); +const rotationY = frame * 0.02; + + + + +; +``` + +## Using `` inside `` + +The `layout` prop of any `` inside a `` must be set to `none`. + +```tsx +import { Sequence } from "remotion"; +import { ThreeCanvas } from "@remotion/three"; + +const { width, height } = useVideoConfig(); + + + + + + + + +; +``` diff --git a/.agents/skills/remotion-best-practices/rules/animations.md b/.agents/skills/remotion-best-practices/rules/animations.md new file mode 100644 index 0000000..619591e --- /dev/null +++ b/.agents/skills/remotion-best-practices/rules/animations.md @@ -0,0 +1,27 @@ +--- +name: animations +description: Fundamental animation skills for Remotion +metadata: + tags: animations, transitions, frames, useCurrentFrame +--- + +All animations MUST be driven by the `useCurrentFrame()` hook. +Write animations in seconds and multiply them by the `fps` value from `useVideoConfig()`. + +```tsx +import { useCurrentFrame } from "remotion"; + +export const FadeIn = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const opacity = interpolate(frame, [0, 2 * fps], [0, 1], { + extrapolateRight: "clamp", + }); + + return
Hello World!
; +}; +``` + +CSS transitions or animations are FORBIDDEN - they will not render correctly. +Tailwind animation class names are FORBIDDEN - they will not render correctly. diff --git a/.agents/skills/remotion-best-practices/rules/assets.md b/.agents/skills/remotion-best-practices/rules/assets.md new file mode 100644 index 0000000..a5d4015 --- /dev/null +++ b/.agents/skills/remotion-best-practices/rules/assets.md @@ -0,0 +1,78 @@ +--- +name: assets +description: Importing images, videos, audio, and fonts into Remotion +metadata: + tags: assets, staticFile, images, fonts, public +--- + +# Importing assets in Remotion + +## The public folder + +Place assets in the `public/` folder at your project root. + +## Using staticFile() + +You MUST use `staticFile()` to reference files from the `public/` folder: + +```tsx +import { Img, staticFile } from "remotion"; + +export const MyComposition = () => { + return ; +}; +``` + +The function returns an encoded URL that works correctly when deploying to subdirectories. + +## Using with components + +**Images:** + +```tsx +import { Img, staticFile } from "remotion"; + +; +``` + +**Videos:** + +```tsx +import { Video } from "@remotion/media"; +import { staticFile } from "remotion"; + +