fix: harden hosted platform orchestration #11
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: Cross-Platform Validation | |
| on: | |
| pull_request: | |
| push: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: cross-platform-validation-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| FLUTTER_VERSION: 3.47.0-0.2.pre | |
| jobs: | |
| windows-validation: | |
| runs-on: windows-latest | |
| timeout-minutes: 120 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - run: flutter pub get | |
| - name: Build Windows release | |
| id: release_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build windows --release | |
| - name: Snapshot Windows production application | |
| id: production_e2e_artifact | |
| if: always() && steps.release_build.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| $source = 'lddc/build/windows/x64/runner/Release' | |
| $destination = 'lddc/build/production_e2e/windows' | |
| if (Test-Path -LiteralPath $destination) { | |
| Remove-Item -LiteralPath $destination -Recurse -Force | |
| } | |
| Copy-Item -LiteralPath $source -Destination $destination -Recurse | |
| if (-not (Test-Path -LiteralPath "$destination/lddc.exe" -PathType Leaf)) { | |
| throw 'Windows production snapshot is incomplete' | |
| } | |
| - name: Build Windows debug application for native automation | |
| id: debug_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build windows --debug | |
| - name: Run offline application flows | |
| id: offline | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_real_integration.ps1 -Profile offline -Device windows -Platform windows | |
| - name: Run native channel flows | |
| id: native_channel | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_real_integration.ps1 -Profile platform -Device windows -Platform windows | |
| - name: Run Windows file dialog automation | |
| id: system_ui | |
| if: always() && steps.debug_build.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_windows_platform_tests.ps1 | |
| - name: Run real desktop process and service tests | |
| id: desktop_process | |
| continue-on-error: true | |
| shell: pwsh | |
| run: >- | |
| ./tool/test/run_desktop_process_e2e.ps1 -Platform windows | |
| -AppExe lddc/build/production_e2e/windows/lddc.exe | |
| - name: Run production media resource checks | |
| id: media_resource | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_platform_media_resource.ps1 -Platform windows -LoopCount 3 | |
| - name: Summarize required Windows phases | |
| id: required_outcomes | |
| if: always() | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| python tool/test/ci_phase_summary.py ` | |
| --title 'Windows validation' ` | |
| --phase 'release_build|Release build|${{ steps.release_build.outcome }}' ` | |
| --phase 'production_e2e_artifact|Production snapshot|${{ steps.production_e2e_artifact.outcome }}' ` | |
| --phase 'debug_build|Debug build|${{ steps.debug_build.outcome }}' ` | |
| --phase 'offline|Offline application|${{ steps.offline.outcome }}' ` | |
| --phase 'native_channel|Native channels|${{ steps.native_channel.outcome }}' ` | |
| --phase 'system_ui|System file dialog|${{ steps.system_ui.outcome }}' ` | |
| --phase 'desktop_process|Desktop process|${{ steps.desktop_process.outcome }}' ` | |
| --phase 'media_resource|Media resources|${{ steps.media_resource.outcome }}' | |
| - name: Upload Windows diagnostics | |
| if: always() && steps.required_outcomes.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: windows-validation-diagnostics | |
| path: lddc/build/integration_reports/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Enforce required Windows result | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ('${{ steps.required_outcomes.outcome }}' -ne 'success') { | |
| throw 'Windows required validation failed; see the phase summary and diagnostics artifact' | |
| } | |
| macos-validation: | |
| runs-on: macos-15 | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - run: flutter pub get | |
| - name: Keep Apple dependency resolution on CocoaPods | |
| run: flutter config --no-enable-swift-package-manager | |
| - name: Build macOS release | |
| id: release_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build macos --release | |
| - name: Snapshot macOS production application | |
| id: production_e2e_artifact | |
| if: always() && steps.release_build.outcome == 'success' | |
| continue-on-error: true | |
| run: | | |
| destination='lddc/build/production_e2e/macos' | |
| rm -rf "$destination" | |
| mkdir -p "$destination" | |
| cp -R 'lddc/build/macos/Build/Products/Release/LDDC.app' "$destination/LDDC.app" | |
| test -x "$destination/LDDC.app/Contents/MacOS/LDDC" | |
| - name: Build macOS debug application for native automation | |
| id: debug_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build macos --debug | |
| - name: Run offline application flows | |
| id: offline | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_real_integration.ps1 -Profile offline -Device macos -Platform macos | |
| - name: Run native channel flows | |
| id: native_channel | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_real_integration.ps1 -Profile platform -Device macos -Platform macos | |
| - name: Run macOS file panel automation | |
| id: system_ui | |
| if: always() && steps.debug_build.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_macos_platform_tests.ps1 | |
| - name: Run real desktop process and service tests | |
| id: desktop_process | |
| continue-on-error: true | |
| shell: pwsh | |
| run: >- | |
| ./tool/test/run_desktop_process_e2e.ps1 -Platform macos | |
| -AppExe lddc/build/production_e2e/macos/LDDC.app/Contents/MacOS/LDDC | |
| - name: Run production media resource checks | |
| id: media_resource | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_platform_media_resource.ps1 -Platform macos -LoopCount 3 | |
| - name: Summarize required macOS phases | |
| id: required_outcomes | |
| if: always() | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| python tool/test/ci_phase_summary.py ` | |
| --title 'macOS validation' ` | |
| --phase 'release_build|Release build|${{ steps.release_build.outcome }}' ` | |
| --phase 'production_e2e_artifact|Production snapshot|${{ steps.production_e2e_artifact.outcome }}' ` | |
| --phase 'debug_build|Debug build|${{ steps.debug_build.outcome }}' ` | |
| --phase 'offline|Offline application|${{ steps.offline.outcome }}' ` | |
| --phase 'native_channel|Native channels|${{ steps.native_channel.outcome }}' ` | |
| --phase 'system_ui|NSOpenPanel hybrid|${{ steps.system_ui.outcome }}' ` | |
| --phase 'desktop_process|Desktop process|${{ steps.desktop_process.outcome }}' ` | |
| --phase 'media_resource|Media resources|${{ steps.media_resource.outcome }}' | |
| - name: Upload macOS diagnostics | |
| if: always() && steps.required_outcomes.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: macos-validation-diagnostics | |
| path: lddc/build/integration_reports/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Enforce required macOS result | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ('${{ steps.required_outcomes.outcome }}' -ne 'success') { | |
| throw 'macOS required validation failed; see the phase summary and diagnostics artifact' | |
| } | |
| linux-validation: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - name: Install Linux build and automation dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev \ | |
| liblzma-dev libstdc++-12-dev libayatana-appindicator3-dev dbus-x11 \ | |
| xvfb xauth ffmpeg | |
| - run: flutter pub get | |
| - name: Build Linux release | |
| id: release_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build linux --release | |
| - name: Build Linux debug application for native automation | |
| id: debug_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build linux --debug | |
| - name: Run offline application flows | |
| id: offline | |
| continue-on-error: true | |
| run: >- | |
| xvfb-run -a --server-args="-screen 0 1600x1000x24" | |
| pwsh -NoProfile -File tool/test/run_real_integration.ps1 | |
| -Profile offline -Device linux -Platform linux | |
| - name: Run native channel flows | |
| id: native_channel | |
| continue-on-error: true | |
| run: >- | |
| xvfb-run -a --server-args="-screen 0 1600x1000x24" | |
| pwsh -NoProfile -File tool/test/run_real_integration.ps1 | |
| -Profile platform -Device linux -Platform linux | |
| # Flutter Linux 当前不会把 Semantics identifier 暴露到 AT-SPI 子树。 | |
| # Dogtail 脚本继续保留供人工环境验证;hosted CI 不使用坐标回退, | |
| # 也不把无法发现 Flutter 控件的场景伪装成自动化覆盖。 | |
| # desktop_multi_window 0.3.0 与上游未发布 0.3.1 的 Linux teardown | |
| # 仍会错误移除 implicit FlView;上游 #448/#488 尚未修复。当前不维护 | |
| # 第三方 fork,也不能用忽略退出码或保留 child engine 伪造资源回收证据。 | |
| - name: Record Linux multi-window known issue | |
| if: always() | |
| run: | | |
| { | |
| echo '### Linux multi-window coverage' | |
| echo '- Release/Debug, offline flows, native channels and media resources remain required' | |
| echo '- Real desktop_multi_window child teardown: known upstream issue MixinNetwork/flutter-plugins#448/#488' | |
| echo '- No fork is maintained; multi-window runtime remains a manual known issue and is not reported as passed' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Run production media resource checks | |
| id: media_resource | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_platform_media_resource.ps1 -Platform linux -LoopCount 3 | |
| - name: Summarize required Linux phases | |
| id: required_outcomes | |
| if: always() | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| python tool/test/ci_phase_summary.py ` | |
| --title 'Linux validation' ` | |
| --phase 'release_build|Release build|${{ steps.release_build.outcome }}' ` | |
| --phase 'debug_build|Debug build|${{ steps.debug_build.outcome }}' ` | |
| --phase 'offline|Offline application|${{ steps.offline.outcome }}' ` | |
| --phase 'native_channel|Native channels|${{ steps.native_channel.outcome }}' ` | |
| --phase 'media_resource|Media resources|${{ steps.media_resource.outcome }}' | |
| - name: Upload Linux diagnostics | |
| if: always() && steps.required_outcomes.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-validation-diagnostics | |
| path: lddc/build/integration_reports/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Enforce required Linux result | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ('${{ steps.required_outcomes.outcome }}' -ne 'success') { | |
| throw 'Linux required validation failed; see the phase summary and diagnostics artifact' | |
| } | |
| android-validation: | |
| runs-on: ubuntu-latest | |
| # 四个验证阶段的独立硬超时合计 125 分钟,前面还包含 Release 构建、 | |
| # 模拟器启动和失败报告回收。job 总上限必须覆盖这些固定开销,否则 | |
| # GitHub 会在 always() 诊断步骤前直接终止,反而丢失真正的失败证据。 | |
| timeout-minutes: 180 | |
| env: | |
| LDDC_ALLOW_UNSIGNED_CI_RELEASE: '1' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - uses: gradle/actions/setup-gradle@v6 | |
| - run: flutter pub get | |
| - name: Build unsigned Android release outputs | |
| id: release_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: | | |
| flutter build apk --release | |
| flutter build appbundle --release | |
| - name: Enable KVM access for the Android emulator | |
| run: | | |
| test -e /dev/kvm | |
| sudo chmod 0666 /dev/kvm | |
| - name: Run Android application, channel, and DocumentsUI validation | |
| id: platform_validation | |
| continue-on-error: true | |
| uses: ReactiveCircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 35 | |
| arch: x86_64 | |
| profile: pixel_6 | |
| disable-animations: true | |
| script: bash tool/test/run_android_ci_validation.sh | |
| - name: Summarize required Android phases | |
| id: required_outcomes | |
| if: always() | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| python tool/test/ci_phase_summary.py ` | |
| --title 'Android validation' ` | |
| --phase 'release_build|Release APK and AAB|${{ steps.release_build.outcome }}' ` | |
| --phase 'platform_validation|Application and DocumentsUI|${{ steps.platform_validation.outcome }}' | |
| - name: Upload Android diagnostics | |
| if: always() && steps.required_outcomes.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: android-validation-diagnostics | |
| path: | | |
| lddc/build/integration_reports/** | |
| lddc/build/app/outputs/androidTest-results/**/*.xml | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Enforce required Android result | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ('${{ steps.required_outcomes.outcome }}' -ne 'success') { | |
| throw 'Android required validation failed; see the phase summary and diagnostics artifact' | |
| } | |
| ios-validation: | |
| runs-on: macos-latest | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - run: flutter pub get | |
| - name: Keep Apple dependency resolution on CocoaPods | |
| run: flutter config --no-enable-swift-package-manager | |
| - name: Create and boot a compatible iOS simulator | |
| id: simulator | |
| continue-on-error: true | |
| run: bash tool/test/create_ios_simulator.sh | |
| - name: Build iOS release without codesigning | |
| id: release_build | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: flutter build ios --release --no-codesign | |
| - name: Run offline application flows | |
| id: offline | |
| if: always() && steps.simulator.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| # iOS hosted runner 首个 integration target 需要完成原生编译、安装和 VM | |
| # 调试连接;此前 300 秒只终止在隐藏 loading 用例,业务场景尚未开始。 | |
| # 这里只扩大 testStart 前的基础设施窗口,场景步骤仍使用原有严格超时。 | |
| run: ./tool/test/run_real_integration.ps1 -Profile offline -Device $env:DEVICE_ID -Platform ios -StartupTimeoutSeconds 600 | |
| - name: Run native channel flows | |
| id: native_channel | |
| if: always() && steps.simulator.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_real_integration.ps1 -Profile platform -Device $env:DEVICE_ID -Platform ios | |
| - name: Restore iOS debug application after Flutter integration | |
| id: debug_rebuild | |
| if: always() && steps.simulator.outcome == 'success' | |
| continue-on-error: true | |
| working-directory: lddc | |
| # flutter test 会把 Generated.xcconfig 暂时指向 test listener;测试结束后 | |
| # 临时 listener 会被删除。原生测试前重建默认 main.dart,避免 Xcode | |
| # 继续引用已不存在的临时入口。 | |
| run: flutter build ios --debug --simulator | |
| - name: Run iOS native unit tests | |
| id: native_tests | |
| if: always() && steps.debug_rebuild.outcome == 'success' | |
| continue-on-error: true | |
| working-directory: lddc | |
| run: | | |
| # Flutter integration 结束后 Simulator 可能已关机;已启动时 boot 的 | |
| # 非零结果可以忽略,但 bootstatus 和 XCTest 都必须成功。 | |
| xcrun simctl boot "$DEVICE_ID" || true | |
| xcrun simctl bootstatus "$DEVICE_ID" -b | |
| xcodebuild test \ | |
| -workspace ios/Runner.xcworkspace \ | |
| -scheme Runner \ | |
| -destination "platform=iOS Simulator,id=$DEVICE_ID" \ | |
| -parallel-testing-enabled NO \ | |
| CODE_SIGNING_ALLOWED=NO | |
| - name: Run iOS Document Picker automation | |
| id: system_ui | |
| if: always() && steps.debug_rebuild.outcome == 'success' | |
| continue-on-error: true | |
| shell: pwsh | |
| run: ./tool/test/run_ios_platform_tests.ps1 -Device $env:DEVICE_ID | |
| - name: Delete the CI iOS simulator | |
| id: simulator_cleanup | |
| if: always() && env.DEVICE_ID != '' | |
| continue-on-error: true | |
| run: | | |
| # 已关机设备会让 shutdown 返回非零,但不代表泄漏;delete 必须成功。 | |
| xcrun simctl shutdown "$DEVICE_ID" || true | |
| xcrun simctl delete "$DEVICE_ID" | |
| - name: Summarize required iOS phases | |
| id: required_outcomes | |
| if: always() | |
| continue-on-error: true | |
| shell: pwsh | |
| run: | | |
| python tool/test/ci_phase_summary.py ` | |
| --title 'iOS validation' ` | |
| --phase 'simulator|Simulator lifecycle|${{ steps.simulator.outcome }}' ` | |
| --phase 'release_build|Release build|${{ steps.release_build.outcome }}' ` | |
| --phase 'offline|Offline application|${{ steps.offline.outcome }}' ` | |
| --phase 'native_channel|Native channels|${{ steps.native_channel.outcome }}' ` | |
| --phase 'debug_rebuild|Debug application|${{ steps.debug_rebuild.outcome }}' ` | |
| --phase 'native_tests|Native XCTest|${{ steps.native_tests.outcome }}' ` | |
| --phase 'system_ui|Document Picker XCUITest|${{ steps.system_ui.outcome }}' ` | |
| --phase 'simulator_cleanup|Simulator cleanup|${{ steps.simulator_cleanup.outcome }}' | |
| - name: Upload iOS diagnostics | |
| if: always() && steps.required_outcomes.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ios-validation-diagnostics | |
| path: lddc/build/integration_reports/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Enforce required iOS result | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ('${{ steps.required_outcomes.outcome }}' -ne 'success') { | |
| throw 'iOS required validation failed; see the phase summary and diagnostics artifact' | |
| } | |
| experimental-web: | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: beta | |
| cache: true | |
| - run: flutter pub get | |
| - name: Build experimental Web client | |
| working-directory: lddc | |
| run: flutter build web --release | |
| - name: Run browser capability observation | |
| working-directory: lddc | |
| run: | | |
| mkdir -p build/web-observation | |
| flutter test --platform chrome test/observation/web_capability_smoke_test.dart \ | |
| --file-reporter=json:build/web-observation/tests.jsonl | |
| python ../tool/test/json_report_to_junit.py \ | |
| --input build/web-observation/tests.jsonl \ | |
| --output build/web-observation/tests.xml | |
| - name: Upload Web observation report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: experimental-web-observation | |
| path: lddc/build/web-observation/** | |
| if-no-files-found: warn | |
| retention-days: 7 |