Skip to content

fix: harden hosted platform orchestration #11

fix: harden hosted platform orchestration

fix: harden hosted platform orchestration #11

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