project-auto-wizard 0.13.3 → 0.14.0

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.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/bin/project-auto-wizard.js +18 -4
  3. package/package.json +2 -2
  4. package/payload/config/breaking-changes.json +28 -14
  5. package/payload/config/wizard-prompts.yml +102 -46
  6. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  7. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  8. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  9. package/payload/scripts/changelog_manager.py +296 -257
  10. package/payload/scripts/issue_helper.py +54 -50
  11. package/payload/scripts/messages.py +2279 -0
  12. package/payload/scripts/truncate_release_notes.py +7 -6
  13. package/payload/scripts/version_manager.py +64 -61
  14. package/payload/version.yml.template +2 -1
  15. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
  16. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
  17. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  18. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
  19. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
  20. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  29. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  30. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  31. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  32. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  33. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  34. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  35. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  36. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  37. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  38. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  39. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  40. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  41. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  42. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  44. package/src/cli/args.js +98 -52
  45. package/src/cli/help.js +8 -41
  46. package/src/commands/doctor.js +85 -89
  47. package/src/commands/dry-run.js +63 -43
  48. package/src/commands/full.js +79 -77
  49. package/src/commands/install-settings.js +16 -15
  50. package/src/commands/interactive-flutter.js +20 -19
  51. package/src/commands/interactive.js +135 -132
  52. package/src/commands/purge.js +47 -42
  53. package/src/commands/status.js +36 -36
  54. package/src/commands/uninstall.js +43 -41
  55. package/src/context.js +12 -11
  56. package/src/core/assets.js +12 -11
  57. package/src/core/baseline.js +27 -27
  58. package/src/core/branches.js +29 -27
  59. package/src/core/branding.js +7 -6
  60. package/src/core/breaking-check.js +22 -21
  61. package/src/core/breaking.js +15 -8
  62. package/src/core/copy/app-files.js +5 -4
  63. package/src/core/copy/flutter-app.js +8 -8
  64. package/src/core/copy/gitignore.js +58 -50
  65. package/src/core/copy/readme.js +45 -35
  66. package/src/core/copy/simple.js +16 -15
  67. package/src/core/copy/workflows.js +131 -124
  68. package/src/core/deploy-style.js +51 -48
  69. package/src/core/detect-fs.js +57 -57
  70. package/src/core/detect.js +67 -65
  71. package/src/core/errors.js +2 -2
  72. package/src/core/flutter-doctor.js +17 -16
  73. package/src/core/flutter-hooks.js +33 -32
  74. package/src/core/flutter-options.js +34 -33
  75. package/src/core/fsutil.js +12 -12
  76. package/src/core/installed-stores.js +4 -4
  77. package/src/core/logger.js +42 -40
  78. package/src/core/paths-resolve.js +88 -86
  79. package/src/core/paths.js +10 -10
  80. package/src/core/release-options.js +8 -8
  81. package/src/core/removal-exec.js +14 -13
  82. package/src/core/removal-plan.js +37 -37
  83. package/src/core/types.js +47 -47
  84. package/src/core/verify.js +44 -38
  85. package/src/core/version-yml.js +76 -68
  86. package/src/core/wizard-env.js +46 -46
  87. package/src/core/wizard-labels.js +63 -41
  88. package/src/i18n/catalog/en/cli.js +50 -0
  89. package/src/i18n/catalog/en/commands.js +187 -0
  90. package/src/i18n/catalog/en/copy.js +19 -0
  91. package/src/i18n/catalog/en/core.js +74 -0
  92. package/src/i18n/catalog/en/core2.js +15 -0
  93. package/src/i18n/catalog/en/ui.js +221 -0
  94. package/src/i18n/catalog/en.js +12 -0
  95. package/src/i18n/catalog/index.js +5 -0
  96. package/src/i18n/catalog/ko/cli.js +50 -0
  97. package/src/i18n/catalog/ko/commands.js +187 -0
  98. package/src/i18n/catalog/ko/copy.js +19 -0
  99. package/src/i18n/catalog/ko/core.js +74 -0
  100. package/src/i18n/catalog/ko/core2.js +15 -0
  101. package/src/i18n/catalog/ko/ui.js +221 -0
  102. package/src/i18n/catalog/ko.js +10 -0
  103. package/src/i18n/index.js +44 -0
  104. package/src/i18n/languages.js +9 -0
  105. package/src/index.js +128 -94
  106. package/src/ui/ansi.js +7 -7
  107. package/src/ui/banner.js +7 -6
  108. package/src/ui/env-plan.js +82 -81
  109. package/src/ui/prompts.js +97 -122
  110. package/src/ui/readline-engine.js +59 -58
  111. package/src/ui/status-cards.js +37 -31
  112. package/src/ui/summary.js +89 -88
@@ -1,45 +1,45 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # Flutter CI (코드 분석 및 빌드 검증) 워크플로우
2
+ # Flutter CI (code analysis and build verification) workflow
3
3
  #
4
- # 이 워크플로우는 PR 생성 또는 {{MAIN_BRANCH}} 브랜치 푸시 시
5
- # 코드 정적 분석(flutter analyze), 테스트(flutter test)와 Android/iOS 앱 빌드를 검증합니다.
4
+ # On a pull request or a push to the {{MAIN_BRANCH}} branch, this workflow
5
+ # verifies static analysis (flutter analyze), tests (flutter test) and the Android/iOS app builds.
6
6
  #
7
- # 주요 특징:
8
- # - PR 생성/업데이트 시 자동 실행
9
- # - {{MAIN_BRANCH}} 브랜치 푸시 시 자동 실행
10
- # - flutter analyze로 코드 품질 검사
11
- # - test/ 폴더에 테스트(*_test.dart)가 있으면 flutter test 실행 (없으면 건너뜀)
12
- # - Android/iOS 빌드 개별 활성화/비활성화 가능
13
- # - Analyze Only 모드: 빌드 없이 코드 분석만 실행 가능
14
- # - PR인 경우 진행 상황을 댓글로 실시간 업데이트
15
- # - 빌드 실패 시 상세 에러 정보 제공
16
- # - Analyze와 빌드 병렬 실행으로 시간 단축
7
+ # Key features:
8
+ # - Runs automatically when a PR is opened/updated
9
+ # - Runs automatically on a push to the {{MAIN_BRANCH}} branch
10
+ # - Checks code quality with flutter analyze
11
+ # - Runs flutter test when test/ contains tests (*_test.dart); skipped otherwise
12
+ # - Android/iOS builds can be enabled/disabled individually
13
+ # - Analyze Only mode: run only the code analysis, without builds
14
+ # - On a PR, progress is updated live in a comment
15
+ # - Detailed error info when a build fails
16
+ # - Analyze and builds run in parallel to save time
17
17
  #
18
- # 사용 방법:
19
- # 1. 프로젝트에 맞게 상단 env 섹션의 값들을 수정하세요
20
- # 2. ANALYZE_ONLY를 true로 설정하면 빌드 없이 코드 분석만 실행
21
- # 3. ENABLE_ANDROID, ENABLE_IOS로 빌드 대상 플랫폼 선택
22
- # 4. ENV_FILE_PATH로 환경변수 파일 경로 커스터마이징
18
+ # Usage:
19
+ # 1. Edit the values in the env section at the top for your project
20
+ # 2. Set ANALYZE_ONLY to true to run only the code analysis, without builds
21
+ # 3. Choose the build target platforms with ENABLE_ANDROID and ENABLE_IOS
22
+ # 4. Customize the env file path with ENV_FILE_PATH
23
23
  #
24
- # 프로젝트별 설정
24
+ # Project settings
25
25
  #
26
- # ANALYZE_ONLY : true면 analyze·test만 실행, 빌드 스킵 (기본: false)
27
- # ENABLE_ANDROID : Android 빌드 활성화 여부 (ANALYZE_ONLY=false일 때)
28
- # ENABLE_IOS : iOS 빌드 활성화 여부 (ANALYZE_ONLY=false일 때)
29
- # FLUTTER_VERSION : Flutter SDK 버전 (빈 값 = stable 최신)
30
- # JAVA_VERSION : Java 버전 (Android 빌드용)
31
- # XCODE_VERSION : Xcode 버전 (iOS 빌드용)
32
- # ENV_FILE_PATH : dotenv 모드에서 .env를 만들 경로 (Flutter 루트 기준, 기본값: .env)
33
- # ENV_MODE : 환경변수 전달 방식 — dart-define(--dart-define-from-file) | dotenv(.env 파일)
34
- # FLUTTER_PROJECT_DIR : Flutter 루트 경로 (설치 시 자동 설정, 모노레포는 app 등)
35
- # PROJECT_PATH : 변경 감지 기준 경로 (설치 시 자동 설정, "."이면 항상 실행)
26
+ # ANALYZE_ONLY : if true, only analyze and test run and builds are skipped (default: false)
27
+ # ENABLE_ANDROID : whether to build Android (when ANALYZE_ONLY=false)
28
+ # ENABLE_IOS : whether to build iOS (when ANALYZE_ONLY=false)
29
+ # FLUTTER_VERSION : Flutter SDK version (empty = latest stable)
30
+ # JAVA_VERSION : Java version (for the Android build)
31
+ # XCODE_VERSION : Xcode version (for the iOS build)
32
+ # ENV_FILE_PATH : path where .env is created in dotenv mode (relative to the Flutter root, default: .env)
33
+ # ENV_MODE : how env vars are passed - dart-define (--dart-define-from-file) | dotenv (.env file)
34
+ # FLUTTER_PROJECT_DIR : Flutter root path (set automatically at install; app etc. for a monorepo)
35
+ # PROJECT_PATH : path used for change detection (set automatically at install; "." always runs)
36
36
  #
37
- # 필수 GitHub Secrets
38
- # ENV_FILE (선택): .env 파일 내용
37
+ # Required GitHub Secrets
38
+ # ENV_FILE (optional): contents of the .env file
39
39
  #
40
- # ※ CI는 빌드 검증 목적이므로 서명/배포 관련 Secrets 불필요
41
- # ※ 모노레포에서 이 CI를 required check로 쓰려면 개별 job이 아니라 `ci-gate` 하나만 등록하세요.
42
- # (경로가 바뀌지 않아 건너뛴 job은 Success로 집계되고, ci-gate가 최종 결과를 판정합니다)
40
+ # Note: CI only verifies builds, so no signing/deploy Secrets are needed
41
+ # Note: to use this CI as a required check in a monorepo, register only `ci-gate`, not the individual jobs.
42
+ # (jobs skipped because no paths changed count as Success, and ci-gate decides the final result)
43
43
 
44
44
  name: PROJECT-Flutter-CI
45
45
 
@@ -52,15 +52,15 @@ on:
52
52
  workflow_dispatch:
53
53
  inputs:
54
54
  analyze_only:
55
- description: 'Analyze Only 모드 (빌드 없이 코드 분석만)'
55
+ description: 'Analyze Only mode (code analysis only, no builds)'
56
56
  type: boolean
57
57
  default: false
58
58
  enable_android:
59
- description: 'Android 빌드 활성화 (Analyze Only=false일 때)'
59
+ description: 'Enable the Android build (when Analyze Only is false)'
60
60
  type: boolean
61
61
  default: true
62
62
  enable_ios:
63
- description: 'iOS 빌드 활성화 (Analyze Only=false일 때)'
63
+ description: 'Enable the iOS build (when Analyze Only is false)'
64
64
  type: boolean
65
65
  default: true
66
66
 
@@ -68,38 +68,38 @@ permissions:
68
68
  contents: read
69
69
  pull-requests: write
70
70
 
71
- # 프로젝트별 설정 (아래 값들을 수정하세요)
71
+ # Project settings (edit the values below)
72
72
  env:
73
- # 모노레포 변경 감지 경로 (설치 시 자동 설정, 단일 레포는 ".")
73
+ # Monorepo change-detection path (set automatically at install; "." for a single repo)
74
74
  PROJECT_PATH: "." # @wizard auto:project-path
75
- # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등 — 설치 시 자동 설정
75
+ # Flutter root path (relative to the repo root): "." for a single repo, "app" etc. for a monorepo - set automatically at install
76
76
  FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
77
- # 환경변수 전달 방식 (dart-define | dotenv) — 설치 시 선택값으로 자동 설정
77
+ # How env vars are passed (dart-define | dotenv) - set automatically from the install-time choice
78
78
  ENV_MODE: "dart-define" # @wizard auto:flutter-env-mode
79
79
 
80
- # CI 모드 설정
81
- ANALYZE_ONLY: "false" # true: analyze·test만 실행 (빌드 스킵), false: analyze·test + 빌드
80
+ # CI mode settings
81
+ ANALYZE_ONLY: "false" # true: run only analyze and test (skip builds), false: analyze, test and builds
82
82
 
83
- # 빌드 대상 플랫폼 설정 (ANALYZE_ONLY=false일 때만 적용)
84
- ENABLE_ANDROID: "true" # Android 빌드 활성화 (true/false)
85
- ENABLE_IOS: "true" # iOS 빌드 활성화 (true/false)
83
+ # Build target platform settings (only applied when ANALYZE_ONLY=false)
84
+ ENABLE_ANDROID: "true" # enable the Android build (true/false)
85
+ ENABLE_IOS: "true" # enable the iOS build (true/false)
86
86
 
87
- # 프로젝트별 설정
88
- # Flutter SDK 버전 — 비워 두면 stable 채널 최신을 쓴다(최신 flutter create 프로젝트의 SDK 제약과 맞음).
89
- # 특정 버전에 고정하려면 "3.38.9"처럼 적는다. pubspec.yaml의 environment.sdk 범위를 만족해야 한다.
87
+ # Project settings
88
+ # Flutter SDK version - leave empty to use the latest stable channel (matches the SDK constraint of a fresh flutter create project).
89
+ # To pin a version, write it like "3.38.9". It must satisfy the environment.sdk range in pubspec.yaml.
90
90
  FLUTTER_VERSION: ""
91
91
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
92
92
  XCODE_VERSION: "26.0"
93
- ENV_FILE_PATH: ".env" # dotenv 모드에서 .env를 만들 경로 (Flutter 루트 기준, 커스터마이징 가능)
93
+ ENV_FILE_PATH: ".env" # path where .env is created in dotenv mode (relative to the Flutter root, customizable)
94
94
 
95
95
  concurrency:
96
96
  group: flutter-ci-${{ github.ref }}
97
97
  cancel-in-progress: true
98
98
 
99
99
  jobs:
100
- # Job 0: 변경 감지 (PROJECT_PATH 아래가 바뀌었을 때만 이후 job 실행)
100
+ # Job 0: change detection (later jobs run only when something under PROJECT_PATH changed)
101
101
  changes:
102
- name: 변경 감지
102
+ name: Detect changes
103
103
  runs-on: ubuntu-latest
104
104
  permissions:
105
105
  contents: read
@@ -110,22 +110,22 @@ jobs:
110
110
  - name: Checkout repository
111
111
  uses: actions/checkout@v7
112
112
 
113
- - name: 프로젝트 경로 변경 여부 판별
113
+ - name: Check whether the project path changed
114
114
  id: filter
115
115
  if: ${{ github.event_name != 'workflow_dispatch' }}
116
116
  uses: dorny/paths-filter@v4
117
117
  with:
118
- # push는 이번 push로 들어온 커밋만 비교한다(직전 커밋 github.event.before 기준).
119
- # 비워 두면 기본 브랜치와의 누적 diff로 판정해 skip이 사실상 동작하지 않는다.
120
- # 새 브랜치 첫 push(before가 0으로만 채워짐)나 PR은 빈 값 → 기본 동작(기본 브랜치·PR base 비교).
118
+ # For a push, compare only the commits in this push (relative to the previous commit, github.event.before).
119
+ # If left empty, the result is based on the cumulative diff against the default branch, so skipping would effectively never happen.
120
+ # The first push of a new branch (before is all zeros) or a PR uses an empty value -> default behavior (compare against the default branch / PR base).
121
121
  base: ${{ github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000' && github.event.before || '' }}
122
122
  filters: |
123
123
  project:
124
124
  - '${{ env.PROJECT_PATH == '.' && '**' || format('{0}/**', env.PROJECT_PATH) }}'
125
125
 
126
- # Job 1: 준비 단계
126
+ # Job 1: preparation
127
127
  prepare:
128
- name: CI 준비
128
+ name: Prepare CI
129
129
  needs: changes
130
130
  if: ${{ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') }}
131
131
  runs-on: ubuntu-latest
@@ -140,11 +140,23 @@ jobs:
140
140
  enable_ios: ${{ steps.config.outputs.enable_ios }}
141
141
 
142
142
  steps:
143
- # 설정 값 outputs으로 전달
144
- - name: 설정 값 전달
143
+ - name: Checkout messages
144
+ uses: actions/checkout@v7
145
+ with:
146
+ sparse-checkout: |
147
+ .github/scripts/messages.py
148
+ version.yml
149
+ sparse-checkout-cone-mode: false
150
+
151
+ - name: Load messages
152
+ run: echo "PAW_MSG=$(cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py dump flutter_a.ci_)" >> "$GITHUB_ENV"
153
+
154
+ # Pass the settings on as outputs
155
+ - name: Pass settings
145
156
  id: config
146
157
  run: |
147
- # workflow_dispatch 입력값 확인
158
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
159
+ # Check the workflow_dispatch inputs
148
160
  if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
149
161
  ANALYZE_ONLY="${{ github.event.inputs.analyze_only }}"
150
162
  ENABLE_ANDROID="${{ github.event.inputs.enable_android }}"
@@ -158,15 +170,16 @@ jobs:
158
170
  echo "analyze_only=$ANALYZE_ONLY" >> $GITHUB_OUTPUT
159
171
  echo "enable_android=$ENABLE_ANDROID" >> $GITHUB_OUTPUT
160
172
  echo "enable_ios=$ENABLE_IOS" >> $GITHUB_OUTPUT
161
- echo "📋 CI 설정:"
173
+ m flutter_a.ci_config_header
162
174
  echo " Analyze Only: $ANALYZE_ONLY"
163
175
  echo " Android: $ENABLE_ANDROID"
164
176
  echo " iOS: $ENABLE_IOS"
165
- echo " 트리거: ${{ github.event_name }}"
177
+ m flutter_a.ci_config_trigger event_name="${{ github.event_name }}"
166
178
 
167
- - name: 빌드 정보 수집
179
+ - name: Collect build info
168
180
  id: info
169
181
  run: |
182
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
170
183
  if [ "${{ github.event_name }}" == "pull_request" ]; then
171
184
  BRANCH_NAME="${{ github.head_ref }}"
172
185
  else
@@ -177,17 +190,19 @@ jobs:
177
190
  echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
178
191
  echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
179
192
 
180
- echo "📋 빌드 정보:"
181
- echo " 브랜치: $BRANCH_NAME"
182
- echo " 커밋: $COMMIT_HASH"
193
+ m flutter_a.ci_info_header
194
+ m flutter_a.ci_info_branch branch_name="$BRANCH_NAME"
195
+ m flutter_a.ci_info_commit commit_hash="$COMMIT_HASH"
183
196
 
184
- - name: 진행 상황 댓글 생성
197
+ - name: Create progress comment
185
198
  id: comment
186
199
  if: github.event_name == 'pull_request'
187
200
  uses: actions/github-script@v9
188
201
  with:
189
202
  github-token: ${{ secrets.GITHUB_TOKEN }}
190
203
  script: |
204
+ const M = JSON.parse(process.env.PAW_MSG);
205
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
191
206
  const prNumber = context.payload.pull_request.number;
192
207
  const branchName = '${{ steps.info.outputs.branch_name }}';
193
208
  const commitHash = '${{ steps.info.outputs.commit_hash }}';
@@ -198,47 +213,47 @@ jobs:
198
213
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
199
214
  const startTime = Date.now();
200
215
 
201
- // 헤더 결정
216
+ // Decide the header
202
217
  const header = analyzeOnly
203
- ? '## 🔍 Flutter CI (Analyze Only) 진행 중...'
204
- : '## 🔨 Flutter CI 진행 중...';
218
+ ? m('flutter_a.ci_hdr_analyze_running')
219
+ : m('flutter_a.ci_hdr_running');
205
220
 
206
- // 플랫폼 상태 결정
221
+ // Decide the platform status
207
222
  let androidStatus, iosStatus;
208
223
  if (analyzeOnly) {
209
- androidStatus = '⏸️ 스킵';
210
- iosStatus = '⏸️ 스킵';
224
+ androidStatus = m('flutter_a.ci_status_skipped');
225
+ iosStatus = m('flutter_a.ci_status_skipped');
211
226
  } else {
212
- androidStatus = enableAndroid ? '⏳ 진행 중...' : '⏸️ 비활성화';
213
- iosStatus = enableIos ? '⏳ 진행 중...' : '⏸️ 비활성화';
227
+ androidStatus = enableAndroid ? m('flutter_a.ci_status_running') : m('flutter_a.ci_status_disabled');
228
+ iosStatus = enableIos ? m('flutter_a.ci_status_running') : m('flutter_a.ci_status_disabled');
214
229
  }
215
230
 
216
231
  const bodyParts = [
217
232
  header,
218
233
  '',
219
- '| 검사 항목 | 상태 | 소요 시간 |',
234
+ m('flutter_a.ci_table_head'),
220
235
  '|----------|------|----------|',
221
- '| 🔍 Analyze | ⏳ 진행 중... | - |',
222
- '| 🧪 Test | ⏳ 진행 중... | - |',
223
- `| 🤖 Android 빌드 | ${androidStatus} | - |`,
224
- `| 🍎 iOS 빌드 | ${iosStatus} | - |`,
236
+ m('flutter_a.ci_row_analyze_running'),
237
+ m('flutter_a.ci_row_test_running'),
238
+ m('flutter_a.ci_row_android_pending', { status: androidStatus }),
239
+ m('flutter_a.ci_row_ios_pending', { status: iosStatus }),
225
240
  '',
226
- '| 항목 | 값 |',
241
+ m('flutter_a.ci_info_head'),
227
242
  '|------|-----|',
228
- `| **브랜치** | \`${branchName}\` |`,
229
- `| **커밋** | \`${commitHash}\` |`
243
+ m('flutter_a.ci_row_branch', { branch: branchName }),
244
+ m('flutter_a.ci_row_commit', { commit: commitHash })
230
245
  ];
231
246
 
232
247
  if (analyzeOnly) {
233
248
  bodyParts.push('');
234
- bodyParts.push('ℹ️ **Analyze Only 모드**: 빌드 없이 코드 분석만 실행됩니다.');
249
+ bodyParts.push(m('flutter_a.ci_note_analyze_only_running'));
235
250
  }
236
251
 
237
252
  bodyParts.push('');
238
- bodyParts.push(`**[📋 실시간 로그 보기](${runUrl})**`);
253
+ bodyParts.push(m('flutter_a.ci_link_live_log', { url: runUrl }));
239
254
  bodyParts.push('');
240
255
  bodyParts.push('---');
241
- bodyParts.push('*🤖 이 댓글은 자동으로 업데이트됩니다.*');
256
+ bodyParts.push(m('flutter_a.ci_footer_auto_update'));
242
257
 
243
258
  const body = bodyParts.join('\n');
244
259
 
@@ -249,13 +264,13 @@ jobs:
249
264
  body: body
250
265
  });
251
266
 
252
- console.log(`✅ 진행 상황 댓글 생성 완료: #${comment.id}`);
267
+ console.log(m('flutter_a.ci_log_comment_created', { id: comment.id }));
253
268
  core.setOutput('comment_id', comment.id);
254
269
  core.setOutput('start_time', startTime);
255
270
 
256
- # Job 2: 코드 분석 (병렬)
271
+ # Job 2: code analysis (parallel)
257
272
  analyze:
258
- name: 코드 분석
273
+ name: Code analysis
259
274
  runs-on: ubuntu-latest
260
275
  needs: [changes, prepare]
261
276
  if: ${{ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') }}
@@ -269,7 +284,7 @@ jobs:
269
284
  duration: ${{ steps.result.outputs.duration }}
270
285
 
271
286
  steps:
272
- - name: 분석 시작 시간 기록
287
+ - name: Record analysis start time
273
288
  id: analyze_start
274
289
  uses: actions/github-script@v9
275
290
  with:
@@ -279,7 +294,10 @@ jobs:
279
294
  - name: Checkout repository
280
295
  uses: actions/checkout@v7
281
296
 
282
- # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
297
+ - name: Load messages
298
+ run: echo "PAW_MSG=$(cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py dump flutter_a.ci_)" >> "$GITHUB_ENV"
299
+
300
+ # Prepare env vars - a .env file when ENV_MODE=dotenv, a temp file outside the project (--dart-define-from-file) for dart-define
283
301
  - name: Prepare env file
284
302
  env:
285
303
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
@@ -325,41 +343,45 @@ jobs:
325
343
  - name: Run Flutter Analyze
326
344
  id: analyze
327
345
  run: |
328
- echo "🔍 Flutter 코드 분석 시작..."
346
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
347
+ m flutter_a.ci_analyze_start
329
348
  flutter analyze --no-fatal-infos
330
- echo "✅ 코드 분석 완료"
349
+ m flutter_a.ci_analyze_done
331
350
 
332
- # analyze가 실패해도 테스트 결과는 따로 보여 준다. 실패하면 job이 실패해 CI Gate도 실패한다.
351
+ # Show the test result separately even if analyze fails. A failure fails the job and therefore the CI Gate too.
333
352
  - name: Run Flutter Test
334
353
  id: test
335
354
  if: ${{ !cancelled() && steps.deps.outcome == 'success' }}
336
355
  run: |
337
- # test/에 *_test.dart가 없으면 flutter test 자체가 실패하므로 건너뛴다
356
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
357
+ # flutter test itself fails when test/ has no *_test.dart, so skip it
338
358
  if [ -d test ] && [ -n "$(find test -name '*_test.dart' -print -quit)" ]; then
339
- echo "🧪 Flutter 테스트 시작..."
359
+ m flutter_a.ci_test_start
340
360
  flutter test ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
341
361
  echo "ran=true" >> "$GITHUB_OUTPUT"
342
- echo "✅ 테스트 통과"
362
+ m flutter_a.ci_test_passed
343
363
  else
344
- echo "::notice title=Flutter 테스트 건너뜀::test/ 폴더에 *_test.dart 파일이 없어 flutter test를 실행하지 않았습니다"
364
+ echo "::notice title=$(m flutter_a.ci_test_skipped_title)::$(m flutter_a.ci_test_skipped_body)"
345
365
  echo "ran=false" >> "$GITHUB_OUTPUT"
346
366
  fi
347
367
 
348
- - name: 분석 결과 기록
368
+ - name: Record analysis result
349
369
  id: result
350
370
  if: always()
351
371
  uses: actions/github-script@v9
352
372
  with:
353
373
  script: |
374
+ const M = JSON.parse(process.env.PAW_MSG);
375
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
354
376
  const analyzeStart = parseInt('${{ steps.analyze_start.outputs.time }}');
355
377
  const now = Date.now();
356
378
  const elapsed = now - analyzeStart;
357
379
  const minutes = Math.floor(elapsed / 60000);
358
380
  const seconds = Math.floor((elapsed % 60000) / 1000);
359
- const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
381
+ const duration = minutes > 0 ? m('flutter_a.ci_dur_min_sec', { minutes, seconds }) : m('flutter_a.ci_dur_sec', { seconds });
360
382
 
361
383
  const status = '${{ steps.analyze.outcome }}' === 'success' ? 'success' : 'failure';
362
- // none: 테스트 파일 없음, skipped: 의존성 설치 실패로 실행 못 함
384
+ // none: no test files, skipped: not run because the dependency install failed
363
385
  const testOutcome = '${{ steps.test.outcome }}';
364
386
  const testStatus = testOutcome === 'success'
365
387
  ? ('${{ steps.test.outputs.ran }}' === 'true' ? 'success' : 'none')
@@ -368,11 +390,11 @@ jobs:
368
390
  core.setOutput('status', status);
369
391
  core.setOutput('test_status', testStatus);
370
392
  core.setOutput('duration', duration);
371
- console.log(`📋 코드 분석 결과: ${status}, 테스트: ${testStatus} (${duration})`);
393
+ console.log(m('flutter_a.ci_log_analyze_result', { status, test: testStatus, duration }));
372
394
 
373
- # Job 3: Android 빌드 (조건부, 병렬)
395
+ # Job 3: Android build (conditional, parallel)
374
396
  build-android:
375
- name: Android 빌드
397
+ name: Android build
376
398
  if: |
377
399
  (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') &&
378
400
  needs.prepare.outputs.analyze_only != 'true' &&
@@ -388,7 +410,7 @@ jobs:
388
410
  duration: ${{ steps.result.outputs.duration }}
389
411
 
390
412
  steps:
391
- - name: 빌드 시작 시간 기록
413
+ - name: Record build start time
392
414
  id: build_start
393
415
  uses: actions/github-script@v9
394
416
  with:
@@ -398,7 +420,10 @@ jobs:
398
420
  - name: Checkout repository
399
421
  uses: actions/checkout@v7
400
422
 
401
- # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
423
+ - name: Load messages
424
+ run: echo "PAW_MSG=$(cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py dump flutter_a.ci_)" >> "$GITHUB_ENV"
425
+
426
+ # Prepare env vars - a .env file when ENV_MODE=dotenv, a temp file outside the project (--dart-define-from-file) for dart-define
402
427
  - name: Prepare env file
403
428
  env:
404
429
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
@@ -464,45 +489,49 @@ jobs:
464
489
  - name: Setup Gradle
465
490
  working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/android
466
491
  run: |
467
- # flutter create 기본 .gitignore는 gradlew를 커밋하지 않는다 — 없으면 flutter build가 만든다
492
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
493
+ # The default flutter create .gitignore does not commit gradlew - flutter build creates it when missing
468
494
  if [ -f gradlew ]; then
469
495
  chmod +x gradlew
470
496
  echo "✅ Gradle wrapper permissions set"
471
497
  else
472
- echo "ℹ️ gradlew가 저장소에 없습니다 — flutter build가 생성합니다"
498
+ m flutter_a.gradlew_missing
473
499
  fi
474
500
 
475
- # APK 빌드 (CI는 빌드 검증 목적이므로 서명 불필요한 debug 빌드 사용)
501
+ # APK build (CI only verifies the build, so use a debug build that needs no signing)
476
502
  - name: Build APK
477
503
  id: build
478
504
  run: |
479
- echo "📦 Flutter APK 빌드 시작 (debug)..."
505
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
506
+ m flutter_a.ci_apk_start
480
507
  flutter build apk --debug ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
481
- echo "✅ APK 빌드 완료"
508
+ m flutter_a.ci_apk_done
482
509
  ls -la ./build/app/outputs/flutter-apk/ || true
483
510
 
484
- - name: 빌드 결과 기록
511
+ - name: Record build result
485
512
  id: result
486
513
  if: always()
487
514
  uses: actions/github-script@v9
488
515
  with:
489
516
  script: |
517
+ const M = JSON.parse(process.env.PAW_MSG);
518
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
490
519
  const buildStart = parseInt('${{ steps.build_start.outputs.time }}');
491
520
  const now = Date.now();
492
521
  const elapsed = now - buildStart;
493
522
  const minutes = Math.floor(elapsed / 60000);
494
523
  const seconds = Math.floor((elapsed % 60000) / 1000);
495
- const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
524
+ const duration = minutes > 0 ? m('flutter_a.ci_dur_min_sec', { minutes, seconds }) : m('flutter_a.ci_dur_sec', { seconds });
496
525
 
497
526
  const status = '${{ steps.build.outcome }}' === 'success' ? 'success' : 'failure';
498
527
 
499
528
  core.setOutput('status', status);
500
529
  core.setOutput('duration', duration);
501
- console.log(`📋 Android 빌드 결과: ${status} (${duration})`);
530
+ console.log(m('flutter_a.ci_log_android_result', { status, duration }));
502
531
 
503
- # Job 4: iOS 빌드 (조건부, 병렬)
532
+ # Job 4: iOS build (conditional, parallel)
504
533
  build-ios:
505
- name: iOS 빌드
534
+ name: iOS build
506
535
  if: |
507
536
  (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') &&
508
537
  needs.prepare.outputs.analyze_only != 'true' &&
@@ -518,7 +547,7 @@ jobs:
518
547
  duration: ${{ steps.result.outputs.duration }}
519
548
 
520
549
  steps:
521
- - name: 빌드 시작 시간 기록
550
+ - name: Record build start time
522
551
  id: build_start
523
552
  uses: actions/github-script@v9
524
553
  with:
@@ -528,12 +557,15 @@ jobs:
528
557
  - name: Checkout repository
529
558
  uses: actions/checkout@v7
530
559
 
560
+ - name: Load messages
561
+ run: echo "PAW_MSG=$(cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py dump flutter_a.ci_)" >> "$GITHUB_ENV"
562
+
531
563
  - name: Select Xcode version
532
564
  run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
533
565
 
534
566
  - name: Install iOS device platform
535
567
  env:
536
- # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
568
+ # xcode-select alone does not propagate to subprocesses, so set it explicitly
537
569
  DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
538
570
  run: |
539
571
  echo "=== iPhoneOS.platform check ==="
@@ -548,7 +580,7 @@ jobs:
548
580
  echo "=== Downloading iOS platform ==="
549
581
  xcodebuild -downloadPlatform iOS
550
582
 
551
- # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
583
+ # Prepare env vars - a .env file when ENV_MODE=dotenv, a temp file outside the project (--dart-define-from-file) for dart-define
552
584
  - name: Prepare env file
553
585
  env:
554
586
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
@@ -598,54 +630,71 @@ jobs:
598
630
 
599
631
  - name: Install CocoaPods
600
632
  run: |
601
- # 플러그인이 없는 기본 프로젝트는 Podfile이 없다 — 플러그인이 생기면 flutter build가 Podfile을 만든다
633
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
634
+ # A default project without plugins has no Podfile - once plugins are added, flutter build creates it
602
635
  if [ -f ios/Podfile ]; then
603
636
  gem install cocoapods
604
637
  (cd ios && (pod install --repo-update || pod install))
605
638
  echo "✅ CocoaPods installed"
606
639
  else
607
- echo "ℹ️ ios/Podfile이 없어 pod install을 건너뜁니다 (CocoaPods 플러그인 없음)"
640
+ m flutter_a.ci_podfile_missing
608
641
  fi
609
642
 
610
643
  - name: Build iOS
611
644
  id: build
612
645
  run: |
613
- echo "📦 Flutter iOS 빌드 시작..."
646
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
647
+ m flutter_a.ci_ios_start
614
648
  flutter build ios --release --no-codesign ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
615
- echo "✅ iOS 빌드 완료"
649
+ m flutter_a.ci_ios_done
616
650
 
617
- - name: 빌드 결과 기록
651
+ - name: Record build result
618
652
  id: result
619
653
  if: always()
620
654
  uses: actions/github-script@v9
621
655
  with:
622
656
  script: |
657
+ const M = JSON.parse(process.env.PAW_MSG);
658
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
623
659
  const buildStart = parseInt('${{ steps.build_start.outputs.time }}');
624
660
  const now = Date.now();
625
661
  const elapsed = now - buildStart;
626
662
  const minutes = Math.floor(elapsed / 60000);
627
663
  const seconds = Math.floor((elapsed % 60000) / 1000);
628
- const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
664
+ const duration = minutes > 0 ? m('flutter_a.ci_dur_min_sec', { minutes, seconds }) : m('flutter_a.ci_dur_sec', { seconds });
629
665
 
630
666
  const status = '${{ steps.build.outcome }}' === 'success' ? 'success' : 'failure';
631
667
 
632
668
  core.setOutput('status', status);
633
669
  core.setOutput('duration', duration);
634
- console.log(`📋 iOS 빌드 결과: ${status} (${duration})`);
670
+ console.log(m('flutter_a.ci_log_ios_result', { status, duration }));
635
671
 
636
- # Job 5: 결과 보고
672
+ # Job 5: result report
637
673
  report:
638
- name: 결과 보고
674
+ name: Report results
639
675
  if: always() && github.event_name == 'pull_request' && needs.changes.outputs.project == 'true'
640
676
  runs-on: ubuntu-latest
641
677
  needs: [changes, prepare, analyze, build-android, build-ios]
642
678
 
643
679
  steps:
644
- - name: 최종 결과 댓글 업데이트
680
+ - name: Checkout messages
681
+ uses: actions/checkout@v7
682
+ with:
683
+ sparse-checkout: |
684
+ .github/scripts/messages.py
685
+ version.yml
686
+ sparse-checkout-cone-mode: false
687
+
688
+ - name: Load messages
689
+ run: echo "PAW_MSG=$(cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py dump flutter_a.ci_)" >> "$GITHUB_ENV"
690
+
691
+ - name: Update final result comment
645
692
  uses: actions/github-script@v9
646
693
  with:
647
694
  github-token: ${{ secrets.GITHUB_TOKEN }}
648
695
  script: |
696
+ const M = JSON.parse(process.env.PAW_MSG);
697
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
649
698
  const commentId = parseInt('${{ needs.prepare.outputs.comment_id }}');
650
699
  const startTime = parseInt('${{ needs.prepare.outputs.start_time }}');
651
700
  const branchName = '${{ needs.prepare.outputs.branch_name }}';
@@ -656,7 +705,7 @@ jobs:
656
705
  const runId = '${{ github.run_id }}';
657
706
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
658
707
 
659
- // 결과 수집
708
+ // Collect the results
660
709
  const analyzeStatus = '${{ needs.analyze.outputs.status }}';
661
710
  const analyzeDuration = '${{ needs.analyze.outputs.duration }}' || '-';
662
711
  const testStatus = '${{ needs.analyze.outputs.test_status }}';
@@ -665,137 +714,137 @@ jobs:
665
714
  const iosStatus = '${{ needs.build-ios.outputs.status }}';
666
715
  const iosDuration = '${{ needs.build-ios.outputs.duration }}' || '-';
667
716
 
668
- // 전체 소요 시간 계산
717
+ // Compute the total duration
669
718
  const now = Date.now();
670
719
  const totalElapsed = now - startTime;
671
720
  const totalMin = Math.floor(totalElapsed / 60000);
672
721
  const totalSec = Math.floor((totalElapsed % 60000) / 1000);
673
- const totalDuration = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`;
722
+ const totalDuration = totalMin > 0 ? m('flutter_a.ci_dur_min_sec', { minutes: totalMin, seconds: totalSec }) : m('flutter_a.ci_dur_sec', { seconds: totalSec });
674
723
 
675
- // 상태 결정
724
+ // Decide the status
676
725
  let overallSuccess = true;
677
726
  let analyzeDisplay, androidDisplay, iosDisplay;
678
727
 
679
- // Analyze 상태
728
+ // Analyze status
680
729
  if (analyzeStatus === 'success') {
681
- analyzeDisplay = '✅ 성공';
730
+ analyzeDisplay = m('flutter_a.ci_status_success');
682
731
  } else {
683
- analyzeDisplay = '❌ 실패';
732
+ analyzeDisplay = m('flutter_a.ci_status_failure');
684
733
  overallSuccess = false;
685
734
  }
686
735
 
687
- // Test 상태 (테스트 파일이 없으면 실패로 보지 않는다)
736
+ // Test status (no test files is not treated as a failure)
688
737
  let testDisplay;
689
738
  if (testStatus === 'success') {
690
- testDisplay = '✅ 성공';
739
+ testDisplay = m('flutter_a.ci_status_success');
691
740
  } else if (testStatus === 'none') {
692
- testDisplay = '⏸️ 테스트 없음';
741
+ testDisplay = m('flutter_a.ci_status_no_tests');
693
742
  } else {
694
- testDisplay = testStatus === 'failure' ? '❌ 실패' : '⏸️ 실행 안 됨';
743
+ testDisplay = testStatus === 'failure' ? m('flutter_a.ci_status_failure') : m('flutter_a.ci_status_not_run');
695
744
  overallSuccess = false;
696
745
  }
697
746
 
698
- // Android 상태
747
+ // Android status
699
748
  if (analyzeOnly) {
700
- androidDisplay = '⏸️ 스킵';
749
+ androidDisplay = m('flutter_a.ci_status_skipped');
701
750
  } else if (!enableAndroid) {
702
- androidDisplay = '⏸️ 비활성화';
751
+ androidDisplay = m('flutter_a.ci_status_disabled');
703
752
  } else if (androidStatus === 'success') {
704
- androidDisplay = '✅ 성공';
753
+ androidDisplay = m('flutter_a.ci_status_success');
705
754
  } else {
706
- androidDisplay = '❌ 실패';
755
+ androidDisplay = m('flutter_a.ci_status_failure');
707
756
  overallSuccess = false;
708
757
  }
709
758
 
710
- // iOS 상태
759
+ // iOS status
711
760
  if (analyzeOnly) {
712
- iosDisplay = '⏸️ 스킵';
761
+ iosDisplay = m('flutter_a.ci_status_skipped');
713
762
  } else if (!enableIos) {
714
- iosDisplay = '⏸️ 비활성화';
763
+ iosDisplay = m('flutter_a.ci_status_disabled');
715
764
  } else if (iosStatus === 'success') {
716
- iosDisplay = '✅ 성공';
765
+ iosDisplay = m('flutter_a.ci_status_success');
717
766
  } else {
718
- iosDisplay = '❌ 실패';
767
+ iosDisplay = m('flutter_a.ci_status_failure');
719
768
  overallSuccess = false;
720
769
  }
721
770
 
722
- // 헤더 결정
771
+ // Decide the header
723
772
  let header;
724
773
  if (analyzeOnly) {
725
774
  header = overallSuccess
726
- ? '## ✅ Flutter CI (Analyze Only) 성공!'
727
- : '## ❌ Flutter CI (Analyze Only) 실패';
775
+ ? m('flutter_a.ci_hdr_analyze_success')
776
+ : m('flutter_a.ci_hdr_analyze_failure');
728
777
  } else {
729
778
  header = overallSuccess
730
- ? '## ✅ Flutter CI 성공!'
731
- : '## ❌ Flutter CI 실패';
779
+ ? m('flutter_a.ci_hdr_success')
780
+ : m('flutter_a.ci_hdr_failure');
732
781
  }
733
782
 
734
- // 댓글 본문 생성
783
+ // Build the comment body
735
784
  const bodyParts = [
736
785
  header,
737
786
  '',
738
- '| 검사 항목 | 상태 | 소요 시간 |',
787
+ m('flutter_a.ci_table_head'),
739
788
  '|----------|------|----------|',
740
789
  `| 🔍 Analyze | ${analyzeDisplay} | ${analyzeDuration} |`,
741
790
  `| 🧪 Test | ${testDisplay} | - |`,
742
- `| 🤖 Android 빌드 | ${androidDisplay} | ${analyzeOnly || !enableAndroid ? '-' : androidDuration} |`,
743
- `| 🍎 iOS 빌드 | ${iosDisplay} | ${analyzeOnly || !enableIos ? '-' : iosDuration} |`,
791
+ m('flutter_a.ci_row_android_result', { status: androidDisplay, duration: analyzeOnly || !enableAndroid ? '-' : androidDuration }),
792
+ m('flutter_a.ci_row_ios_result', { status: iosDisplay, duration: analyzeOnly || !enableIos ? '-' : iosDuration }),
744
793
  '',
745
- '| 항목 | 값 |',
794
+ m('flutter_a.ci_info_head'),
746
795
  '|------|-----|',
747
- `| **브랜치** | \`${branchName}\` |`,
748
- `| **커밋** | \`${commitHash}\` |`,
749
- `| **총 소요 시간** | ${totalDuration} |`,
796
+ m('flutter_a.ci_row_branch', { branch: branchName }),
797
+ m('flutter_a.ci_row_commit', { commit: commitHash }),
798
+ m('flutter_a.ci_row_total', { duration: totalDuration }),
750
799
  ''
751
800
  ];
752
801
 
753
- // Analyze Only 모드 안내
802
+ // Analyze Only mode note
754
803
  if (analyzeOnly && overallSuccess) {
755
- bodyParts.push('ℹ️ **Analyze Only 모드**: 빌드 없이 코드 분석만 실행되었습니다.');
804
+ bodyParts.push(m('flutter_a.ci_note_analyze_only_done'));
756
805
  bodyParts.push('');
757
806
  }
758
807
 
759
- // 실패 시 추가 안내
808
+ // Extra guidance on failure
760
809
  if (!overallSuccess) {
761
810
  bodyParts.push(
762
- '### 💡 확인 사항',
811
+ m('flutter_a.ci_hdr_checks'),
763
812
  ''
764
813
  );
765
814
 
766
815
  if (analyzeStatus !== 'success') {
767
- bodyParts.push('**Analyze 실패 시:**');
768
- bodyParts.push('- `flutter analyze` 로컬에서 실행하여 lint 오류 확인');
769
- bodyParts.push('- 코드 스타일 및 타입 오류 수정');
816
+ bodyParts.push(m('flutter_a.ci_analyze_fail_title'));
817
+ bodyParts.push(m('flutter_a.ci_analyze_fail_lint'));
818
+ bodyParts.push(m('flutter_a.ci_analyze_fail_fix'));
770
819
  bodyParts.push('');
771
820
  }
772
821
 
773
822
  if (testStatus === 'failure') {
774
- bodyParts.push('**Test 실패 시:**');
775
- bodyParts.push('- `flutter test` 로컬에서 실행하여 실패한 테스트 확인');
823
+ bodyParts.push(m('flutter_a.ci_test_fail_title'));
824
+ bodyParts.push(m('flutter_a.ci_test_fail_local'));
776
825
  bodyParts.push('');
777
826
  }
778
827
 
779
828
  if (!analyzeOnly && enableAndroid && androidStatus !== 'success') {
780
- bodyParts.push('**Android 빌드 실패 시:**');
781
- bodyParts.push('- Gradle 빌드 로그에서 에러 확인');
782
- bodyParts.push('- 의존성 버전 호환성 확인');
829
+ bodyParts.push(m('flutter_a.ci_android_fail_title'));
830
+ bodyParts.push(m('flutter_a.ci_android_fail_gradle'));
831
+ bodyParts.push(m('flutter_a.ci_android_fail_deps'));
783
832
  bodyParts.push('');
784
833
  }
785
834
 
786
835
  if (!analyzeOnly && enableIos && iosStatus !== 'success') {
787
- bodyParts.push('**iOS 빌드 실패 시:**');
788
- bodyParts.push('- Xcode 빌드 로그에서 에러 확인');
789
- bodyParts.push('- CocoaPods 의존성 확인');
836
+ bodyParts.push(m('flutter_a.ci_ios_fail_title'));
837
+ bodyParts.push(m('flutter_a.ci_ios_fail_xcode'));
838
+ bodyParts.push(m('flutter_a.ci_ios_fail_pods'));
790
839
  bodyParts.push('');
791
840
  }
792
841
  }
793
842
 
794
- bodyParts.push(`**[📋 워크플로우 로그](${runUrl})**`);
843
+ bodyParts.push(m('flutter_a.ci_link_workflow_log', { url: runUrl }));
795
844
 
796
845
  const body = bodyParts.join('\n');
797
846
 
798
- // 댓글 업데이트
847
+ // Update the comment
799
848
  if (commentId) {
800
849
  await github.rest.issues.updateComment({
801
850
  owner: context.repo.owner,
@@ -803,25 +852,34 @@ jobs:
803
852
  comment_id: commentId,
804
853
  body: body
805
854
  });
806
- console.log(`✅ 결과 댓글 업데이트 완료`);
855
+ console.log(m('flutter_a.ci_log_result_updated'));
807
856
  } else {
808
- console.log('⚠️ 댓글 ID가 없어 업데이트를 건너뜁니다.');
857
+ console.log(m('flutter_a.ci_log_no_comment_id'));
809
858
  }
810
859
 
811
- # Job 6: CI Gate (required check로 등록할 단일 진입점)
812
- # 건너뛴(skipped) job은 통과로 보고 failure/cancelled가 하나라도 있으면 실패한다.
860
+ # Job 6: CI Gate (the single entry point to register as a required check)
861
+ # Skipped jobs count as passed; any failure/cancelled result fails the gate.
813
862
  ci-gate:
814
863
  name: CI Gate
815
864
  if: ${{ always() }}
816
865
  needs: [changes, prepare, analyze, build-android, build-ios, report]
817
866
  runs-on: ubuntu-latest
818
867
  steps:
819
- - name: 결과 집계
868
+ - name: Checkout messages
869
+ uses: actions/checkout@v7
870
+ with:
871
+ sparse-checkout: |
872
+ .github/scripts/messages.py
873
+ version.yml
874
+ sparse-checkout-cone-mode: false
875
+
876
+ - name: Aggregate results
820
877
  env:
821
878
  RESULTS: ${{ toJSON(needs.*.result) }}
822
879
  run: |
823
- echo "needs 결과: $RESULTS"
880
+ m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
881
+ m flutter_a.ci_gate_results results="$RESULTS"
824
882
  if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
825
- echo "실패하거나 취소된 job이 있습니다."
883
+ m flutter_a.ci_gate_failed
826
884
  exit 1
827
885
  fi