project-auto-wizard 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/project-auto-wizard.js +19 -0
- package/package.json +36 -0
- package/payload/coderabbit.yaml +19 -0
- package/payload/config/breaking-changes.json +1 -0
- package/payload/config/wizard-prompts.yml +62 -0
- package/payload/scripts/changelog_manager.py +726 -0
- package/payload/scripts/version_manager.py +617 -0
- package/payload/version.yml.template +48 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
- package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
- package/src/cli/args.js +95 -0
- package/src/cli/help.js +27 -0
- package/src/commands/full.js +53 -0
- package/src/commands/interactive.js +276 -0
- package/src/commands/revert.js +60 -0
- package/src/commands/version.js +31 -0
- package/src/commands/workflows.js +49 -0
- package/src/context.js +26 -0
- package/src/core/assets.js +47 -0
- package/src/core/branches.js +60 -0
- package/src/core/branding.js +15 -0
- package/src/core/breaking-check.js +65 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +26 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +23 -0
- package/src/core/copy/workflows.js +233 -0
- package/src/core/detect-fs.js +106 -0
- package/src/core/detect.js +62 -0
- package/src/core/fsutil.js +46 -0
- package/src/core/options-ask.js +99 -0
- package/src/core/paths-resolve.js +261 -0
- package/src/core/paths.js +16 -0
- package/src/core/version-yml.js +190 -0
- package/src/core/wizard-env.js +101 -0
- package/src/core/wizard-labels.js +107 -0
- package/src/index.js +162 -0
- package/src/ui/ansi.js +26 -0
- package/src/ui/banner.js +29 -0
- package/src/ui/env-plan.js +207 -0
- package/src/ui/prompts.js +99 -0
- package/src/ui/readline-engine.js +257 -0
- package/src/ui/status-cards.js +56 -0
- package/src/ui/summary.js +127 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# Flutter Android 테스트용 APK 빌드 워크플로우
|
|
3
|
+
# ===================================================================
|
|
4
|
+
#
|
|
5
|
+
# 이 워크플로우는 기능 브랜치에서 수동으로 실행하여
|
|
6
|
+
# 테스트용 Android APK를 빌드합니다.
|
|
7
|
+
#
|
|
8
|
+
# 주요 특징:
|
|
9
|
+
# - workflow_dispatch로 수동 실행 또는 repository_dispatch로 트리거
|
|
10
|
+
# - 버전은 version.yml에서 동적으로 읽어 사용 (운영 버전과 동일, 빌드 번호로 구분)
|
|
11
|
+
# - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
|
|
12
|
+
# - GitHub API로 이슈 정보 가져와서 빌드 정보에 포함
|
|
13
|
+
# - APK 파일만 생성 (Play Store 배포 제거)
|
|
14
|
+
# - 아티팩트로 업로드하여 다운로드 가능 (최대 500MB/아티팩트)
|
|
15
|
+
# - 아티팩트 이름에 브랜치명 포함하여 구분 용이
|
|
16
|
+
# - Firebase App Distribution 연동 (FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 시크릿 있을 때)
|
|
17
|
+
#
|
|
18
|
+
# 사용 방법:
|
|
19
|
+
# 1. 기능 브랜치로 체크아웃
|
|
20
|
+
# 2. GitHub Actions에서 이 워크플로우 선택
|
|
21
|
+
# 3. "Run workflow" 버튼 클릭
|
|
22
|
+
# 4. 빌드 완료 후 아티팩트에서 APK 다운로드
|
|
23
|
+
#
|
|
24
|
+
# ===================================================================
|
|
25
|
+
# 🔑 필수 GitHub Secrets
|
|
26
|
+
# ===================================================================
|
|
27
|
+
#
|
|
28
|
+
# 🔐 Release Keystore (Play Store 배포와 동일):
|
|
29
|
+
# RELEASE_KEYSTORE_BASE64: 릴리즈 키스토어 파일 (base64)
|
|
30
|
+
# RELEASE_KEYSTORE_PASSWORD: 키스토어 비밀번호
|
|
31
|
+
# RELEASE_KEY_ALIAS: 키 별칭
|
|
32
|
+
# RELEASE_KEY_PASSWORD: 키 비밀번호
|
|
33
|
+
#
|
|
34
|
+
# 📝 환경 설정 (선택):
|
|
35
|
+
# ENV_FILE (선택): .env 파일 내용
|
|
36
|
+
# GOOGLE_SERVICES_JSON (선택): google-services.json 내용 (Firebase 사용 시)
|
|
37
|
+
#
|
|
38
|
+
# 🔥 Firebase App Distribution (선택):
|
|
39
|
+
# FIREBASE_SERVICE_ACCOUNT_JSON_BASE64: Firebase 서비스 계정 JSON (base64)
|
|
40
|
+
#
|
|
41
|
+
# 🪄 빠른 설정: .github/util/flutter/firebase-wizard/firebase-wizard.html을
|
|
42
|
+
# 브라우저에서 열어 5단계 마법사로 Firebase 배포를 자동 설정할 수 있습니다.
|
|
43
|
+
#
|
|
44
|
+
# ===================================================================
|
|
45
|
+
|
|
46
|
+
name: PROJECT-Flutter-Android-Test-APK
|
|
47
|
+
|
|
48
|
+
concurrency:
|
|
49
|
+
group: ${{ github.workflow }}-${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
|
|
50
|
+
cancel-in-progress: true
|
|
51
|
+
|
|
52
|
+
on:
|
|
53
|
+
workflow_dispatch:
|
|
54
|
+
repository_dispatch:
|
|
55
|
+
types: [build-android-app]
|
|
56
|
+
|
|
57
|
+
# ===================================================================
|
|
58
|
+
# 🔧 프로젝트별 설정 (아래 값들을 수정하세요)
|
|
59
|
+
# ===================================================================
|
|
60
|
+
env:
|
|
61
|
+
FLUTTER_VERSION: "3.35.5"
|
|
62
|
+
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
|
|
63
|
+
ENV_FILE_PATH: ".env"
|
|
64
|
+
FIREBASE_APP_ID: "{FIREBASE_APP_ID}"
|
|
65
|
+
FIREBASE_TESTER_GROUP: "{TESTER_GROUP}"
|
|
66
|
+
|
|
67
|
+
jobs:
|
|
68
|
+
prepare-test-build:
|
|
69
|
+
name: 테스트 빌드 준비
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
|
|
72
|
+
outputs:
|
|
73
|
+
version: ${{ steps.build_info.outputs.version }}
|
|
74
|
+
issue_url: ${{ steps.issue_info.outputs.issue_url }}
|
|
75
|
+
issue_title: ${{ steps.issue_info.outputs.issue_title }}
|
|
76
|
+
issue_number: ${{ steps.issue_info.outputs.issue_number }}
|
|
77
|
+
branch_name: ${{ steps.issue_info.outputs.branch_name }}
|
|
78
|
+
commit_hash: ${{ steps.build_info.outputs.commit_hash }}
|
|
79
|
+
pr_number: ${{ steps.build_info.outputs.pr_number }}
|
|
80
|
+
build_number: ${{ steps.build_info.outputs.build_number }}
|
|
81
|
+
progress_comment_id: ${{ steps.progress_comment.outputs.comment_id }}
|
|
82
|
+
progress_start_time: ${{ steps.progress_comment.outputs.start_time }}
|
|
83
|
+
firebase_available: ${{ steps.firebase_check.outputs.available }}
|
|
84
|
+
|
|
85
|
+
steps:
|
|
86
|
+
- name: 브랜치 존재 여부 사전 확인
|
|
87
|
+
id: verify_branch
|
|
88
|
+
if: github.event_name == 'repository_dispatch'
|
|
89
|
+
uses: actions/github-script@v7
|
|
90
|
+
with:
|
|
91
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
92
|
+
script: |
|
|
93
|
+
const branchName = '${{ github.event.client_payload.branch_name }}';
|
|
94
|
+
const prNumber = '${{ github.event.client_payload.pr_number }}';
|
|
95
|
+
const runId = '${{ github.run_id }}';
|
|
96
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
97
|
+
|
|
98
|
+
console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await github.rest.repos.getBranch({
|
|
102
|
+
owner: context.repo.owner,
|
|
103
|
+
repo: context.repo.repo,
|
|
104
|
+
branch: branchName
|
|
105
|
+
});
|
|
106
|
+
console.log(`✅ 브랜치 존재 확인됨: ${branchName}`);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.status === 404) {
|
|
109
|
+
console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`);
|
|
110
|
+
|
|
111
|
+
// 에러 댓글 작성
|
|
112
|
+
const body = [
|
|
113
|
+
'🤖 ❌ **Android 테스트 APK 빌드 실패 - 브랜치를 찾을 수 없습니다**',
|
|
114
|
+
'',
|
|
115
|
+
'| 항목 | 값 |',
|
|
116
|
+
'|------|-----|',
|
|
117
|
+
`| **요청된 브랜치** | \`${branchName}\` |`,
|
|
118
|
+
`| **이슈/PR** | #${prNumber} |`,
|
|
119
|
+
'',
|
|
120
|
+
'### 💡 확인 사항',
|
|
121
|
+
'1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
|
|
122
|
+
'2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요',
|
|
123
|
+
'3. 브랜치명에 오타가 없는지 확인하세요',
|
|
124
|
+
'',
|
|
125
|
+
`🔗 [워크플로우 로그](${runUrl})`
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
await github.rest.issues.createComment({
|
|
129
|
+
owner: context.repo.owner,
|
|
130
|
+
repo: context.repo.repo,
|
|
131
|
+
issue_number: parseInt(prNumber),
|
|
132
|
+
body: body
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`);
|
|
136
|
+
} else {
|
|
137
|
+
core.setFailed(`브랜치 확인 중 오류: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- name: Check Firebase credentials
|
|
142
|
+
id: firebase_check
|
|
143
|
+
env:
|
|
144
|
+
FIREBASE_SECRET: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
|
|
145
|
+
run: |
|
|
146
|
+
if [ -n "$FIREBASE_SECRET" ]; then
|
|
147
|
+
echo "available=true" >> $GITHUB_OUTPUT
|
|
148
|
+
echo "✅ Firebase 서비스 계정 감지 - App Distribution 업로드 예정"
|
|
149
|
+
else
|
|
150
|
+
echo "available=false" >> $GITHUB_OUTPUT
|
|
151
|
+
echo "⚠️ Firebase 서비스 계정 없음 - App Distribution 업로드 건너뜀"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ===================================================================
|
|
155
|
+
# 진행 상황 댓글 시스템
|
|
156
|
+
# ===================================================================
|
|
157
|
+
- name: 진행 상황 댓글 생성
|
|
158
|
+
id: progress_comment
|
|
159
|
+
if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != ''
|
|
160
|
+
uses: actions/github-script@v7
|
|
161
|
+
with:
|
|
162
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
163
|
+
script: |
|
|
164
|
+
const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
|
|
165
|
+
const runId = '${{ github.run_id }}';
|
|
166
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
const firebaseAvailable = '${{ steps.firebase_check.outputs.available }}' === 'true';
|
|
169
|
+
|
|
170
|
+
const rows = [
|
|
171
|
+
'| 🔧 준비 | ⏳ 진행 중... | - |',
|
|
172
|
+
'| 🔨 APK 빌드 | ⏸️ 대기 | - |',
|
|
173
|
+
'| 📤 GitHub 아티팩트 업로드 | ⏸️ 대기 | - |',
|
|
174
|
+
];
|
|
175
|
+
if (firebaseAvailable) {
|
|
176
|
+
rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const body = [
|
|
180
|
+
'## 🤖 Android APK 빌드 중...',
|
|
181
|
+
'',
|
|
182
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
183
|
+
'|------|------|----------|',
|
|
184
|
+
...rows,
|
|
185
|
+
'',
|
|
186
|
+
`📋 **[실시간 로그 보기](${runUrl})**`
|
|
187
|
+
].join('\n');
|
|
188
|
+
|
|
189
|
+
const { data: comment } = await github.rest.issues.createComment({
|
|
190
|
+
owner: context.repo.owner,
|
|
191
|
+
repo: context.repo.repo,
|
|
192
|
+
issue_number: prNumber,
|
|
193
|
+
body: body
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log(`✅ 진행상황 댓글 생성 완료 (ID: ${comment.id})`);
|
|
197
|
+
core.setOutput('comment_id', comment.id.toString());
|
|
198
|
+
core.setOutput('start_time', startTime.toString());
|
|
199
|
+
|
|
200
|
+
- name: Checkout repository
|
|
201
|
+
uses: actions/checkout@v5
|
|
202
|
+
with:
|
|
203
|
+
fetch-depth: 0
|
|
204
|
+
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
|
|
205
|
+
|
|
206
|
+
- name: Pull latest changes
|
|
207
|
+
run: |
|
|
208
|
+
BRANCH_NAME="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
|
|
209
|
+
echo "🌿 브랜치: $BRANCH_NAME"
|
|
210
|
+
git pull origin "$BRANCH_NAME" || echo "⚠️ Pull 실패 (이미 최신 상태일 수 있음)"
|
|
211
|
+
|
|
212
|
+
# 브랜치명에서 이슈 번호 추출 및 GitHub API로 이슈 정보 가져오기
|
|
213
|
+
- name: 브랜치명에서 이슈 정보 추출
|
|
214
|
+
id: issue_info
|
|
215
|
+
run: |
|
|
216
|
+
# repository_dispatch인 경우 client_payload에서 브랜치명 가져오기
|
|
217
|
+
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
|
|
218
|
+
BRANCH_NAME="${{ github.event.client_payload.branch_name }}"
|
|
219
|
+
ISSUE_NUMBER="${{ github.event.client_payload.issue_number }}"
|
|
220
|
+
else
|
|
221
|
+
BRANCH_NAME="${{ github.ref_name }}"
|
|
222
|
+
# 브랜치명에서 이슈 번호 추출 (#387 형식)
|
|
223
|
+
ISSUE_NUMBER=$(echo "$BRANCH_NAME" | sed -n 's/.*#\([0-9]*\).*/\1/p')
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "🌿 브랜치명: $BRANCH_NAME"
|
|
227
|
+
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
228
|
+
|
|
229
|
+
if [ -z "$ISSUE_NUMBER" ]; then
|
|
230
|
+
echo "⚠️ 브랜치명에서 이슈 번호를 찾을 수 없습니다."
|
|
231
|
+
echo "issue_url=" >> $GITHUB_OUTPUT
|
|
232
|
+
echo "issue_title=" >> $GITHUB_OUTPUT
|
|
233
|
+
echo "issue_number=" >> $GITHUB_OUTPUT
|
|
234
|
+
else
|
|
235
|
+
echo "✅ 추출된 이슈 번호: #$ISSUE_NUMBER"
|
|
236
|
+
ISSUE_URL="https://github.com/${{ github.repository }}/issues/$ISSUE_NUMBER"
|
|
237
|
+
echo "issue_url=$ISSUE_URL" >> $GITHUB_OUTPUT
|
|
238
|
+
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
|
|
239
|
+
|
|
240
|
+
# GitHub API로 이슈 정보 가져오기
|
|
241
|
+
echo "🔍 GitHub API로 이슈 정보 조회 중..."
|
|
242
|
+
ISSUE_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
243
|
+
"https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER")
|
|
244
|
+
|
|
245
|
+
ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r '.title // "이슈 정보 없음"')
|
|
246
|
+
ISSUE_STATE=$(echo "$ISSUE_RESPONSE" | jq -r '.state // "unknown"')
|
|
247
|
+
|
|
248
|
+
if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "이슈 정보 없음" ]; then
|
|
249
|
+
echo "⚠️ 이슈 #$ISSUE_NUMBER를 찾을 수 없습니다."
|
|
250
|
+
echo "issue_title=" >> $GITHUB_OUTPUT
|
|
251
|
+
else
|
|
252
|
+
echo "📋 이슈 제목: $ISSUE_TITLE"
|
|
253
|
+
echo "📌 이슈 상태: $ISSUE_STATE"
|
|
254
|
+
echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# 빌드 정보 생성
|
|
259
|
+
- name: 빌드 정보 생성
|
|
260
|
+
id: build_info
|
|
261
|
+
run: |
|
|
262
|
+
# version_manager.py로 버전 가져오기 (프로덕션 워크플로우와 동일한 방식)
|
|
263
|
+
VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
264
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
265
|
+
|
|
266
|
+
COMMIT_SHA="${{ github.sha }}"
|
|
267
|
+
COMMIT_SHORT=$(echo "$COMMIT_SHA" | cut -c1-7)
|
|
268
|
+
BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
|
269
|
+
|
|
270
|
+
# repository_dispatch 이벤트인 경우 PR 번호를 빌드 번호로 사용
|
|
271
|
+
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
|
|
272
|
+
BUILD_NUMBER="${{ github.event.client_payload.build_number }}"
|
|
273
|
+
PR_NUMBER="${{ github.event.client_payload.pr_number }}"
|
|
274
|
+
echo "📋 repository_dispatch 이벤트 감지"
|
|
275
|
+
echo " PR 번호: #$PR_NUMBER"
|
|
276
|
+
echo " 빌드 번호: $BUILD_NUMBER (PR 번호 사용)"
|
|
277
|
+
else
|
|
278
|
+
BUILD_NUMBER="${{ github.run_number }}"
|
|
279
|
+
PR_NUMBER=""
|
|
280
|
+
echo "📋 workflow_dispatch 이벤트 감지"
|
|
281
|
+
echo " 빌드 번호: $BUILD_NUMBER (기본값)"
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
echo "commit_hash=$COMMIT_SHORT" >> $GITHUB_OUTPUT
|
|
285
|
+
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
|
|
286
|
+
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
|
287
|
+
echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT
|
|
288
|
+
|
|
289
|
+
echo "📋 빌드 정보:"
|
|
290
|
+
echo " 빌드 번호: #$BUILD_NUMBER"
|
|
291
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
292
|
+
echo " PR 번호: #$PR_NUMBER"
|
|
293
|
+
fi
|
|
294
|
+
echo " 커밋 해시: $COMMIT_SHORT"
|
|
295
|
+
echo " 빌드 날짜: $BUILD_DATE"
|
|
296
|
+
|
|
297
|
+
build-android-test:
|
|
298
|
+
name: Android 테스트 APK 빌드
|
|
299
|
+
runs-on: ubuntu-latest
|
|
300
|
+
needs: prepare-test-build
|
|
301
|
+
|
|
302
|
+
steps:
|
|
303
|
+
- name: Checkout repository
|
|
304
|
+
uses: actions/checkout@v5
|
|
305
|
+
with:
|
|
306
|
+
fetch-depth: 0
|
|
307
|
+
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
|
|
308
|
+
|
|
309
|
+
- name: Pull latest changes
|
|
310
|
+
run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}
|
|
311
|
+
|
|
312
|
+
- name: Create .env file
|
|
313
|
+
env:
|
|
314
|
+
ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
|
|
315
|
+
run: |
|
|
316
|
+
printf '%s\n' "$ENV_CONTENT" > ${{ env.ENV_FILE_PATH }}
|
|
317
|
+
echo "✅ ${{ env.ENV_FILE_PATH }} file created"
|
|
318
|
+
|
|
319
|
+
- name: Setup Release Keystore
|
|
320
|
+
env:
|
|
321
|
+
KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
|
|
322
|
+
run: |
|
|
323
|
+
mkdir -p android/keystore
|
|
324
|
+
echo "$KEYSTORE_BASE64" | base64 -d > android/keystore/key.jks
|
|
325
|
+
echo "✅ Release Keystore 생성 완료"
|
|
326
|
+
ls -la android/keystore/
|
|
327
|
+
|
|
328
|
+
- name: Create key.properties
|
|
329
|
+
env:
|
|
330
|
+
STORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
|
|
331
|
+
KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
|
|
332
|
+
KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
|
|
333
|
+
run: |
|
|
334
|
+
cat > android/key.properties << EOF
|
|
335
|
+
storeFile=keystore/key.jks
|
|
336
|
+
storePassword=$STORE_PASSWORD
|
|
337
|
+
keyAlias=$KEY_ALIAS
|
|
338
|
+
keyPassword=$KEY_PASSWORD
|
|
339
|
+
EOF
|
|
340
|
+
echo "✅ key.properties 생성 완료"
|
|
341
|
+
|
|
342
|
+
# google-services.json 생성 (선택적)
|
|
343
|
+
- name: Create google-services.json
|
|
344
|
+
shell: bash
|
|
345
|
+
env:
|
|
346
|
+
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
|
|
347
|
+
run: |
|
|
348
|
+
if [ -n "$GOOGLE_SERVICES_JSON" ]; then
|
|
349
|
+
mkdir -p android/app
|
|
350
|
+
printf '%s' "$GOOGLE_SERVICES_JSON" > android/app/google-services.json
|
|
351
|
+
echo "✅ google-services.json created"
|
|
352
|
+
else
|
|
353
|
+
echo "ℹ️ GOOGLE_SERVICES_JSON secret not provided, skipping"
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# Flutter 설정
|
|
357
|
+
- name: Set up Flutter
|
|
358
|
+
uses: subosito/flutter-action@v2
|
|
359
|
+
with:
|
|
360
|
+
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
361
|
+
cache: true
|
|
362
|
+
|
|
363
|
+
- name: Verify Flutter version
|
|
364
|
+
run: |
|
|
365
|
+
echo "✅ Flutter setup completed"
|
|
366
|
+
flutter --version
|
|
367
|
+
|
|
368
|
+
# Flutter 및 Gradle 캐시
|
|
369
|
+
- name: Cache Flutter dependencies
|
|
370
|
+
uses: actions/cache@v4
|
|
371
|
+
with:
|
|
372
|
+
path: ~/.pub-cache
|
|
373
|
+
key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
|
|
374
|
+
restore-keys: ${{ runner.os }}-flutter-pub-
|
|
375
|
+
|
|
376
|
+
- name: Cache Gradle dependencies
|
|
377
|
+
uses: actions/cache@v4
|
|
378
|
+
with:
|
|
379
|
+
path: |
|
|
380
|
+
~/.gradle/caches
|
|
381
|
+
~/.gradle/wrapper
|
|
382
|
+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle', '**/gradle-wrapper.properties') }}
|
|
383
|
+
restore-keys: |
|
|
384
|
+
${{ runner.os }}-gradle-
|
|
385
|
+
|
|
386
|
+
# 프로젝트 의존성 설치
|
|
387
|
+
- name: Install dependencies
|
|
388
|
+
run: |
|
|
389
|
+
flutter pub get
|
|
390
|
+
echo "✅ Dependencies installed"
|
|
391
|
+
|
|
392
|
+
# Gradle 셋업
|
|
393
|
+
- name: Setup Gradle
|
|
394
|
+
working-directory: android
|
|
395
|
+
run: |
|
|
396
|
+
chmod +x gradlew
|
|
397
|
+
echo "✅ Gradle wrapper permissions set"
|
|
398
|
+
|
|
399
|
+
# Java 설정
|
|
400
|
+
- name: Set up Java
|
|
401
|
+
uses: actions/setup-java@v3
|
|
402
|
+
with:
|
|
403
|
+
distribution: "temurin"
|
|
404
|
+
java-version: ${{ env.JAVA_VERSION }}
|
|
405
|
+
|
|
406
|
+
- name: Verify Java version
|
|
407
|
+
run: |
|
|
408
|
+
echo "✅ Java setup completed"
|
|
409
|
+
java -version
|
|
410
|
+
|
|
411
|
+
# Ruby 설정
|
|
412
|
+
- name: Set up Ruby
|
|
413
|
+
uses: ruby/setup-ruby@v1
|
|
414
|
+
with:
|
|
415
|
+
ruby-version: "3.4.1"
|
|
416
|
+
bundler-cache: true
|
|
417
|
+
|
|
418
|
+
- name: Verify Ruby version
|
|
419
|
+
run: |
|
|
420
|
+
echo "✅ Ruby setup completed"
|
|
421
|
+
ruby -v
|
|
422
|
+
|
|
423
|
+
# Fastlane 설치 (Bundler 방식 - gem 충돌 방지)
|
|
424
|
+
- name: Install Fastlane
|
|
425
|
+
working-directory: android
|
|
426
|
+
run: |
|
|
427
|
+
# multi_json: google-apis transitive 의존성이 선언 누락한 upstream 버그 회피 (gemspec 미선언 → Gem::LoadError)
|
|
428
|
+
printf 'source "https://rubygems.org"\ngem "fastlane"\ngem "multi_json"\n' > Gemfile
|
|
429
|
+
bundle install
|
|
430
|
+
echo "✅ Fastlane installed (Bundler)"
|
|
431
|
+
bundle exec fastlane --version
|
|
432
|
+
|
|
433
|
+
# 진행상황 - 준비 완료
|
|
434
|
+
- name: 진행상황 - 준비 완료
|
|
435
|
+
id: progress_step1
|
|
436
|
+
if: github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
437
|
+
uses: actions/github-script@v7
|
|
438
|
+
with:
|
|
439
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
440
|
+
script: |
|
|
441
|
+
const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
|
|
442
|
+
const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
|
|
443
|
+
const runId = '${{ github.run_id }}';
|
|
444
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
445
|
+
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
const elapsed = Math.round((now - startTime) / 1000);
|
|
448
|
+
const minutes = Math.floor(elapsed / 60);
|
|
449
|
+
const seconds = elapsed % 60;
|
|
450
|
+
const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
451
|
+
const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
|
|
452
|
+
|
|
453
|
+
const rows = [
|
|
454
|
+
`| 🔧 준비 | ✅ 완료 | ${elapsedStr} |`,
|
|
455
|
+
'| 🔨 APK 빌드 | ⏳ 진행 중... | - |',
|
|
456
|
+
'| 📤 GitHub 아티팩트 업로드 | ⏸️ 대기 | - |',
|
|
457
|
+
];
|
|
458
|
+
if (firebaseAvailable) {
|
|
459
|
+
rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const body = [
|
|
463
|
+
'## 🤖 Android APK 빌드 중...',
|
|
464
|
+
'',
|
|
465
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
466
|
+
'|------|------|----------|',
|
|
467
|
+
...rows,
|
|
468
|
+
'',
|
|
469
|
+
`📋 **[실시간 로그 보기](${runUrl})**`
|
|
470
|
+
].join('\n');
|
|
471
|
+
|
|
472
|
+
await github.rest.issues.updateComment({
|
|
473
|
+
owner: context.repo.owner,
|
|
474
|
+
repo: context.repo.repo,
|
|
475
|
+
comment_id: commentId,
|
|
476
|
+
body: body
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
console.log(`✅ 진행상황 업데이트: 준비 완료 (${elapsedStr})`);
|
|
480
|
+
core.setOutput('elapsed', elapsedStr);
|
|
481
|
+
core.setOutput('end_time', now.toString());
|
|
482
|
+
|
|
483
|
+
- name: Generate APK filename
|
|
484
|
+
id: apk_filename
|
|
485
|
+
run: |
|
|
486
|
+
REPO_NAME="${{ github.event.repository.name }}"
|
|
487
|
+
COMMIT_SHORT="${{ needs.prepare-test-build.outputs.commit_hash }}"
|
|
488
|
+
BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
|
|
489
|
+
ISSUE_NUMBER="${{ needs.prepare-test-build.outputs.issue_number }}"
|
|
490
|
+
ISSUE_TITLE="${{ needs.prepare-test-build.outputs.issue_title }}"
|
|
491
|
+
|
|
492
|
+
# 이슈 제목 정제
|
|
493
|
+
CLEAN_TITLE=""
|
|
494
|
+
if [ -n "$ISSUE_TITLE" ]; then
|
|
495
|
+
CLEAN_TITLE="$ISSUE_TITLE"
|
|
496
|
+
# 1. 이모지 제거 (유니코드 이모지 범위)
|
|
497
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[\xF0\x9F][\x80-\xBF][\x80-\xBF][\x80-\xBF]//g; s/[\xE2\x9C\xE2\x9A\xE2\xAD\xE2\x9D\xE2\x99\xE2\x98\xE2\x9E\xE2\xAC\xE2\xAB\xE2\xAC][\x80-\xBF][\x80-\xBF]//g; s/[\xE2\x80\x8D]//g; s/\xEF\xB8\x8F//g; s/\xE2\x83\xA3//g')
|
|
498
|
+
# 2. 대괄호 태그 제거 [xxx]
|
|
499
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/\[[^]]*\]//g')
|
|
500
|
+
# 3. 앞뒤 공백 trim
|
|
501
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
502
|
+
# 4. 공백 → _
|
|
503
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[[:space:]]/_/g')
|
|
504
|
+
# 5. 파일시스템 비호환 문자 제거
|
|
505
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[#\/\\:*?"<>|]//g')
|
|
506
|
+
# 6. 연속 밑줄 정리
|
|
507
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/__*/_/g; s/^_//; s/_$//')
|
|
508
|
+
# 7. 30자 제한
|
|
509
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | cut -c1-30)
|
|
510
|
+
# 8. 끝 밑줄 제거
|
|
511
|
+
CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/_$//')
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
# 파일명 조립
|
|
515
|
+
if [ -n "$ISSUE_NUMBER" ] && [ -n "$CLEAN_TITLE" ]; then
|
|
516
|
+
APK_NAME="${REPO_NAME}-${ISSUE_NUMBER}-${BUILD_NUMBER}-${CLEAN_TITLE}-${COMMIT_SHORT}.apk"
|
|
517
|
+
elif [ -n "$ISSUE_NUMBER" ]; then
|
|
518
|
+
APK_NAME="${REPO_NAME}-${ISSUE_NUMBER}-${BUILD_NUMBER}-${COMMIT_SHORT}.apk"
|
|
519
|
+
else
|
|
520
|
+
APK_NAME="${REPO_NAME}-${BUILD_NUMBER}-${COMMIT_SHORT}.apk"
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# 아티팩트 이름 (확장자 제거)
|
|
524
|
+
ARTIFACT_NAME="${APK_NAME%.apk}"
|
|
525
|
+
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
|
|
526
|
+
echo "apk_name=$APK_NAME" >> $GITHUB_OUTPUT
|
|
527
|
+
echo "📦 APK 파일명: $APK_NAME"
|
|
528
|
+
|
|
529
|
+
- name: Set test build version
|
|
530
|
+
run: |
|
|
531
|
+
VERSION="${{ needs.prepare-test-build.outputs.version }}"
|
|
532
|
+
BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
|
|
533
|
+
echo "📋 테스트 빌드 버전 설정: ${VERSION}+${BUILD_NUMBER}"
|
|
534
|
+
sed -i "s/^version: .*/version: ${VERSION}+${BUILD_NUMBER}/" pubspec.yaml
|
|
535
|
+
echo "✅ pubspec.yaml 버전 변경 완료:"
|
|
536
|
+
grep "^version:" pubspec.yaml
|
|
537
|
+
|
|
538
|
+
# APK 빌드 (Fastlane 또는 직접 빌드)
|
|
539
|
+
- name: Build APK
|
|
540
|
+
run: |
|
|
541
|
+
# Fastlane Fastfile이 있으면 사용, 없으면 직접 빌드
|
|
542
|
+
if [ -f "android/fastlane/Fastfile" ]; then
|
|
543
|
+
echo "📦 Fastlane을 사용하여 빌드..."
|
|
544
|
+
cd android
|
|
545
|
+
bundle exec fastlane build --verbose || flutter build apk --release
|
|
546
|
+
else
|
|
547
|
+
echo "📦 Flutter 직접 빌드..."
|
|
548
|
+
flutter build apk --release
|
|
549
|
+
fi
|
|
550
|
+
ls -la ./build/app/outputs/flutter-apk/ || true
|
|
551
|
+
echo "✅ APK built"
|
|
552
|
+
|
|
553
|
+
# APK 파일 이름 변경 및 준비
|
|
554
|
+
- name: Rename and Prepare APK
|
|
555
|
+
run: |
|
|
556
|
+
mkdir -p ./android/app/build/outputs/apk/release/
|
|
557
|
+
APK_NAME="${{ steps.apk_filename.outputs.apk_name }}"
|
|
558
|
+
mv ./build/app/outputs/flutter-apk/app-release.apk ./android/app/build/outputs/apk/release/${APK_NAME}
|
|
559
|
+
echo "✅ APK renamed to ${APK_NAME}"
|
|
560
|
+
ls -la ./android/app/build/outputs/apk/release/
|
|
561
|
+
|
|
562
|
+
# APK 파일 크기 확인
|
|
563
|
+
APK_SIZE=$(stat -c%s "./android/app/build/outputs/apk/release/${APK_NAME}" 2>/dev/null || stat -f%z "./android/app/build/outputs/apk/release/${APK_NAME}" 2>/dev/null || echo "0")
|
|
564
|
+
APK_SIZE_MB=$(echo "scale=2; $APK_SIZE / 1024 / 1024" | bc)
|
|
565
|
+
echo "📦 APK 파일 크기: ${APK_SIZE_MB}MB"
|
|
566
|
+
|
|
567
|
+
# 아티팩트 용량 제한 확인 (500MB)
|
|
568
|
+
if [ "$APK_SIZE" -gt 524288000 ]; then
|
|
569
|
+
echo "⚠️ 경고: APK 파일 크기가 500MB를 초과합니다 (${APK_SIZE_MB}MB)"
|
|
570
|
+
else
|
|
571
|
+
echo "✅ APK 파일 크기가 아티팩트 제한 내입니다 (${APK_SIZE_MB}MB / 500MB)"
|
|
572
|
+
fi
|
|
573
|
+
|
|
574
|
+
# 진행상황 - APK 빌드 완료
|
|
575
|
+
- name: 진행상황 - APK 빌드 완료
|
|
576
|
+
id: progress_step2
|
|
577
|
+
if: github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
578
|
+
uses: actions/github-script@v7
|
|
579
|
+
with:
|
|
580
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
581
|
+
script: |
|
|
582
|
+
const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
|
|
583
|
+
const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}');
|
|
584
|
+
const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
|
|
585
|
+
const runId = '${{ github.run_id }}';
|
|
586
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
587
|
+
|
|
588
|
+
const now = Date.now();
|
|
589
|
+
const elapsed = Math.round((now - step1EndTime) / 1000);
|
|
590
|
+
const minutes = Math.floor(elapsed / 60);
|
|
591
|
+
const seconds = elapsed % 60;
|
|
592
|
+
const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
593
|
+
const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
|
|
594
|
+
|
|
595
|
+
const rows = [
|
|
596
|
+
`| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
|
|
597
|
+
`| 🔨 APK 빌드 | ✅ 완료 | ${elapsedStr} |`,
|
|
598
|
+
'| 📤 GitHub 아티팩트 업로드 | ⏳ 진행 중... | - |',
|
|
599
|
+
];
|
|
600
|
+
if (firebaseAvailable) {
|
|
601
|
+
rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const body = [
|
|
605
|
+
'## 🤖 Android APK 빌드 중...',
|
|
606
|
+
'',
|
|
607
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
608
|
+
'|------|------|----------|',
|
|
609
|
+
...rows,
|
|
610
|
+
'',
|
|
611
|
+
`📋 **[실시간 로그 보기](${runUrl})**`
|
|
612
|
+
].join('\n');
|
|
613
|
+
|
|
614
|
+
await github.rest.issues.updateComment({
|
|
615
|
+
owner: context.repo.owner,
|
|
616
|
+
repo: context.repo.repo,
|
|
617
|
+
comment_id: commentId,
|
|
618
|
+
body: body
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
console.log(`✅ 진행상황 업데이트: APK 빌드 완료 (${elapsedStr})`);
|
|
622
|
+
core.setOutput('elapsed', elapsedStr);
|
|
623
|
+
core.setOutput('end_time', now.toString());
|
|
624
|
+
|
|
625
|
+
# 빌드 정보 파일 생성
|
|
626
|
+
- name: Create build info file
|
|
627
|
+
run: |
|
|
628
|
+
BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
|
|
629
|
+
PR_NUMBER="${{ needs.prepare-test-build.outputs.pr_number }}"
|
|
630
|
+
BRANCH_NAME="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
|
|
631
|
+
COMMIT_SHA="${{ github.sha }}"
|
|
632
|
+
COMMIT_SHORT="${{ needs.prepare-test-build.outputs.commit_hash }}"
|
|
633
|
+
BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
|
634
|
+
|
|
635
|
+
cat > build-info.txt << EOF
|
|
636
|
+
테스트 APK 빌드 정보
|
|
637
|
+
|
|
638
|
+
빌드 번호: #$BUILD_NUMBER
|
|
639
|
+
EOF
|
|
640
|
+
|
|
641
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
642
|
+
cat >> build-info.txt << EOF
|
|
643
|
+
PR 번호: #$PR_NUMBER
|
|
644
|
+
EOF
|
|
645
|
+
fi
|
|
646
|
+
|
|
647
|
+
cat >> build-info.txt << EOF
|
|
648
|
+
브랜치: $BRANCH_NAME
|
|
649
|
+
커밋: $COMMIT_SHORT
|
|
650
|
+
전체 커밋 해시: $COMMIT_SHA
|
|
651
|
+
빌드 날짜: $BUILD_DATE
|
|
652
|
+
|
|
653
|
+
EOF
|
|
654
|
+
|
|
655
|
+
# 이슈 정보가 있으면 추가
|
|
656
|
+
if [ -n "${{ needs.prepare-test-build.outputs.issue_number }}" ]; then
|
|
657
|
+
ISSUE_NUMBER="${{ needs.prepare-test-build.outputs.issue_number }}"
|
|
658
|
+
ISSUE_TITLE="${{ needs.prepare-test-build.outputs.issue_title }}"
|
|
659
|
+
ISSUE_URL="${{ needs.prepare-test-build.outputs.issue_url }}"
|
|
660
|
+
|
|
661
|
+
cat >> build-info.txt << EOF
|
|
662
|
+
관련 이슈:
|
|
663
|
+
- #$ISSUE_NUMBER: $ISSUE_TITLE
|
|
664
|
+
- URL: $ISSUE_URL
|
|
665
|
+
|
|
666
|
+
EOF
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
echo "📋 빌드 정보 파일 생성 완료:"
|
|
670
|
+
cat build-info.txt
|
|
671
|
+
|
|
672
|
+
# 빌드 메타데이터 파일 생성 (댓글 작성 워크플로우용)
|
|
673
|
+
- name: Create build metadata file
|
|
674
|
+
run: |
|
|
675
|
+
cat > build-metadata.json << EOF
|
|
676
|
+
{
|
|
677
|
+
"pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
|
|
678
|
+
"build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
|
|
679
|
+
"issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
|
|
680
|
+
"branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
|
|
681
|
+
}
|
|
682
|
+
EOF
|
|
683
|
+
cat build-metadata.json
|
|
684
|
+
|
|
685
|
+
# APK와 빌드 정보를 아티팩트로 업로드 (브랜치명 포함)
|
|
686
|
+
- name: Upload APK and build info as Artifact
|
|
687
|
+
id: upload_artifact
|
|
688
|
+
uses: actions/upload-artifact@v4
|
|
689
|
+
with:
|
|
690
|
+
name: ${{ steps.apk_filename.outputs.artifact_name }}
|
|
691
|
+
path: |
|
|
692
|
+
./android/app/build/outputs/apk/release/*.apk
|
|
693
|
+
build-info.txt
|
|
694
|
+
build-metadata.json
|
|
695
|
+
retention-days: 7
|
|
696
|
+
if-no-files-found: error
|
|
697
|
+
|
|
698
|
+
- name: Record artifact upload time
|
|
699
|
+
id: artifact_time
|
|
700
|
+
if: github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
701
|
+
uses: actions/github-script@v7
|
|
702
|
+
with:
|
|
703
|
+
script: |
|
|
704
|
+
const step2EndTime = parseInt('${{ steps.progress_step2.outputs.end_time }}');
|
|
705
|
+
const now = Date.now();
|
|
706
|
+
const elapsed = Math.round((now - step2EndTime) / 1000);
|
|
707
|
+
const minutes = Math.floor(elapsed / 60);
|
|
708
|
+
const seconds = elapsed % 60;
|
|
709
|
+
const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
710
|
+
core.setOutput('elapsed', elapsedStr);
|
|
711
|
+
core.setOutput('end_time', now.toString());
|
|
712
|
+
|
|
713
|
+
# Firebase App Distribution 업로드 (시크릿 존재 시에만)
|
|
714
|
+
- name: Setup Firebase credentials file
|
|
715
|
+
if: needs.prepare-test-build.outputs.firebase_available == 'true'
|
|
716
|
+
env:
|
|
717
|
+
FIREBASE_SA_BASE64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
|
|
718
|
+
run: |
|
|
719
|
+
echo "$FIREBASE_SA_BASE64" | base64 --decode > firebase-service-account.json
|
|
720
|
+
echo "✅ Firebase 서비스 계정 JSON 파일 생성 완료"
|
|
721
|
+
|
|
722
|
+
- name: Upload to Firebase App Distribution
|
|
723
|
+
id: firebase_upload
|
|
724
|
+
if: needs.prepare-test-build.outputs.firebase_available == 'true'
|
|
725
|
+
continue-on-error: true
|
|
726
|
+
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
|
727
|
+
with:
|
|
728
|
+
appId: ${{ env.FIREBASE_APP_ID }}
|
|
729
|
+
serviceCredentialsFile: firebase-service-account.json
|
|
730
|
+
groups: ${{ env.FIREBASE_TESTER_GROUP }}
|
|
731
|
+
file: ./android/app/build/outputs/apk/release/${{ steps.apk_filename.outputs.apk_name }}
|
|
732
|
+
releaseNotes: |
|
|
733
|
+
테스트 빌드 #${{ needs.prepare-test-build.outputs.build_number }}
|
|
734
|
+
브랜치: ${{ needs.prepare-test-build.outputs.branch_name }}
|
|
735
|
+
커밋: ${{ needs.prepare-test-build.outputs.commit_hash }}
|
|
736
|
+
이슈: #${{ needs.prepare-test-build.outputs.issue_number }} ${{ needs.prepare-test-build.outputs.issue_title }}
|
|
737
|
+
|
|
738
|
+
- name: Cleanup Firebase credentials
|
|
739
|
+
if: always() && needs.prepare-test-build.outputs.firebase_available == 'true'
|
|
740
|
+
run: rm -f firebase-service-account.json
|
|
741
|
+
|
|
742
|
+
- name: Firebase upload result
|
|
743
|
+
id: firebase_result
|
|
744
|
+
if: needs.prepare-test-build.outputs.firebase_available == 'true'
|
|
745
|
+
run: |
|
|
746
|
+
if [ "${{ steps.firebase_upload.outcome }}" == "success" ]; then
|
|
747
|
+
echo "status=success" >> $GITHUB_OUTPUT
|
|
748
|
+
echo "✅ Firebase App Distribution 업로드 성공"
|
|
749
|
+
else
|
|
750
|
+
echo "status=failure" >> $GITHUB_OUTPUT
|
|
751
|
+
echo "❌ Firebase App Distribution 업로드 실패 - APK 아티팩트는 정상 업로드됨"
|
|
752
|
+
fi
|
|
753
|
+
|
|
754
|
+
- name: Record Firebase upload time
|
|
755
|
+
id: firebase_time
|
|
756
|
+
if: needs.prepare-test-build.outputs.firebase_available == 'true' && github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
757
|
+
uses: actions/github-script@v7
|
|
758
|
+
with:
|
|
759
|
+
script: |
|
|
760
|
+
const artifactEndTime = parseInt('${{ steps.artifact_time.outputs.end_time }}');
|
|
761
|
+
const now = Date.now();
|
|
762
|
+
const elapsed = Math.round((now - artifactEndTime) / 1000);
|
|
763
|
+
const minutes = Math.floor(elapsed / 60);
|
|
764
|
+
const seconds = elapsed % 60;
|
|
765
|
+
const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
766
|
+
core.setOutput('elapsed', elapsedStr);
|
|
767
|
+
|
|
768
|
+
# 진행 상황 최종 업데이트 - 성공
|
|
769
|
+
- name: 진행 상황 최종 업데이트 - 성공
|
|
770
|
+
if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
771
|
+
uses: actions/github-script@v7
|
|
772
|
+
with:
|
|
773
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
774
|
+
script: |
|
|
775
|
+
const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
|
|
776
|
+
const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
|
|
777
|
+
const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
|
|
778
|
+
const step2Elapsed = '${{ steps.progress_step2.outputs.elapsed }}';
|
|
779
|
+
const step3Elapsed = '${{ steps.artifact_time.outputs.elapsed }}';
|
|
780
|
+
const step4Elapsed = '${{ steps.firebase_time.outputs.elapsed }}';
|
|
781
|
+
const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
|
|
782
|
+
const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}';
|
|
783
|
+
const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}';
|
|
784
|
+
const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}';
|
|
785
|
+
const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}';
|
|
786
|
+
const runId = '${{ github.run_id }}';
|
|
787
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
788
|
+
|
|
789
|
+
const artifactUrl = '${{ steps.upload_artifact.outputs.artifact-url }}' || `${runUrl}#artifacts`;
|
|
790
|
+
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const totalElapsed_s = Math.round((now - startTime) / 1000);
|
|
793
|
+
const totalMin = Math.floor(totalElapsed_s / 60);
|
|
794
|
+
const totalSec = totalElapsed_s % 60;
|
|
795
|
+
const totalElapsed = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`;
|
|
796
|
+
|
|
797
|
+
const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
|
|
798
|
+
const firebaseSuccess = '${{ steps.firebase_result.outputs.status }}' === 'success';
|
|
799
|
+
|
|
800
|
+
const lines = [
|
|
801
|
+
'## 🤖 ✅ Android APK 빌드 완료!',
|
|
802
|
+
'',
|
|
803
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
804
|
+
'|------|------|----------|',
|
|
805
|
+
`| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
|
|
806
|
+
`| 🔨 APK 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
|
|
807
|
+
`| 📤 GitHub 아티팩트 업로드 | ✅ 완료 | ${step3Elapsed || '-'} |`,
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
if (firebaseAvailable) {
|
|
811
|
+
if (firebaseSuccess) {
|
|
812
|
+
lines.push(`| 🔥 Firebase App Distribution | ✅ 완료 | ${step4Elapsed || '-'} |`);
|
|
813
|
+
} else {
|
|
814
|
+
lines.push(`| 🔥 Firebase App Distribution | ❌ 실패 | ${step4Elapsed || '-'} |`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const version = '${{ needs.prepare-test-build.outputs.version }}';
|
|
819
|
+
|
|
820
|
+
lines.push(
|
|
821
|
+
'',
|
|
822
|
+
'### 📦 빌드 결과',
|
|
823
|
+
'| 항목 | 값 |',
|
|
824
|
+
'|------|-----|',
|
|
825
|
+
`| **앱 버전** | \`${version}(${buildNumber})\` |`,
|
|
826
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
827
|
+
`| **커밋** | \`${commitHash}\` |`,
|
|
828
|
+
`| **총 소요 시간** | ${totalElapsed} |`,
|
|
829
|
+
'',
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
if (firebaseAvailable && firebaseSuccess) {
|
|
833
|
+
lines.push('🔥 **Firebase App Tester 앱에서 설치 가능합니다**');
|
|
834
|
+
} else if (firebaseAvailable && !firebaseSuccess) {
|
|
835
|
+
lines.push('⚠️ **Firebase 업로드 실패 - 아티팩트에서 APK를 다운로드해주세요**');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
lines.push(
|
|
839
|
+
`📦 **[아티팩트 다운로드](${artifactUrl})**`,
|
|
840
|
+
`🔗 **[워크플로우 실행 로그](${runUrl})**`
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const body = lines.join('\n');
|
|
844
|
+
|
|
845
|
+
await github.rest.issues.updateComment({
|
|
846
|
+
owner: context.repo.owner,
|
|
847
|
+
repo: context.repo.repo,
|
|
848
|
+
comment_id: commentId,
|
|
849
|
+
body: body
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
console.log(`✅ 진행상황 업데이트: 빌드 성공 (총 ${totalElapsed})`);
|
|
853
|
+
|
|
854
|
+
// PR과 연결된 이슈가 있고, PR 번호와 다른 경우 이슈에도 댓글
|
|
855
|
+
if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) {
|
|
856
|
+
const issueLines = [
|
|
857
|
+
'🤖 ✅ **Android APK 빌드 완료**',
|
|
858
|
+
'',
|
|
859
|
+
`- 앱 버전: \`${version}(${buildNumber})\``,
|
|
860
|
+
`- 커밋: \`${commitHash}\``,
|
|
861
|
+
'',
|
|
862
|
+
];
|
|
863
|
+
|
|
864
|
+
if (firebaseAvailable && firebaseSuccess) {
|
|
865
|
+
issueLines.push('🔥 **Firebase App Tester 앱에서 설치 가능합니다**');
|
|
866
|
+
} else if (firebaseAvailable && !firebaseSuccess) {
|
|
867
|
+
issueLines.push('⚠️ **Firebase 업로드 실패 - 아티팩트에서 APK를 다운로드해주세요**');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
issueLines.push(`📦 **[아티팩트 다운로드](${artifactUrl})**`);
|
|
871
|
+
|
|
872
|
+
await github.rest.issues.createComment({
|
|
873
|
+
owner: context.repo.owner,
|
|
874
|
+
repo: context.repo.repo,
|
|
875
|
+
issue_number: parseInt(issueNumber),
|
|
876
|
+
body: issueLines.join('\n')
|
|
877
|
+
});
|
|
878
|
+
console.log(`✅ 이슈 #${issueNumber}에도 빌드 성공 댓글 작성 완료`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
# 진행 상황 최종 업데이트 - 실패
|
|
882
|
+
- name: 진행 상황 최종 업데이트 - 실패
|
|
883
|
+
if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != ''
|
|
884
|
+
uses: actions/github-script@v7
|
|
885
|
+
with:
|
|
886
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
887
|
+
script: |
|
|
888
|
+
const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
|
|
889
|
+
const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
|
|
890
|
+
const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}') || 0;
|
|
891
|
+
const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
|
|
892
|
+
const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
|
|
893
|
+
const buildNumber = '${{ needs.prepare-test-build.outputs.build_number }}';
|
|
894
|
+
const version = '${{ needs.prepare-test-build.outputs.version }}';
|
|
895
|
+
const branchName = '${{ needs.prepare-test-build.outputs.branch_name }}';
|
|
896
|
+
const issueNumber = '${{ needs.prepare-test-build.outputs.issue_number }}';
|
|
897
|
+
const runId = '${{ github.run_id }}';
|
|
898
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
899
|
+
|
|
900
|
+
const now = Date.now();
|
|
901
|
+
|
|
902
|
+
let step2ElapsedStr = '-';
|
|
903
|
+
if (step1EndTime > 0) {
|
|
904
|
+
const step2_s = Math.round((now - step1EndTime) / 1000);
|
|
905
|
+
const s2Min = Math.floor(step2_s / 60);
|
|
906
|
+
const s2Sec = step2_s % 60;
|
|
907
|
+
step2ElapsedStr = s2Min > 0 ? `${s2Min}분 ${s2Sec}초` : `${s2Sec}초`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const total_s = Math.round((now - startTime) / 1000);
|
|
911
|
+
const tMin = Math.floor(total_s / 60);
|
|
912
|
+
const tSec = total_s % 60;
|
|
913
|
+
const totalElapsed = tMin > 0 ? `${tMin}분 ${tSec}초` : `${tSec}초`;
|
|
914
|
+
|
|
915
|
+
const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
|
|
916
|
+
|
|
917
|
+
const rows = [
|
|
918
|
+
`| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
|
|
919
|
+
`| 🔨 APK 빌드 | ❌ 실패 | ${step2ElapsedStr} |`,
|
|
920
|
+
'| 📤 GitHub 아티팩트 업로드 | ⏸️ 건너뜀 | - |',
|
|
921
|
+
];
|
|
922
|
+
if (firebaseAvailable) {
|
|
923
|
+
rows.push('| 🔥 Firebase App Distribution | ⏸️ 건너뜀 | - |');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const body = [
|
|
927
|
+
'## 🤖 ❌ Android APK 빌드 실패',
|
|
928
|
+
'',
|
|
929
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
930
|
+
'|------|------|----------|',
|
|
931
|
+
...rows,
|
|
932
|
+
'',
|
|
933
|
+
`- 앱 버전: \`${version}(${buildNumber})\``,
|
|
934
|
+
`- 브랜치: \`${branchName}\``,
|
|
935
|
+
`- 총 소요 시간: ${totalElapsed}`,
|
|
936
|
+
'',
|
|
937
|
+
'❌ 빌드 중 오류가 발생했습니다.',
|
|
938
|
+
'',
|
|
939
|
+
`🔗 **[워크플로우 실행 로그 확인](${runUrl})**`
|
|
940
|
+
].join('\n');
|
|
941
|
+
|
|
942
|
+
await github.rest.issues.updateComment({
|
|
943
|
+
owner: context.repo.owner,
|
|
944
|
+
repo: context.repo.repo,
|
|
945
|
+
comment_id: commentId,
|
|
946
|
+
body: body
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
console.log(`❌ 진행상황 업데이트: 빌드 실패 (${totalElapsed})`);
|
|
950
|
+
|
|
951
|
+
if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) {
|
|
952
|
+
const issueBody = [
|
|
953
|
+
'🤖 ❌ **Android APK 빌드 실패**',
|
|
954
|
+
'',
|
|
955
|
+
`- 앱 버전: \`${version}(${buildNumber})\``,
|
|
956
|
+
'',
|
|
957
|
+
'❌ 빌드 중 오류가 발생했습니다.',
|
|
958
|
+
'',
|
|
959
|
+
`🔗 **[워크플로우 실행 로그 확인](${runUrl})**`
|
|
960
|
+
].join('\n');
|
|
961
|
+
|
|
962
|
+
await github.rest.issues.createComment({
|
|
963
|
+
owner: context.repo.owner,
|
|
964
|
+
repo: context.repo.repo,
|
|
965
|
+
issue_number: parseInt(issueNumber),
|
|
966
|
+
body: issueBody
|
|
967
|
+
});
|
|
968
|
+
console.log(`❌ 이슈 #${issueNumber}에도 빌드 실패 댓글 작성 완료`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# 콘솔 알림
|
|
972
|
+
- name: Notify Build Success
|
|
973
|
+
if: success()
|
|
974
|
+
run: |
|
|
975
|
+
echo "✅ Android 테스트 APK 빌드 성공!"
|
|
976
|
+
echo "빌드 번호: ${{ needs.prepare-test-build.outputs.build_number }}"
|
|
977
|
+
if [ -n "${{ needs.prepare-test-build.outputs.pr_number }}" ]; then
|
|
978
|
+
echo "PR 번호: ${{ needs.prepare-test-build.outputs.pr_number }}"
|
|
979
|
+
fi
|
|
980
|
+
echo "브랜치: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
|
|
981
|
+
echo "커밋: ${{ needs.prepare-test-build.outputs.commit_hash }}"
|
|
982
|
+
if [ -n "${{ needs.prepare-test-build.outputs.issue_url }}" ]; then
|
|
983
|
+
echo "관련 이슈: ${{ needs.prepare-test-build.outputs.issue_url }}"
|
|
984
|
+
fi
|
|
985
|
+
echo ""
|
|
986
|
+
echo "📦 아티팩트에서 APK 파일을 다운로드할 수 있습니다."
|
|
987
|
+
|
|
988
|
+
- name: Notify on Failure
|
|
989
|
+
if: failure()
|
|
990
|
+
run: |
|
|
991
|
+
echo "❌ Android 테스트 APK 빌드 실패!"
|
|
992
|
+
echo "로그를 확인해주세요."
|