ci: cut GitHub Release on tag runs (#225) #346
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: ['**'] | |
| tags: ['*'] | |
| pull_request: | |
| workflow_dispatch: | |
| # Auto-cancel in-flight runs when a new push lands on the same ref. | |
| # Tag builds (refs/tags/*) get a unique-per-run group so release runs | |
| # never cancel each other. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/') && github.run_id || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| ROOT: "${{ github.workspace }}" | |
| SCRIPTS: "${{ github.workspace }}/.github/scripts" | |
| SERIOUS_PYTHON_SITE_PACKAGES: "${{ github.workspace }}/site-packages" | |
| SERIOUS_PYTHON_APP: "${{ github.workspace }}/python-app" | |
| UV_PYTHON: "3.12" | |
| # futureware-tech/simulator-action upstream still declares node20 in | |
| # action.yml (no Node 24 release as of v5). Force the runner to execute | |
| # node20-declared JS actions on Node 24 so we don't trip the deprecation | |
| # warning that becomes a hard error on 2026-09-16. | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| # Pin flet so the Python side matches the Dart-side flet version. With | |
| # `--only-binary :all:`, pip would otherwise silently downgrade to an | |
| # older flet whose transitive deps (e.g. msgpack) have wheels for the | |
| # target python_version / platform, leaving the two halves mismatched. | |
| FLET_VERSION: "0.85.3" | |
| permissions: | |
| contents: read | |
| jobs: | |
| version_tables_in_sync: | |
| name: Version tables in sync with manifest | |
| # Tags only publish; tests run on branches/PRs before the tag is cut. | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Regenerate version tables and check for drift | |
| working-directory: src/serious_python | |
| run: | | |
| set -euo pipefail | |
| flutter pub get >/dev/null | |
| dart run serious_python:gen_version_tables | |
| if ! git diff --quiet; then | |
| echo "::error::Generated Python version tables are out of date." | |
| echo "The committed lib/src/python_versions.dart and the four" | |
| echo "python_versions.properties must match python-build's" | |
| echo "manifest.json for the pinned pythonReleaseDate. To fix: edit" | |
| echo "python-build's manifest.json + cut a release, bump" | |
| echo "pythonReleaseDate, then run" | |
| echo " (cd src/serious_python && dart run serious_python:gen_version_tables)" | |
| echo "and commit the regenerated files." | |
| git --no-pager diff | |
| exit 1 | |
| fi | |
| bridge_example_macos: | |
| name: Test Bridge example on macOS (${{ matrix.build_system }}, Python ${{ matrix.python_version }}) | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: macos-26 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python_version: ['3.12', '3.13', '3.14'] | |
| build_system: ['cocoapods', 'spm'] | |
| env: | |
| SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Cache flet downloads | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.flet/cache | |
| key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} | |
| restore-keys: | | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}- | |
| # Avoid release-time cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Configure ${{ matrix.build_system }} | |
| run: | | |
| if [ "${{ matrix.build_system }}" = "spm" ]; then | |
| flutter config --enable-swift-package-manager | |
| else | |
| flutter config --no-enable-swift-package-manager | |
| fi | |
| - name: Package + run integration test | |
| working-directory: "src/serious_python/example/bridge_example" | |
| run: | | |
| # SPM is the package command's default: it does host-side staging and | |
| # writes the SP_NATIVE_SET cache-bust key (we export it into the build). | |
| # CocoaPods opts out via SERIOUS_PYTHON_DARWIN_SPM=false; the podspec | |
| # prepare_command stages then. (The SPM job leaves the var unset so it | |
| # exercises the default path docs/readme commands use.) | |
| if [ "${{ matrix.build_system }}" = "cocoapods" ]; then | |
| export SERIOUS_PYTHON_DARWIN_SPM=false | |
| fi | |
| dart run serious_python:main package app/src --platform Darwin --python-version ${{ matrix.python_version }} | |
| if [ "${{ matrix.build_system }}" = "spm" ]; then | |
| export SP_NATIVE_SET="$(cat build/.serious_python_spm_key)" | |
| fi | |
| # Each test file is invoked separately. `flutter test integration_test` | |
| # over the directory reuses one VM session, but the second file's | |
| # `runApp()` then trips "Unable to start the app on the device" — | |
| # the spawn-then-teardown of bridge_example's embedded Python + | |
| # PythonBridge ports isn't clean enough for VM reuse. One process | |
| # per file sidesteps the issue. | |
| flutter test integration_test/interactivity_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| flutter test integration_test/throughput_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| flutter test integration_test/memory_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| bridge_example_ios: | |
| name: Test Bridge example on iOS (${{ matrix.build_system }}, Python ${{ matrix.python_version }}) | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: macos-26 | |
| timeout-minutes: 25 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python_version: ['3.12', '3.13', '3.14'] | |
| build_system: ['cocoapods', 'spm'] | |
| env: | |
| SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Cache flet downloads | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.flet/cache | |
| key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} | |
| restore-keys: | | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}- | |
| # Avoid release-time cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Setup iOS Simulator | |
| id: simulator | |
| uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 # v5 | |
| with: | |
| model: 'iPhone 17 Pro Max' | |
| os: "iOS" | |
| os_version: "26.5" | |
| shutdown_after_job: true | |
| wait_for_boot: true | |
| - name: Configure ${{ matrix.build_system }} | |
| run: | | |
| if [ "${{ matrix.build_system }}" = "spm" ]; then | |
| flutter config --enable-swift-package-manager | |
| else | |
| flutter config --no-enable-swift-package-manager | |
| fi | |
| - name: Package + run integration test | |
| working-directory: "src/serious_python/example/bridge_example" | |
| env: | |
| # Pass the simulator UDID via env rather than interpolating the | |
| # step output straight into the run script (template injection). | |
| SIMULATOR_UDID: ${{ steps.simulator.outputs.udid }} | |
| run: | | |
| ts() { date '+%H:%M:%S'; } | |
| # SPM is the default; CocoaPods opts out (the SPM job leaves the var | |
| # unset to exercise the default path docs/readme commands use). | |
| if [ "${{ matrix.build_system }}" = "cocoapods" ]; then | |
| export SERIOUS_PYTHON_DARWIN_SPM=false | |
| fi | |
| echo "[$(ts)] >>> dart run serious_python:main package" | |
| # certifi is a placeholder requirement: serious_python_darwin's | |
| # sync_site_packages.sh only populates dist_ios/site-xcframeworks | |
| # (the iOS native C-extensions — embedded by the podspec script_phase | |
| # under CocoaPods, or enumerated into extra-xcframeworks under SPM) | |
| # when iOS-specific site-packages subdirs exist. Empty --requirements | |
| # skips that branch and the build fails. | |
| dart run serious_python:main package app/src --platform iOS --python-version ${{ matrix.python_version }} --requirements certifi | |
| if [ "${{ matrix.build_system }}" = "spm" ]; then | |
| export SP_NATIVE_SET="$(cat build/.serious_python_spm_key)" | |
| fi | |
| echo "[$(ts)] >>> flutter test integration_test (per-file)" | |
| # See macOS job for why each file runs as a separate invocation. | |
| flutter test integration_test/interactivity_test.dart --device-id "$SIMULATOR_UDID" --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| flutter test integration_test/throughput_test.dart --device-id "$SIMULATOR_UDID" --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| flutter test integration_test/memory_test.dart --device-id "$SIMULATOR_UDID" --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| echo "[$(ts)] >>> done" | |
| bridge_example_android: | |
| name: Test Bridge example on Android (Python ${{ matrix.python_version }}) | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # x86_64 matches the emulator architecture; only build/install for | |
| # that ABI to keep CI fast. | |
| python_version: ['3.12', '3.13', '3.14'] | |
| env: | |
| SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Cache flet downloads | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.flet/cache | |
| key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} | |
| restore-keys: | | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}- | |
| # Avoid release-time cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| - name: Gradle cache | |
| # Stay on v5 — v6 moved the caching component to a separate | |
| # `gradle-actions-caching` library under proprietary commercial | |
| # Terms of Use (https://blog.gradle.org/github-actions-for-gradle-v6). | |
| # v5 was the last fully MIT-licensed major and is on Node 24. | |
| uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 | |
| with: | |
| # Avoid release-time cache poisoning. | |
| cache-disabled: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: AVD cache | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| id: avd-cache | |
| with: | |
| path: | | |
| ~/.android/avd/* | |
| ~/.android/adb* | |
| key: avd-bridge | |
| # Avoid release-time cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Setup Android Emulator + Run tests | |
| uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 | |
| env: | |
| EMULATOR_PORT: 5554 | |
| with: | |
| avd-name: android_emulator | |
| api-level: 33 | |
| target: google_atd | |
| arch: x86_64 | |
| profile: pixel_5 | |
| sdcard-path-or-size: 128M | |
| ram-size: 2048M | |
| disk-size: 4096M | |
| emulator-port: ${{ env.EMULATOR_PORT }} | |
| disable-animations: true | |
| emulator-options: -no-window -noaudio -no-boot-anim -wipe-data -cache-size 1000 -partition-size 8192 | |
| pre-emulator-launch-script: | | |
| sdkmanager --list_installed | |
| script: | | |
| cd src/serious_python/example/bridge_example && dart run serious_python:main package app/src --platform Android --python-version ${{ matrix.python_version }} | |
| # Per-file: `flutter test integration_test` (whole dir) reuses one | |
| # VM session and the second file fails to start the app. See | |
| # bridge_example_macos for the discovery. | |
| cd src/serious_python/example/bridge_example && flutter test integration_test/interactivity_test.dart --device-id emulator-${{ env.EMULATOR_PORT }} --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| cd src/serious_python/example/bridge_example && flutter test integration_test/throughput_test.dart --device-id emulator-${{ env.EMULATOR_PORT }} --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| cd src/serious_python/example/bridge_example && flutter test integration_test/memory_test.dart --device-id emulator-${{ env.EMULATOR_PORT }} --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} | |
| - name: Diagnostics on failure | |
| if: failure() | |
| shell: bash | |
| run: | | |
| set +e | |
| REPO="$GITHUB_WORKSPACE" | |
| ABI=x86_64 | |
| echo "=== serious_python_android/android/src/main/jniLibs/$ABI/ ===" | |
| ls -la "$REPO/src/serious_python_android/android/src/main/jniLibs/$ABI/" 2>/dev/null || echo "(not found)" | |
| echo | |
| echo "=== example/build/app/outputs/apk/debug/*.apk ===" | |
| APK=$(find "$REPO/src/serious_python/example/bridge_example/build" -name "*-debug.apk" 2>/dev/null | head -n1) | |
| echo "APK: $APK" | |
| if [ -n "$APK" ]; then | |
| echo "Native libs inside APK:" | |
| unzip -l "$APK" | grep -E "lib/$ABI/" || echo "(no lib/$ABI/ entries)" | |
| fi | |
| bridge_example_windows: | |
| name: Test Bridge example on Windows (Python ${{ matrix.python_version }}) | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| # Explicit pin instead of `windows-latest` to avoid silent image moves. | |
| # `windows-2025-vs2026` is what GitHub redirects `windows-latest` to | |
| # starting 2026-06-15 (Server 2025 + VS 2026). | |
| runs-on: windows-2025-vs2026 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python_version: ['3.12', '3.13', '3.14'] | |
| env: | |
| SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Cache flet downloads | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.flet/cache | |
| key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} | |
| restore-keys: | | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}- | |
| # Avoid release-time cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Package + run integration test | |
| working-directory: "src/serious_python/example/bridge_example" | |
| shell: bash | |
| run: | | |
| dart run serious_python:main package app/src --platform Windows --python-version ${{ matrix.python_version }} | |
| # bash + pipefail so the test step actually fails when flutter fails | |
| # (default PowerShell shell on windows runners eats the failure when | |
| # piping into tail). | |
| set -o pipefail | |
| # Per-file: `flutter test integration_test` (whole dir) reuses one | |
| # VM session and the second file fails to start the app. See | |
| # bridge_example_macos for the discovery. | |
| flutter test integration_test/interactivity_test.dart -d windows --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| flutter test integration_test/throughput_test.dart -d windows --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| flutter test integration_test/memory_test.dart -d windows --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| - name: Diagnostics on failure | |
| if: failure() | |
| shell: bash | |
| working-directory: "src/serious_python/example/bridge_example" | |
| run: | | |
| set +e | |
| DBG_DIR=build/windows/x64/runner/Debug | |
| echo "=== runner/Debug dir ===" | |
| ls -la $DBG_DIR/ || true | |
| echo | |
| echo "=== dart_bridge*.dll bundled? ===" | |
| ls -la $DBG_DIR/dart_bridge*.dll || echo "(no dart_bridge dll bundled)" | |
| echo | |
| echo "=== runner/Debug/Lib (Python stdlib) ===" | |
| ls $DBG_DIR/Lib | head -20 || true | |
| echo | |
| echo "=== runner/Debug/site-packages ===" | |
| ls -la $DBG_DIR/site-packages/ || true | |
| bridge_example_linux: | |
| name: Test Bridge example on Linux ${{ matrix.title }} (Python ${{ matrix.python_version }}) | |
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python_version: ['3.12', '3.13', '3.14'] | |
| arch: [amd64, arm64] | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-24.04 | |
| title: AMD64 | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| title: ARM64 | |
| env: | |
| SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Get Flutter version from .fvmrc | |
| uses: kuhnroyal/flutter-fvm-config-action/config@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| id: fvm-config-action | |
| with: | |
| path: '.fvmrc' | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 | |
| with: | |
| flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }} | |
| channel: ${{ matrix.arch == 'arm64' && 'master' || 'stable' }} | |
| cache: true | |
| - name: Cache flet downloads | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.flet/cache | |
| key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} | |
| restore-keys: | | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- | |
| flet-cache-${{ runner.os }}-${{ runner.arch }}- | |
| # Read-only on tag/release builds to avoid cache poisoning. | |
| lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }} | |
| - name: Install Linux desktop build deps | |
| run: | | |
| sudo apt-get update --allow-releaseinfo-change | |
| sudo apt-get install -y xvfb libgtk-3-dev | |
| if [ "${{ matrix.arch }}" = "amd64" ]; then | |
| sudo apt-get install -y \ | |
| libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ | |
| libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base \ | |
| gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ | |
| gstreamer1.0-plugins-ugly gstreamer1.0-libav | |
| else | |
| sudo apt-get install -y \ | |
| clang ninja-build gstreamer1.0-plugins-bad \ | |
| gstreamer1.0-plugins-ugly gstreamer1.0-libav | |
| fi | |
| - name: Package + run integration test | |
| working-directory: src/serious_python/example/bridge_example | |
| run: | | |
| flutter pub get | |
| dart run serious_python:main package app/src \ | |
| --platform Linux \ | |
| --python-version ${{ matrix.python_version }} | |
| set -o pipefail | |
| # Three back-to-back `xvfb-run` invocations race for display | |
| # numbers — the second/third sometimes trips "Xvfb failed to | |
| # start" because the previous Xvfb's display isn't released yet | |
| # (3-of-6 Linux jobs in run 27441055242 hit this). Start one | |
| # Xvfb up front, share DISPLAY across all three test runs. | |
| Xvfb :99 -screen 0 1280x1024x24 >/tmp/xvfb.log 2>&1 & | |
| XVFB_PID=$! | |
| export DISPLAY=:99 | |
| trap 'kill $XVFB_PID 2>/dev/null || true' EXIT | |
| # Brief settle so Xvfb is accepting connections before flutter | |
| # test tries to open the window. xdpyinfo isn't in the ubuntu | |
| # runner's default toolchain; a fixed sleep is sufficient. | |
| sleep 1 | |
| # Per-file: `flutter test integration_test` (whole dir) reuses one | |
| # VM session and the second file fails to start the app. See | |
| # bridge_example_macos for the discovery. | |
| flutter test integration_test/interactivity_test.dart -d linux --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| flutter test integration_test/throughput_test.dart -d linux --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| flutter test integration_test/memory_test.dart -d linux --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} -v 2>&1 | tail -300 | |
| - name: Diagnostics on failure | |
| if: failure() | |
| working-directory: src/serious_python/example/bridge_example | |
| run: | | |
| set +e | |
| DBG_DIR=build/linux/${{ matrix.arch == 'arm64' && 'arm64' || 'x64' }}/debug/bundle | |
| echo "=== bundle dir ===" | |
| ls -la $DBG_DIR/ 2>/dev/null | |
| echo | |
| echo "=== bundle/lib (bundled shared libs) ===" | |
| ls -la $DBG_DIR/lib/ 2>/dev/null | |
| echo | |
| echo "=== readelf -d on libdart_bridge.so ===" | |
| readelf -d $DBG_DIR/lib/libdart_bridge.so 2>/dev/null | grep -E "NEEDED|libpython" || echo "(libdart_bridge.so not found)" | |
| echo | |
| echo "=== bundle data/flutter_assets/native_assets/linux/ ===" | |
| ls -la $DBG_DIR/data/flutter_assets/native_assets/linux/ 2>/dev/null | |
| echo | |
| echo "=== Python stdlib dir ===" | |
| ls -d $DBG_DIR/python3.* 2>/dev/null | head -5 | |
| publish: | |
| name: Publish to pub.dev | |
| needs: | |
| - bridge_example_macos | |
| - bridge_example_ios | |
| - bridge_example_android | |
| - bridge_example_linux | |
| - bridge_example_windows | |
| runs-on: ubuntu-22.04 | |
| # The bridge_example_* deps are skipped on tag builds (tags only publish); | |
| # `!cancelled()` lets publish run despite those skipped needs. | |
| if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| with: | |
| # Avoid release-time cache poisoning. | |
| enable-cache: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| - name: Setup Flutter | |
| uses: kuhnroyal/flutter-fvm-config-action/setup@c378498f1d1962d33039c3989411093ef8a17b2c # v3.3 | |
| with: | |
| path: '.fvmrc' | |
| cache: true | |
| - name: Compute PKG_VER | |
| run: | | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| # Extract the tag name | |
| tag="${GITHUB_REF#refs/tags/}" | |
| # Remove leading "v" if present | |
| PKG_VER="${tag#v}" | |
| else | |
| # Get the latest tag, or fall back to "v0.0.0" if none exist | |
| cv=$(git describe --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| # Remove leading "v" if present | |
| cv=${cv#v} | |
| # Split into major/minor components | |
| major=$(echo "$cv" | cut -d. -f1) | |
| minor=$(echo "$cv" | cut -d. -f2) | |
| # Construct the package version | |
| PKG_VER="${major}.${minor}.${GITHUB_RUN_NUMBER}" | |
| fi | |
| export PKG_VER | |
| echo "PKG_VER=$PKG_VER" | tee -a "$GITHUB_ENV" | |
| - name: Configure pub.dev credentials | |
| run: | | |
| mkdir -p $HOME/.config/dart | |
| echo "${{ secrets.PUB_DEV_TOKEN }}" | base64 --decode > $HOME/.config/dart/pub-credentials.json | |
| - name: Patch pubspec versions | |
| working-directory: "src" | |
| run: | | |
| for pkg in \ | |
| "serious_python" \ | |
| "serious_python_platform_interface" \ | |
| "serious_python_android" \ | |
| "serious_python_darwin" \ | |
| "serious_python_windows" \ | |
| "serious_python_linux"; do | |
| uv run "$SCRIPTS/patch_pubspec.py" "$pkg/pubspec.yaml" "$PKG_VER" | |
| done | |
| - name: Publish packages | |
| run: | | |
| publish_pkg () { | |
| pushd "$1" >/dev/null | |
| dart pub publish --force | |
| popd >/dev/null | |
| } | |
| publish_pkg src/serious_python_platform_interface | |
| sleep 600 | |
| publish_pkg src/serious_python_android | |
| publish_pkg src/serious_python_darwin | |
| publish_pkg src/serious_python_windows | |
| publish_pkg src/serious_python_linux | |
| sleep 600 | |
| publish_pkg src/serious_python | |
| release: | |
| name: Publish GitHub Release | |
| needs: | |
| - publish | |
| runs-on: ubuntu-latest | |
| if: ${{ startsWith(github.ref, 'refs/tags/v') }} | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Detect pre-release | |
| id: prerelease | |
| # Dart/pub pre-releases carry a semver hyphen suffix (e.g. v1.2.0-beta.1); | |
| # mark those as GitHub pre-releases. | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| if [[ "$REF_NAME" == *-* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | |
| with: | |
| name: ${{ github.ref_name }} | |
| tag_name: ${{ github.ref_name }} | |
| prerelease: ${{ steps.prerelease.outputs.is_prerelease == 'true' }} | |
| generate_release_notes: true |