Skip to content

Commit 98cd2fd

Browse files
feat: ci
1 parent e7b0076 commit 98cd2fd

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master, staging, 'sdk*']
6+
pull_request:
7+
branches: [master, staging]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ci-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
env:
15+
FLUTTER_VERSION: '3.32.0'
16+
17+
jobs:
18+
# ── Analyze & Format ──────────────────────────────────────────────
19+
analyze:
20+
name: Analyze & Format
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
submodules: recursive
26+
27+
- uses: subosito/flutter-action@v2
28+
with:
29+
flutter-version: ${{ env.FLUTTER_VERSION }}
30+
cache: true
31+
32+
- name: Install dependencies
33+
run: flutter pub get
34+
35+
- name: Check formatting
36+
run: dart format --set-exit-if-changed --line-length=300 lib/
37+
38+
- name: Analyze
39+
run: flutter analyze --no-fatal-infos
40+
41+
# ── Build Android ─────────────────────────────────────────────────
42+
build-android:
43+
name: Build Android
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
with:
48+
submodules: recursive
49+
50+
- uses: actions/setup-java@v4
51+
with:
52+
distribution: 'corretto'
53+
java-version: '17'
54+
55+
- uses: subosito/flutter-action@v2
56+
with:
57+
flutter-version: ${{ env.FLUTTER_VERSION }}
58+
cache: true
59+
60+
- name: Install dependencies
61+
run: flutter pub get
62+
63+
- name: Build example APK
64+
working-directory: example
65+
run: flutter build apk --debug
66+
67+
# ── Build iOS ─────────────────────────────────────────────────────
68+
build-ios:
69+
name: Build iOS
70+
runs-on: macos-latest
71+
steps:
72+
- uses: actions/checkout@v4
73+
with:
74+
submodules: recursive
75+
76+
- uses: subosito/flutter-action@v2
77+
with:
78+
flutter-version: ${{ env.FLUTTER_VERSION }}
79+
cache: true
80+
81+
- name: Install dependencies
82+
run: flutter pub get
83+
84+
- name: Build example (iOS Simulator)
85+
working-directory: example
86+
run: flutter build ios --simulator --no-codesign
87+
88+
# ── Integration Tests (iOS) ───────────────────────────────────────
89+
test-ios:
90+
name: "Tests iOS: ${{ matrix.category }}"
91+
runs-on: macos-latest
92+
needs: build-ios
93+
strategy:
94+
fail-fast: false
95+
matrix:
96+
category: [light, server]
97+
steps:
98+
- uses: actions/checkout@v4
99+
with:
100+
submodules: recursive
101+
102+
- uses: subosito/flutter-action@v2
103+
with:
104+
flutter-version: ${{ env.FLUTTER_VERSION }}
105+
cache: true
106+
107+
- name: Install dependencies
108+
run: flutter pub get
109+
110+
- name: Boot iOS Simulator
111+
run: |
112+
DEVICE_ID=$(xcrun simctl list devices available -j | python3 -c "
113+
import json,sys
114+
data=json.load(sys.stdin)
115+
for runtime,devices in data['devices'].items():
116+
if 'iOS' in runtime:
117+
for d in devices:
118+
if d['isAvailable'] and 'iPhone' in d['name']:
119+
print(d['udid']); sys.exit()
120+
")
121+
xcrun simctl boot "${DEVICE_ID}" 2>/dev/null || true
122+
echo "DEVICE_ID=${DEVICE_ID}" >> "${GITHUB_ENV}"
123+
124+
- name: Run tests
125+
env:
126+
TEST_CATEGORY: ${{ matrix.category }}
127+
working-directory: example
128+
run: ../scripts/run_integration_tests.sh --"${TEST_CATEGORY}" --device "${DEVICE_ID}" --fresh
129+
timeout-minutes: 30
130+
131+
- name: Generate summary
132+
if: always()
133+
run: |
134+
PASSED=$(grep -rh "PASSED" test_results/*/worker_*.log 2>/dev/null | wc -l | tr -d ' ')
135+
FAILED=$(grep -rh "FAILED" test_results/*/worker_*.log 2>/dev/null | wc -l | tr -d ' ')
136+
FAILED_LIST=$(grep -rh "FAILED" test_results/*/worker_*.log 2>/dev/null | sed 's/.*FAILED (\(.*\)).*/\1/' | tr '\n' ',' | sed 's/,$//')
137+
echo "{\"platform\":\"iOS\",\"category\":\"${{ matrix.category }}\",\"passed\":${PASSED:-0},\"failed\":${FAILED:-0},\"failed_tests\":\"${FAILED_LIST}\"}" > test_results/summary.json
138+
139+
- name: Upload test results
140+
if: always()
141+
uses: actions/upload-artifact@v4
142+
with:
143+
name: test-results-ios-${{ matrix.category }}
144+
path: test_results/
145+
retention-days: 14
146+
147+
# ── Integration Tests (Android) ───────────────────────────────────
148+
test-android:
149+
name: "Tests Android: ${{ matrix.category }}"
150+
runs-on: ubuntu-latest
151+
needs: build-android
152+
strategy:
153+
fail-fast: false
154+
matrix:
155+
category: [light, server]
156+
steps:
157+
- uses: actions/checkout@v4
158+
with:
159+
submodules: recursive
160+
161+
- uses: actions/setup-java@v4
162+
with:
163+
distribution: 'corretto'
164+
java-version: '17'
165+
166+
- uses: subosito/flutter-action@v2
167+
with:
168+
flutter-version: ${{ env.FLUTTER_VERSION }}
169+
cache: true
170+
171+
- name: Install dependencies
172+
run: flutter pub get
173+
174+
- name: Enable KVM
175+
run: |
176+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
177+
sudo udevadm control --reload-rules
178+
sudo udevadm trigger --name-match=kvm
179+
180+
- name: Run tests
181+
uses: reactivecircus/android-emulator-runner@v2
182+
env:
183+
TEST_CATEGORY: ${{ matrix.category }}
184+
with:
185+
api-level: 34
186+
arch: x86_64
187+
profile: pixel_6
188+
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect
189+
script: cd example && ../scripts/run_integration_tests.sh --"${TEST_CATEGORY}" --fresh
190+
timeout-minutes: 45
191+
192+
- name: Generate summary
193+
if: always()
194+
run: |
195+
PASSED=$(grep -rh "PASSED" test_results/*/worker_*.log 2>/dev/null | wc -l | tr -d ' ')
196+
FAILED=$(grep -rh "FAILED" test_results/*/worker_*.log 2>/dev/null | wc -l | tr -d ' ')
197+
FAILED_LIST=$(grep -rh "FAILED" test_results/*/worker_*.log 2>/dev/null | sed 's/.*FAILED (\(.*\)).*/\1/' | tr '\n' ',' | sed 's/,$//')
198+
echo "{\"platform\":\"Android\",\"category\":\"${{ matrix.category }}\",\"passed\":${PASSED:-0},\"failed\":${FAILED:-0},\"failed_tests\":\"${FAILED_LIST}\"}" > test_results/summary.json
199+
200+
- name: Upload test results
201+
if: always()
202+
uses: actions/upload-artifact@v4
203+
with:
204+
name: test-results-android-${{ matrix.category }}
205+
path: test_results/
206+
retention-days: 14
207+
208+
# ── Report Results to PR ──────────────────────────────────────────
209+
report:
210+
name: Report Results
211+
if: always() && github.event_name == 'pull_request'
212+
needs: [analyze, test-ios, test-android]
213+
runs-on: ubuntu-latest
214+
permissions:
215+
pull-requests: write
216+
steps:
217+
- name: Download all test results
218+
uses: actions/download-artifact@v4
219+
with:
220+
pattern: test-results-*
221+
path: results/
222+
223+
- name: Build report and comment on PR
224+
env:
225+
GH_TOKEN: ${{ github.token }}
226+
PR_NUMBER: ${{ github.event.pull_request.number }}
227+
REPO: ${{ github.repository }}
228+
ANALYZE_RESULT: ${{ needs.analyze.result }}
229+
run: |
230+
BODY="## CI Test Results\n\n"
231+
232+
if [ "${ANALYZE_RESULT}" = "success" ]; then
233+
BODY+="**Analyze & Format:** :white_check_mark: Passed\n\n"
234+
else
235+
BODY+="**Analyze & Format:** :x: Failed\n\n"
236+
fi
237+
238+
BODY+="### Integration Tests\n\n"
239+
BODY+="| Platform | Category | Passed | Failed | Status |\n"
240+
BODY+="|----------|----------|--------|--------|--------|\n"
241+
242+
TOTAL_PASSED=0
243+
TOTAL_FAILED=0
244+
FAILED_DETAILS=""
245+
246+
for summary in results/*/summary.json; do
247+
[ -f "$summary" ] || continue
248+
PLATFORM=$(python3 -c "import json; d=json.load(open('$summary')); print(d['platform'])")
249+
CATEGORY=$(python3 -c "import json; d=json.load(open('$summary')); print(d['category'])")
250+
PASSED=$(python3 -c "import json; d=json.load(open('$summary')); print(d['passed'])")
251+
FAILED=$(python3 -c "import json; d=json.load(open('$summary')); print(d['failed'])")
252+
FAILED_TESTS=$(python3 -c "import json; d=json.load(open('$summary')); print(d.get('failed_tests',''))")
253+
254+
TOTAL_PASSED=$((TOTAL_PASSED + PASSED))
255+
TOTAL_FAILED=$((TOTAL_FAILED + FAILED))
256+
257+
if [ "$FAILED" -gt 0 ]; then
258+
STATUS=":x: Failed"
259+
FAILED_DETAILS+="\n**${PLATFORM} - ${CATEGORY}:** ${FAILED_TESTS}"
260+
else
261+
STATUS=":white_check_mark: Passed"
262+
fi
263+
264+
BODY+="| ${PLATFORM} | ${CATEGORY} | ${PASSED} | ${FAILED} | ${STATUS} |\n"
265+
done
266+
267+
BODY+="\n**Total: ${TOTAL_PASSED} passed, ${TOTAL_FAILED} failed**\n"
268+
269+
if [ -n "$FAILED_DETAILS" ]; then
270+
BODY+="\n<details><summary>Failed tests</summary>\n${FAILED_DETAILS}\n</details>\n"
271+
fi
272+
273+
COMMENT_TAG="<!-- ci-test-results -->"
274+
BODY="${COMMENT_TAG}\n${BODY}"
275+
276+
EXISTING=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq ".[] | select(.body | contains(\"${COMMENT_TAG}\")) | .id" | head -1)
277+
278+
if [ -n "$EXISTING" ]; then
279+
printf "$BODY" | gh api "repos/${REPO}/issues/comments/${EXISTING}" -X PATCH -F "body=@-"
280+
else
281+
printf "$BODY" | gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -F "body=@-"
282+
fi

0 commit comments

Comments
 (0)