CHORE: 升级版本号到 1.0.27 #169
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| 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 | |
| run: flutter analyze --no-fatal-warnings --no-fatal-infos | |
| 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: | | |
| flutter test \ | |
| --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=POSTHOG_HOST=${{ vars.POSTHOG_HOST }} \ | |
| --dart-define=SUPABASE_URL=${{ vars.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=POSTHOG_HOST=${{ vars.POSTHOG_HOST }} \ | |
| --dart-define=SUPABASE_URL=${{ vars.SUPABASE_URL }} \ | |
| --dart-define=SUPABASE_PUBLISHABLE_KEY=${{ vars.SUPABASE_PUBLISHABLE_KEY }} \ | |
| --dart-define=GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }} |