Repository navigation
229 lines (205 loc) · 8.85 KB
/
Copy pathci.yml
File metadata and controls
229 lines (205 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
name: CI
on:
workflow_dispatch:
push:
branches: [main, master]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main, master]
paths-ignore:
- '**.md'
- 'docs/**'
env:
FLUTTER_VERSION: '3.41.5'
jobs:
analyze:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: flutter-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
- name: Get dependencies
run: flutter pub get
- name: Run analyze
# integration_test 由独立设备流程负责,不纳入主 CI 的静态分析门禁。
run: flutter analyze --no-fatal-warnings --no-fatal-infos lib test
test:
runs-on: ubuntu-latest
needs: analyze
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: flutter-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
- name: Get dependencies
run: flutter pub get
- name: Run tests
# Ubuntu runner 上全量测试收尾时,偶发「全部用例通过但 flutter test 以 1
# 退出」的 Flutter 测试运行器收尾工具链 flakiness(发生在所有用例已完成、
# reporter 已写出结果之后,与 app 代码无关)。done.success 也可能被污染;
# 少数用例还会只出现无 error payload 的 testDone error。真实断言失败会伴随
# error 事件,因此 CI 只把带 error payload 的 failure/error 判为失败。
run: |
# 多个测试文件共享平台插件、全局 provider 和测试数据库;串行执行避免
# 文件级并发导致的跨测试污染。单个文件内的 testWidgets 仍按测试框架顺序执行。
flutter test \
--concurrency=1 \
--file-reporter=json:test-results.json || true
python3 - <<'PY'
import json, sys
done = None
names, errors, failures = {}, {}, []
try:
with open('test-results.json', encoding='utf-8') as f:
for line in f:
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
event_type = event.get('type')
if event_type == 'testStart':
test = event.get('test') or {}
test_id = test.get('id')
if test_id is not None:
names[test_id] = test.get('name') or f'test #{test_id}'
elif event_type == 'error':
test_id = event.get('testID')
if test_id is not None:
errors.setdefault(test_id, []).append(event)
elif event_type == 'testDone':
if event.get('result') in ('failure', 'error'):
failures.append(event)
elif event_type == 'done':
done = event
except FileNotFoundError:
print('::error::test-results.json 缺失 —— 测试未产出结果')
sys.exit(1)
if done is None:
print('::error::未找到 done 事件,测试运行未正常结束')
sys.exit(1)
real_failures = [
event for event in failures
if errors.get(event.get('testID'))
]
orphan_failures = [
event for event in failures
if not errors.get(event.get('testID'))
]
if orphan_failures:
print(f'::warning::忽略 {len(orphan_failures)} 个无错误载荷的 testDone failure/error')
for event in orphan_failures:
test_id = event.get('testID')
print(f'无错误载荷用例: {names.get(test_id, f"test #{test_id}")} ({event.get("result")})')
if real_failures:
print(f'::error::flutter test 报告 {len(real_failures)} 个失败/错误用例')
for event in real_failures:
test_id = event.get('testID')
print(f'失败用例: {names.get(test_id, f"test #{test_id}")} ({event.get("result")})')
for error in errors.get(test_id, []):
print(error.get('error') or '')
print(error.get('stackTrace') or '')
sys.exit(1)
print('未发现失败/错误用例,测试通过')
PY
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: flutter-test-results
path: test-results.json
if-no-files-found: warn
retention-days: 14
build:
needs: [analyze, test]
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
platform: [ios, android]
include:
- platform: ios
runner: macos-latest
- platform: android
runner: ubuntu-latest
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: flutter-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
- name: Get dependencies
run: flutter pub get
# Pods 缓存键必须同时绑定 pubspec.lock:ffmpeg_kit_flutter_new_min 的
# FFmpegKit xcframework 由 podspec prepare_command 下载到 pub-cache 包目录,
# 该命令只在 pod 首次安装时执行。若 pub-cache 因 pubspec.lock 变更被重建(Frameworks 丢失),
# 而 Pods 缓存仅按 Podfile.lock 命中,pod install 会跳过 prepare_command,
# 导致 framework 头文件缺失、iOS 构建失败。不用 restore-keys(部分命中会带回旧
# Manifest.lock,同样跳过 prepare_command)。
- name: Cache CocoaPods
if: matrix.platform == 'ios'
uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock', 'pubspec.lock') }}
- name: Install CocoaPods
if: matrix.platform == 'ios'
run: cd ios && pod install
- name: Build iOS (no codesign)
if: matrix.platform == 'ios'
run: |
flutter build ios --release --no-codesign --flavor=prod \
--dart-define=API_BASE_URL=${{ vars.API_BASE_URL }} \
--dart-define=API_CHINA_BASE_URL=${{ vars.API_CHINA_BASE_URL }} \
--dart-define=POSTHOG_HOST=${{ vars.POSTHOG_HOST }} \
--dart-define=SUPABASE_URL=${{ vars.SUPABASE_URL }} \
--dart-define=CHINA_SUPABASE_URL=${{ vars.CHINA_SUPABASE_URL }} \
--dart-define=SUPABASE_PUBLISHABLE_KEY=${{ vars.SUPABASE_PUBLISHABLE_KEY }} \
--dart-define=GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}
- name: Setup Java
if: matrix.platform == 'android'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Restore Android signing config
if: matrix.platform == 'android'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks
cat > android/key.properties <<EOF
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=../app/upload-keystore.jks
EOF
- name: Build Android APK
if: matrix.platform == 'android'
run: |
flutter clean
flutter build apk --release --flavor=prod \
--target-platform android-arm64 \
--dart-define=API_BASE_URL=${{ vars.API_BASE_URL }} \
--dart-define=API_CHINA_BASE_URL=${{ vars.API_CHINA_BASE_URL }} \
--dart-define=POSTHOG_HOST=${{ vars.POSTHOG_HOST }} \
--dart-define=SUPABASE_URL=${{ vars.SUPABASE_URL }} \
--dart-define=CHINA_SUPABASE_URL=${{ vars.CHINA_SUPABASE_URL }} \
--dart-define=SUPABASE_PUBLISHABLE_KEY=${{ vars.SUPABASE_PUBLISHABLE_KEY }} \
--dart-define=GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}