name: CI - Sanity Checks on: push: branches: - main # Keep untrusted code on the read-only pull_request event. Never use pull_request_target here. pull_request: branches: - main workflow_dispatch: inputs: build_linux_packages: description: > Smoke-build the Linux deb/rpm/pacman packages. Off by default: it needs a release build plus fpm, and build.yml only packages from main, so this is the only way to exercise linux/packaging from a branch. default: false type: boolean env: # Only place this workflow names the SDK; .github/actions/setup-flutter-git pins the same release. FLUTTER_VERSION: "3.44.0" jobs: analyze: name: Code Analysis runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Cache Pub dependencies uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.pub-cache key: ${{ runner.os }}-pub-v3-${{ hashFiles('**/pubspec.yaml', '**/pubspec.lock') }} - name: Install dependencies run: | flutter pub get - name: Install wakelock_plus development dependencies working-directory: packages/wakelock_plus run: flutter pub get --enforce-lockfile --no-example - name: Verify generated files committed run: scripts/codegen.sh --check - name: Verify translation hygiene run: python3 scripts/clean_translations.py --check --strict - name: Verify workflow and script guards run: bash scripts/ci_guard_checks.sh - name: Verify formatting run: | paths=(lib) [ ! -d test ] || paths+=(test) find "${paths[@]}" -name "*.dart" ! -name "*.g.dart" ! -name "*.freezed.dart" -type f -print0 | xargs -0 -r dart format --output=none --set-exit-if-changed - name: Verify icon consistency run: dart run scripts/check_icon_consistency.dart - name: Analyze code run: dart run scripts/check_analyzer.dart - name: Check for unused code run: | echo "🔍 Checking for unused code..." dart run dart_code_linter:metrics check-unused-code lib 2>&1 | tee unused_code.txt if grep -qi "no unused code found" unused_code.txt; then echo "✅ No unused code found" else echo "❌ Found unused code:" cat unused_code.txt exit 1 fi - name: Check for unused files run: | echo "🔍 Checking for unused files..." dart run dart_code_linter:metrics check-unused-files lib 2>&1 | tee unused_files.txt if grep -qi "no unused files found" unused_files.txt; then echo "✅ No unused files found" else echo "❌ Found unused files:" cat unused_files.txt exit 1 fi test: name: Unit Tests runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Cache Pub dependencies uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.pub-cache key: ${{ runner.os }}-pub-v3-${{ hashFiles('**/pubspec.yaml', '**/pubspec.lock') }} - name: Install dependencies run: | flutter clean flutter pub get - name: Run tests run: | if [ -d "test" ] && [ "$(find test -name '*_test.dart' | wc -l)" -gt 0 ]; then scripts/run_tests.sh else echo "No tests found, skipping test execution" fi - name: Install wakelock_plus test dependencies working-directory: packages/wakelock_plus run: flutter pub get --enforce-lockfile - name: Run wakelock_plus VM tests working-directory: packages/wakelock_plus run: flutter test test/wakelock_plus_linux_plugin_test.dart - name: Run wakelock_plus Chrome tests working-directory: packages/wakelock_plus run: flutter test --platform chrome --dart-define=WEB_PLUGIN_TESTS=true test/wakelock_plus_web_plugin_test.dart # The vendored atomic-write patch has to keep the upstream contract, not # just the new behaviour. Upstream's own suites are the check for that. - name: Run the vendored desktop preference store tests run: | for pkg in shared_preferences_linux shared_preferences_windows; do (cd "packages/$pkg" && flutter pub get --enforce-lockfile && flutter test) done android-test: name: Android JVM and Native Tests runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Java uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5 with: distribution: "temurin" java-version: "21" - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Cache Pub dependencies uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.pub-cache key: ${{ runner.os }}-pub-v3-${{ hashFiles('**/pubspec.yaml', '**/pubspec.lock') }} - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Install dependencies run: flutter pub get - name: Configure Android local properties run: printf 'flutter.sdk=%s\nsdk.dir=%s\n' "$FLUTTER_ROOT" "$ANDROID_HOME" > android/local.properties - name: Configure Android host native tests run: | cmake -S android/app/src/test/cpp -B build/android-host-tests \ -DCMAKE_BUILD_TYPE=Debug - name: Build Android host native tests run: cmake --build build/android-host-tests --parallel 2 - name: Run Android host native tests run: | ctest --test-dir build/android-host-tests \ --output-on-failure --no-tests=error - name: Run Android JVM unit tests working-directory: android run: ./gradlew :app:testDebugUnitTest :saf_util:testDebugUnitTest :libass:testDebugUnitTest -x :app:compileFlutterBuildDebug --continue - name: Check Android API compatibility working-directory: android run: ./gradlew :app:lintDebug native-format: name: Native Formatting runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Java uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5 with: distribution: "temurin" java-version: "17" - name: Verify native formatting run: scripts/format_native.sh --check - name: Verify Linux native acquisition and build plan run: bash linux/packaging/build-libmpv_test.sh linux-native-test: name: Linux native reliability (${{ matrix.sanitizer }}) runs-on: ubuntu-latest permissions: contents: read strategy: fail-fast: false matrix: include: - sanitizer: address lifecycle_sanitizers: ON - sanitizer: thread lifecycle_sanitizers: OFF steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Install Linux native test dependencies run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev \ libstdc++-12-dev libmpv-dev libepoxy-dev libcurl4-openssl-dev libevdev-dev \ libwayland-dev libegl-dev - name: Prepare Flutter Linux configuration run: | flutter pub get --enforce-lockfile --no-example flutter build linux --debug --config-only --no-pub - name: Configure Linux native reliability tests run: | cmake -S linux -B build/linux-native-${{ matrix.sanitizer }} -G Ninja \ -DCMAKE_BUILD_TYPE=Debug \ -DPLEZY_BUILD_MPV_PLAYER_LIFECYCLE_TESTS=ON \ -DPLEZY_MPV_LIFECYCLE_SANITIZERS=${{ matrix.lifecycle_sanitizers }} \ -DPLEZY_BUILD_MPV_RELIABILITY_TESTS=ON \ -DPLEZY_MPV_RELIABILITY_SANITIZER=${{ matrix.sanitizer }} # `plezy` is the runner itself. Without it nothing in CI ever compiles # my_application.cc, mpv_plugin.cc or the Wayland video plane — the # reliability test targets each pull in only a couple of translation units, # so a break in the rest of linux/runner reached a release build unseen. - name: Build Linux native reliability tests run: | cmake --build build/linux-native-${{ matrix.sanitizer }} --parallel 2 --target \ plezy \ mpv_player_lifecycle_test \ mpv_player_hdr_output_test \ mpv_property_result_contract_test \ hdr_metadata_test \ plane_geometry_test \ video_params_test - name: Run Linux native reliability tests run: | ctest --test-dir build/linux-native-${{ matrix.sanitizer }} \ --output-on-failure --no-tests=error apple-native-test: name: Apple native reliability (${{ matrix.platform }}) runs-on: macos-26 permissions: contents: read strategy: fail-fast: false matrix: include: - platform: iOS project_directory: ios workspace: ios/Runner.xcworkspace simulator_runtime: iOS simulator_platform: iOS static_destination: "" - platform: macOS project_directory: macos workspace: macos/Runner.xcworkspace simulator_runtime: "" simulator_platform: "" static_destination: platform=macOS - platform: tvOS project_directory: tvos workspace: tvos/Runner.xcworkspace simulator_runtime: tvOS simulator_platform: tvOS static_destination: "" steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Install locked Dart dependencies run: flutter pub get --enforce-lockfile --no-example - name: Record committed CocoaPods lockfile if: matrix.platform != 'tvOS' env: PODFILE_LOCK: ${{ matrix.project_directory }}/Podfile.lock run: | test -f "$PODFILE_LOCK" shasum -a 256 "$PODFILE_LOCK" > "$RUNNER_TEMP/plezy-podfile-lock.sha256" - name: Prepare iOS Flutter build settings if: matrix.platform == 'iOS' run: flutter build ios --config-only --simulator --debug --no-pub - name: Prepare macOS Flutter build settings if: matrix.platform == 'macOS' run: flutter build macos --config-only --debug --no-pub - name: Prepare tvOS Flutter engine if: matrix.platform == 'tvOS' run: tvos/scripts/fetch_engine.sh - name: Verify Flutter configuration preserved CocoaPods lockfile if: matrix.platform != 'tvOS' run: shasum -a 256 --check "$RUNNER_TEMP/plezy-podfile-lock.sha256" - name: Install locked CocoaPods dependencies if: matrix.platform != 'tvOS' working-directory: ${{ matrix.project_directory }} run: pod install --deployment - name: Install tvOS CocoaPods dependencies if: matrix.platform == 'tvOS' run: tvos/scripts/pod_install.sh - name: Verify tvOS project wiring if: matrix.platform == 'tvOS' run: ruby tvos/scripts/test_wire_mpv.rb - name: Select Apple test destination env: SIMULATOR_RUNTIME: ${{ matrix.simulator_runtime }} SIMULATOR_PLATFORM: ${{ matrix.simulator_platform }} STATIC_DESTINATION: ${{ matrix.static_destination }} run: | python3 - <<'PY' import json import os import subprocess destination = os.environ["STATIC_DESTINATION"] if not destination: runtime_name = os.environ["SIMULATOR_RUNTIME"] payload = json.loads( subprocess.check_output( ["xcrun", "simctl", "list", "devices", "available", "-j"], text=True, ) ) devices = [ device for runtime, candidates in payload["devices"].items() if f".{runtime_name}-" in runtime for device in candidates if device.get("isAvailable", False) ] if not devices: raise SystemExit(f"no available {runtime_name} simulator") destination = ( f"platform={os.environ['SIMULATOR_PLATFORM']} Simulator," f"id={devices[0]['udid']}" ) with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as output: output.write(f"APPLE_TEST_DESTINATION={destination}\n") PY - name: Run Apple native reliability tests run: | xcodebuild test \ -workspace "${{ matrix.workspace }}" \ -scheme Runner \ -configuration Debug \ -destination "$APPLE_TEST_DESTINATION" \ -disableAutomaticPackageResolution \ CODE_SIGNING_ALLOWED=NO \ COMPILER_INDEX_STORE_ENABLE=NO windows-native-test: name: Windows native reliability (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} permissions: contents: read strategy: fail-fast: false matrix: include: - arch: x64 runner: windows-latest flutter_setup: action - arch: arm64 runner: windows-11-arm flutter_setup: git steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Install 7-Zip if: matrix.arch == 'arm64' shell: pwsh run: choco install 7zip -y - name: Setup Flutter if: matrix.flutter_setup == 'action' uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Set up Flutter from its pinned commit if: matrix.flutter_setup == 'git' uses: ./.github/actions/setup-flutter-git - name: Install locked Dart dependencies shell: pwsh run: flutter pub get --enforce-lockfile --no-example # Windows replacement semantics (`MoveFileExW` with # MOVEFILE_REPLACE_EXISTING) cannot be proven on the POSIX runners, and a # memory file system proves nothing about either. The vendored # shared_preferences_windows store write is the fix for #1732, so exercise # it here, on a real NTFS volume, against the real backend. - name: Run the vendored Windows preference store tests shell: pwsh run: flutter test test/services/prefs_store_atomic_write_windows_test.dart - name: Install patched Flutter engine shell: pwsh run: | flutter precache --windows .\windows\tool\install-patched-engine.ps1 - name: Prepare Flutter Windows configuration shell: pwsh run: flutter build windows --debug --config-only --no-pub - name: Configure Windows native reliability tests shell: pwsh run: | $buildDir = "build/windows/${{ matrix.arch }}" cmake -S windows -B $buildDir ` -DPLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS=ON ` -DPLEZY_BUILD_DISPLAY_RECOVERY_TESTS=ON - name: Build Windows native reliability tests shell: pwsh run: | $buildDir = "build/windows/${{ matrix.arch }}" cmake --build $buildDir --config Debug --parallel 2 --target ` mpv_property_result_contract_test ` mpv_player_property_contract_test ` display_mode_manager_test - name: Run Windows native reliability tests shell: pwsh run: | $buildDir = "build/windows/${{ matrix.arch }}" ctest --test-dir "$buildDir/runner" -C Debug --output-on-failure --no-tests=error dependency-check: name: Dependency Validation runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false - name: Cache Pub dependencies uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.pub-cache key: ${{ runner.os }}-pub-v3-${{ hashFiles('**/pubspec.yaml', '**/pubspec.lock') }} - name: Verify dependencies run: | flutter clean flutter pub get flutter pub outdated server: name: Server checks runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum - name: Run server checks run: scripts/ci_server_checks.sh website: name: Website checks runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: "1.3.14" - name: Run website checks run: scripts/ci_website_checks.sh # The only job that checks packaging against a real artifact. build.yml also # packages Linux, but refuses any ref but refs/heads/main, and # check_linux_package_deps.py can only compare a hand-written list against # CMake - it cannot prove the list reaches the artifact, nor see the # transitive libraries the bundled libmpv drags in. This can, by reading the # built bundle back with ldd. # # Runs on every push to main and on request, but not on pull requests: it # needs a release build plus fpm, which is minutes of runner time that most # changes here have no reason to pay. That buys post-merge detection rather # than pre-merge, which is the deliberate trade. Dispatch it from a branch # with build_linux_packages when touching linux/packaging - which is the only # way to exercise it before merging. linux-packages: name: Linux package smoke build if: ${{ inputs.build_linux_packages || github.event_name == 'push' }} runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Setup Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true pub-cache: false # The packaging deps, minus libmpv: this job builds it from source below, # because the distro's is a different version with different windowing # backends, and a package smoke-built against it cannot show that # build-libmpv.sh still works or that the bundle it produces is coherent. - name: Install packaging dependencies run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ clang cmake meson ninja-build pkg-config nasm libgtk-3-dev liblzma-dev \ libstdc++-12-dev libepoxy-dev libcurl4-openssl-dev libevdev-dev \ libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev \ libharfbuzz-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev \ libva-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev \ libgbm-dev libwayland-dev wayland-protocols liblcms2-dev libmujs-dev \ liblua5.2-dev rpm libarchive-tools imagemagick ruby-dev build-essential sudo gem install fpm --version 1.17.0 --no-document # Keyed the same way build.yml keys it, so editing the script or its pinned # inputs is what invalidates the cache - and this branch's whole point is # that those edits get exercised somewhere. - name: Cache libmpv build id: libmpv-cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: libmpv-prefix key: ci-libmpv-${{ runner.arch }}-${{ hashFiles('linux/packaging/build-libmpv.sh', 'linux/packaging/native-inputs.json') }} - name: Build libmpv if: steps.libmpv-cache.outputs.cache-hit != 'true' run: bash linux/packaging/build-libmpv.sh # The windowing backends the runner depends on, read off the library the # script just produced. The plane hands mpv MPV_RENDER_PARAM_WL_DISPLAY, so # a libmpv without Wayland cannot find the VAAPI device and quietly decodes # in software; X11 and VDPAU are gone with the texture path. - name: Check the built libmpv's backends run: | LIB=$(find libmpv-prefix -name 'libmpv.so.2' | head -1) echo "== $LIB ==" ldd "$LIB" | grep -iE 'wayland|libX11|vdpau' || echo '(none)' ldd "$LIB" | grep -q libwayland-client || { echo "::error::the built libmpv does not link libwayland-client, so the video plane cannot work" exit 1 } - name: Build the release bundle run: | flutter pub get --enforce-lockfile --no-example flutter build linux --release env: PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/x86_64-linux-gnu/pkgconfig # Mirrors build.yml: resolve the bundle completely, then package from it. # libmpv travels with us, so nothing depends on a host one - which also # removes the transitive pull of everything libmpv links, hence bundle-libs # before packaging rather than after. - name: Copy libmpv into the bundle run: | BUNDLE_LIB=build/linux/x64/release/bundle/lib LIBMPV_DIR=$(dirname "$(find libmpv-prefix -name 'libmpv.so' | head -1)") cp -a "$LIBMPV_DIR"/libmpv.so* "$BUNDLE_LIB/" cp -a libmpv-prefix/lib/libshaderc_shared.so* "$BUNDLE_LIB/" - name: Bundle shared libraries run: bash linux/packaging/bundle-libs.sh build/linux/x64/release/bundle - name: Copy wrapper script into the bundle run: cp linux/packaging/plezy.sh build/linux/x64/release/bundle/plezy.sh # Derives what the resolved bundle still needs from the host and proves # every one of those libraries is declared. This is the check that would # have caught bundling libmpv without also declaring what libmpv links. - name: Verify every unbundled library the bundle needs is declared run: python3 linux/packaging/check-bundle-host-deps.py build/linux/x64/release/bundle # OUTPUT_DIR defaults to the repo root; name it so the paths below are not # a guess about where fpm dropped things. The host-dependency guard is # skipped because the named step above just ran it against this same # bundle; the internal run exists for by-hand packaging outside CI. - name: Build the packages run: | mkdir -p "$OUTPUT_DIR" python3 linux/packaging/build-packages.py env: OUTPUT_DIR: ${{ github.workspace }}/packages PLEZY_SKIP_HOST_DEP_CHECK: "1" # The guard above reconciles the depends lists with the staged bundle; only # the packages themselves can show that list survived fpm. Every name comes # from build-packages.py, so adding a library there is verified here without # a second edit - and this job is off by default, so a hand-copied list # would rot unseen. The release job in build.yml runs the same script # against the artifacts users install, so the assertions cannot drift. - name: Verify the declared dependencies reached the package metadata run: python3 linux/packaging/check-package-deps.py "${{ github.workspace }}/packages" # Same resolved bundle the packages were cut from, so the tarball is not a # second, differently-assembled artifact. It is the one to put on a USB for # a foreign machine, because it needs nothing installed. - name: Create the tarball run: | BUNDLE_DIR=build/linux/x64/release/bundle mkdir -p "$OUTPUT_DIR" tar -czf "$OUTPUT_DIR/plezy-linux-x64.tar.gz" -C "$BUNDLE_DIR" . echo "=== libmpv travelling in every artifact ===" ldd "$BUNDLE_DIR/lib/libmpv.so" | grep -iE 'wayland|libX11|vdpau' || echo '(none)' env: OUTPUT_DIR: ${{ github.workspace }}/packages - name: Upload the packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: linux-packages-smoke path: ${{ github.workspace }}/packages/plezy-linux-x64.* if-no-files-found: error retention-days: 7