Skip to content

[docs] Scope browser installer support accurately #47

[docs] Scope browser installer support accurately

[docs] Scope browser installer support accurately #47

Workflow file for this run

# SPDX-License-Identifier: MIT
# Part of PyBLE (https://pyble.dev) — see /LICENSE.
#
# PyBLE CI — G0/G1 gate suite + 3-chip build matrix. Runs on every push and PR.
# - no-leak : reject forbidden proprietary tokens in shippable code (CON-6).
# - spdx : every source file carries SPDX-License-Identifier: MIT (NFR-MAINT-4).
# - dco : every commit in the PR is Signed-off-by (CON-6).
# - import-boundary : no widget (lib/** outside lib/pble) imports lib/ble (NFR-MAINT-1, CON-8).
# - locale-parity : ARB key parity vs en + ASCII technical-identifier rule + no
# hard-coded display text in lib/app (BLD-5, CON-9, FR-I18N-1..4).
# - sha-drift : checked-out submodule SHA == versions.lock, else refuse (BLD-1/2).
# - app : Flutter app gate (X-03) — resolve against the TRACKED lockfile,
# static analysis, and unit/widget/conformance suites (BLD-2).
# - app-goldens : exact pixel baselines on pinned macOS, matching the
# renderer used to review and commit the reference PNGs.
# - web : pinned-Node install, formatting, lint, type, unit, and
# static-export build checks for tools/web.
# - docs : every repository-local Markdown target exists.
# - firmware-host : shell gates plus the complete Python host/release suite.
# - android-webview : real Blockly platform-WebView integration on a clean,
# pinned Android 14 / API 34 AOSP x86_64 AVD.
# - build : cross-compile the empty agent for esp32 / esp32-s3 / esp32-c3
# (BLD-3/4/5) — the REAL toolchain job (installs ESP-IDF + submodule).
name: ci
on:
push:
branches: ["**"]
pull_request:
permissions:
contents: read
jobs:
no-leak:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: No-leak gate (CON-6)
run: tools/ci/no_leak.sh
spdx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: SPDX-MIT header lint (NFR-MAINT-4)
run: tools/ci/spdx_lint.sh
web:
needs: [no-leak, spdx]
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NEXT_TELEMETRY_DISABLED: "1"
defaults:
run:
working-directory: tools/web
steps:
- uses: actions/checkout@v4
- name: Set up pinned Node.js
uses: actions/setup-node@v4
with:
node-version-file: tools/web/.node-version
cache: npm
cache-dependency-path: tools/web/package-lock.json
- name: Install locked website dependencies
run: npm ci
- name: Website checks and static export
run: npm run check
import-boundary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Import-boundary lint — only lib/pble may import lib/ble (NFR-MAINT-1, CON-8, FR-BLE-8)
run: tools/ci/import_boundary.sh
locale-parity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Locale-parity gate — ARB key parity vs en + technical-identifier + hard-coded-text (BLD-5, CON-9, FR-I18N-1..4)
run: tools/ci/locale_parity.sh
dco:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: DCO sign-off check (CON-6)
env:
# On a PR, inspect exactly the PR commits; on push, the pushed range.
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
if [ -n "${{ github.base_ref }}" ]; then
git fetch --no-tags origin "${{ github.base_ref }}"
PYBLE_DCO_RANGE="origin/${{ github.base_ref }}..${{ github.event.pull_request.head.sha }}" tools/ci/dco_check.sh
else
PYBLE_DCO_RANGE="${{ github.event.before }}..${{ github.sha }}" tools/ci/dco_check.sh
fi
sha-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: SHA-drift gate — submodule pin == versions.lock (BLD-1/2)
run: tools/ci/sha_drift.sh
patches:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Patches policy — default zero, each needs a written reason (CON-12/BLD-15)
run: tools/ci/patches_policy.sh firmware/patches
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Documentation link tests
run: python3 -m unittest discover -s tests/publication -p "test_*.py" -v
- name: Public Markdown link gate
run: python3 tools/ci/docs_links.py
firmware-host:
needs: [no-leak, spdx, patches]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Initialize pinned MicroPython host dependency
run: |
git -C firmware/upstream/micropython submodule update --init --depth 1 \
lib/micropython-lib
- name: Firmware shell and conformance gates
run: tests/firmware_tests/run_tests.sh
- name: Complete firmware host and release suite
run: python3 -m unittest discover -s tests/firmware_tests/host -p "test_*.py" -v
app:
# The Flutter app gate (X-03). Resolves strictly against the TRACKED
# app/pubspec.lock (BLD-2), runs static analysis, and runs the full test
# non-pixel suite (unit/widget/conformance — G1). Runs only after the fast
# bash gates pass, so a leak / SPDX / import-boundary / locale-parity failure
# short-circuits before the heavier toolchain setup. (dco runs as its own
# required check.)
needs: [no-leak, spdx, import-boundary, locale-parity]
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- name: Cache pub packages (keyed on the tracked lockfile)
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: pub-${{ runner.os }}-${{ hashFiles('app/pubspec.lock') }}
restore-keys: |
pub-${{ runner.os }}-
- name: Set up Flutter 3.44.1 (pinned, stable) + SDK cache
uses: subosito/flutter-action@v2
with:
flutter-version: 3.44.1
channel: stable
cache: true
- name: Resolve deps against the TRACKED lockfile (BLD-2)
run: flutter pub get --enforce-lockfile
- name: Generate localizations from ARB (gen-l10n, X-12/FR-I18N-1)
# `generate: true` also regenerates on `flutter pub get`; this explicit
# step makes AppLocalizations generation a first-class, visible CI stage
# from app/l10n.yaml + lib/localization/arb before analysis/tests run.
run: flutter gen-l10n
- name: Dart formatting
run: dart format --output=none --set-exit-if-changed lib test integration_test
- name: Static analysis
run: flutter analyze
- name: Test suite — unit/widget/conformance (G1)
run: flutter test --exclude-tags golden
app-goldens:
# Pixel goldens are renderer-specific. Run them on pinned Apple Silicon
# macOS, matching the environment used to review and commit the baselines;
# the Linux app job still runs every non-pixel Flutter test.
needs: [no-leak, spdx, locale-parity]
runs-on: macos-15
timeout-minutes: 15
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- name: Cache pub packages (keyed on OS and tracked lockfile)
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: pub-${{ runner.os }}-${{ hashFiles('app/pubspec.lock') }}
restore-keys: |
pub-${{ runner.os }}-
- name: Set up Flutter 3.44.1 (pinned, stable) + SDK cache
uses: subosito/flutter-action@v2
with:
flutter-version: 3.44.1
channel: stable
cache: true
- name: Resolve deps against the TRACKED lockfile (BLD-2)
run: flutter pub get --enforce-lockfile
- name: Generate localizations from ARB (gen-l10n, X-12/FR-I18N-1)
run: flutter gen-l10n
- name: Pixel golden suite — reviewed macOS baselines (G1)
run: flutter test --tags golden
android-webview:
# Exercise the real Android WebView/JavaScript-channel boundary, not the
# host-side widget substitute. Keep the AVD construction in plain Android
# SDK tools so the image, memory, boot bound, and diagnostics are visible.
needs: [app, app-goldens]
runs-on: ubuntu-24.04
timeout-minutes: 40
defaults:
run:
working-directory: app
env:
PYBLE_ANDROID_IMAGE: system-images;android-34;default;x86_64
PYBLE_ANDROID_COMPILE_PLATFORM: platforms;android-36
PYBLE_ANDROID_BUILD_TOOLS: build-tools;36.0.0
PYBLE_ANDROID_NDK: ndk;28.2.13676358
PYBLE_ANDROID_AVD: pyble_api34_aosp
PYBLE_ANDROID_SERIAL: emulator-5554
PYBLE_ANDROID_LOG_DIR: /tmp/pyble-android-integration
PYBLE_ANDROID_RAW_EMULATOR_LOG: /tmp/pyble-android-emulator.log
ANDROID_AVD_HOME: /tmp/pyble-android-avd
GRADLE_USER_HOME: /tmp/pyble-gradle-home
steps:
- uses: actions/checkout@v4
- name: Set up Flutter 3.44.1 (pinned, stable) + SDK cache
uses: subosito/flutter-action@v2
with:
flutter-version: 3.44.1
channel: stable
cache: true
- name: Resolve deps against the TRACKED lockfile (BLD-2)
run: flutter pub get --enforce-lockfile
- name: Generate localizations from ARB (X-12/FR-I18N-1)
run: flutter gen-l10n
- name: Bound Android Gradle resources
shell: bash
run: |
set -euo pipefail
mkdir -p "$GRADLE_USER_HOME"
printf '%s\n' \
'org.gradle.jvmargs=-Xmx3072m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m' \
'org.gradle.daemon=false' \
'org.gradle.workers.max=1' \
'org.gradle.parallel=false' \
'systemProp.file.encoding=UTF-8' \
'kotlin.compiler.execution.strategy=in-process' \
> "$GRADLE_USER_HOME/gradle.properties"
- name: Install pinned Android SDK packages
shell: bash
run: |
set -euo pipefail
if [ -d "${JAVA_HOME_17_X64:-}" ]; then
export JAVA_HOME="$JAVA_HOME_17_X64"
export PATH="$JAVA_HOME/bin:$PATH"
printf '%s\n' "JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
printf '%s\n' "$JAVA_HOME/bin" >> "$GITHUB_PATH"
fi
SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
if [ -z "$SDK_ROOT" ]; then
echo "ANDROID_SDK_ROOT/ANDROID_HOME is not configured" >&2
exit 1
fi
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
AVDMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/avdmanager"
if [ ! -x "$SDKMANAGER" ] || [ ! -x "$AVDMANAGER" ]; then
echo "Android command-line tools are unavailable under $SDK_ROOT" >&2
exit 1
fi
mkdir -p "$PYBLE_ANDROID_LOG_DIR" "$ANDROID_AVD_HOME"
# GitHub's image normally has accepted licenses. Feeding affirmative
# answers keeps a fresh image non-interactive; package installation
# below remains the authoritative, unmasked failure boundary.
yes | "$SDKMANAGER" --licenses \
>"$PYBLE_ANDROID_LOG_DIR/sdk-licenses.log" 2>&1 || true
# API 34 first boot enforces a 6 GiB userdata minimum and also
# requires 20 percent free-space headroom. GitHub's runner carries
# several side-by-side NDKs, while PyBLE intentionally builds with
# exactly one pinned revision. Remove only the unused NDK packages
# before installing the pinned SDK set so the AVD has honest space.
df -h "$SDK_ROOT" \
| tee "$PYBLE_ANDROID_LOG_DIR/disk-before-prune.log"
"$SDKMANAGER" --list_installed \
| tee "$PYBLE_ANDROID_LOG_DIR/sdk-list-installed.log"
mapfile -t PYBLE_UNUSED_NDKS < <(
awk -F '|' -v keep="$PYBLE_ANDROID_NDK" '
{
package = $1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", package)
}
package ~ /^ndk;/ && package != keep { print package }
' "$PYBLE_ANDROID_LOG_DIR/sdk-list-installed.log"
)
if [ "${#PYBLE_UNUSED_NDKS[@]}" -gt 0 ]; then
"$SDKMANAGER" --uninstall "${PYBLE_UNUSED_NDKS[@]}" \
| tee "$PYBLE_ANDROID_LOG_DIR/sdk-prune.log"
fi
"$SDKMANAGER" --channel=0 \
platform-tools \
emulator \
"$PYBLE_ANDROID_COMPILE_PLATFORM" \
"$PYBLE_ANDROID_BUILD_TOOLS" \
"$PYBLE_ANDROID_NDK" \
"$PYBLE_ANDROID_IMAGE" \
| tee "$PYBLE_ANDROID_LOG_DIR/sdk-install.log"
PYBLE_PINNED_NDK_VERSION="${PYBLE_ANDROID_NDK##*;}"
PYBLE_PINNED_NDK_ROOT="$SDK_ROOT/ndk/$PYBLE_PINNED_NDK_VERSION"
if [ ! -d "$PYBLE_PINNED_NDK_ROOT" ]; then
echo "Pinned Android NDK is unavailable at $PYBLE_PINNED_NDK_ROOT" >&2
exit 1
fi
export ANDROID_NDK="$PYBLE_PINNED_NDK_ROOT"
export ANDROID_NDK_HOME="$PYBLE_PINNED_NDK_ROOT"
export ANDROID_NDK_ROOT="$PYBLE_PINNED_NDK_ROOT"
export ANDROID_NDK_PATH="$PYBLE_PINNED_NDK_ROOT"
export ANDROID_NDK_LATEST_HOME="$PYBLE_PINNED_NDK_ROOT"
df -h "$SDK_ROOT" \
| tee "$PYBLE_ANDROID_LOG_DIR/disk-after-install.log"
printf '%s\n' \
"PYBLE_ANDROID_SDK_ROOT=$SDK_ROOT" \
"PYBLE_ANDROID_ADB=$SDK_ROOT/platform-tools/adb" \
"PYBLE_ANDROID_EMULATOR=$SDK_ROOT/emulator/emulator" \
"PYBLE_ANDROID_AVDMANAGER=$AVDMANAGER" \
"ANDROID_NDK=$ANDROID_NDK" \
"ANDROID_NDK_HOME=$ANDROID_NDK_HOME" \
"ANDROID_NDK_ROOT=$ANDROID_NDK_ROOT" \
"ANDROID_NDK_PATH=$ANDROID_NDK_PATH" \
"ANDROID_NDK_LATEST_HOME=$ANDROID_NDK_LATEST_HOME" \
>> "$GITHUB_ENV"
- name: Create API 34 AOSP x86_64 tablet AVD
shell: bash
run: |
set -euo pipefail
printf 'no\n' | "$PYBLE_ANDROID_AVDMANAGER" create avd \
--force \
--name "$PYBLE_ANDROID_AVD" \
--package "$PYBLE_ANDROID_IMAGE" \
--device pixel_tablet
AVD_CONFIG="$ANDROID_AVD_HOME/$PYBLE_ANDROID_AVD.avd/config.ini"
if [ ! -f "$AVD_CONFIG" ]; then
echo "AVD config was not created at $AVD_CONFIG" >&2
exit 1
fi
# Pixel Tablet at 2560x1600 / 320 dpi gives a deterministic
# 1280x800 logical tablet surface, matching the Android test geometry.
# API 34 first boot enforces a 6 GiB userdata minimum; state that
# requirement explicitly after reclaiming unused runner NDKs above.
# avdmanager pre-populates these keys. Upsert them so the emulator
# cannot select an earlier device-profile value from a duplicate.
python3 ../tools/ci/android_avd_config.py "$AVD_CONFIG" \
--set 'hw.ramSize=4096' \
--set 'hw.cpu.ncore=4' \
--set 'vm.heapSize=384' \
--set 'disk.dataPartition.size=6144M' \
--set 'hw.lcd.width=2560' \
--set 'hw.lcd.height=1600' \
--set 'hw.lcd.density=320' \
--set 'hw.initialOrientation=landscape' \
--set 'hw.gpu.enabled=yes' \
--set 'hw.gpu.mode=swiftshader_indirect'
- name: Prebuild Android integration application
shell: bash
run: |
set -euo pipefail
PREBUILD_STATUS=0
timeout --signal=TERM --kill-after=30s 18m \
flutter build apk \
--debug \
--no-pub \
--target-platform android-x64 \
--target integration_test/android_smoke_test.dart \
>"$PYBLE_ANDROID_LOG_DIR/flutter-build.log" 2>&1 \
|| PREBUILD_STATUS=$?
cat "$PYBLE_ANDROID_LOG_DIR/flutter-build.log"
timeout --signal=TERM --kill-after=5s 30s \
./android/gradlew --stop \
>"$PYBLE_ANDROID_LOG_DIR/gradle-stop.log" 2>&1 || true
cat "$PYBLE_ANDROID_LOG_DIR/gradle-stop.log"
if [ "$PREBUILD_STATUS" -ne 0 ]; then
exit "$PREBUILD_STATUS"
fi
test -s build/app/outputs/flutter-apk/app-debug.apk
- name: Boot headless emulator (KVM + software GPU)
shell: bash
run: |
set -euo pipefail
if [ ! -e /dev/kvm ]; then
echo "This job requires the hosted runner's /dev/kvm device" >&2
exit 1
fi
sudo chmod 666 /dev/kvm
"$PYBLE_ANDROID_EMULATOR" -accel-check \
2>&1 | tee "$PYBLE_ANDROID_LOG_DIR/accel-check.log"
nohup "$PYBLE_ANDROID_EMULATOR" \
-avd "$PYBLE_ANDROID_AVD" \
-partition-size 6144 \
-port 5554 \
-no-window \
-no-audio \
-no-boot-anim \
-no-snapshot \
-camera-back none \
-camera-front none \
-accel on \
-gpu swiftshader_indirect \
-memory 4096 \
-cores 4 \
>"$PYBLE_ANDROID_RAW_EMULATOR_LOG" 2>&1 &
EMULATOR_PID=$!
printf '%s\n' "$EMULATOR_PID" \
> "$PYBLE_ANDROID_LOG_DIR/emulator.pid"
"$PYBLE_ANDROID_ADB" start-server
BOOTED=0
for ATTEMPT in $(seq 1 180); do
if ! kill -0 "$EMULATOR_PID" 2>/dev/null; then
echo "Android emulator exited before boot completed" >&2
tail -n 200 "$PYBLE_ANDROID_RAW_EMULATOR_LOG" >&2 || true
exit 1
fi
DEVICE_STATE="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" get-state \
2>/dev/null || true
)"
BOOT_COMPLETED="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true
)"
if [ "$DEVICE_STATE" = "device" ] && [ "$BOOT_COMPLETED" = "1" ]; then
BOOTED=1
echo "Android boot completed after $ATTEMPT poll(s)."
break
fi
sleep 2
done
if [ "$BOOTED" -ne 1 ]; then
echo "Android sys.boot_completed did not reach 1 within 360 seconds" >&2
"$PYBLE_ANDROID_ADB" devices -l >&2 || true
tail -n 200 "$PYBLE_ANDROID_RAW_EMULATOR_LOG" >&2 || true
exit 1
fi
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
input keyevent 82
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
wm dismiss-keyguard
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put global device_provisioned 1
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put secure user_setup_complete 1
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put system accelerometer_rotation 0
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put system user_rotation 0
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put global window_animation_scale 0
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put global transition_animation_scale 0
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
settings put global animator_duration_scale 0
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
svc power stayon true
READY_FOR_DRIVER=0
for ATTEMPT in $(seq 1 60); do
BOOT_ANIMATION="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
getprop init.svc.bootanim 2>/dev/null | tr -d '\r' || true
)"
BOOT_ANIMATION_EXIT="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
getprop service.bootanim.exit 2>/dev/null | tr -d '\r' || true
)"
CE_AVAILABLE="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
getprop sys.user.0.ce_available 2>/dev/null \
| tr -d '\r' || true
)"
USER_UNLOCKED="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
cmd user is-user-unlocked 0 2>/dev/null \
| tr -d '\r' || true
)"
if {
[ "$BOOT_ANIMATION" = "stopped" ] ||
[ "$BOOT_ANIMATION_EXIT" = "1" ]
} && {
[ "$CE_AVAILABLE" = "true" ] ||
[ "$CE_AVAILABLE" = "1" ] ||
[ "$USER_UNLOCKED" = "true" ]
}; then
READY_FOR_DRIVER=1
echo "Android services stabilized after $ATTEMPT poll(s)."
break
fi
sleep 2
done
if [ "$READY_FOR_DRIVER" -ne 1 ]; then
echo "Android services did not stabilize within 120 seconds" >&2
echo "bootanim=$BOOT_ANIMATION exit=$BOOT_ANIMATION_EXIT" >&2
echo "ce_available=$CE_AVAILABLE unlocked=$USER_UNLOCKED" >&2
exit 1
fi
# Let the cold AOSP image settle before the prebuilt app is installed,
# so driver attachment cannot race an Activity relaunch.
sleep 20
GMS_PACKAGES="$(
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
pm list packages com.google.android.gms 2>/dev/null \
| tr -d '\r' || true
)"
if [ -n "$GMS_PACKAGES" ]; then
echo "The Android integration image unexpectedly contains GMS:" >&2
printf '%s\n' "$GMS_PACKAGES" >&2
exit 1
fi
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
dumpsys webviewupdate \
>"$PYBLE_ANDROID_LOG_DIR/webview-provider.txt"
WEBVIEW_LINE="$(
grep -F \
'Current WebView package (name, version): (com.android.webview,' \
"$PYBLE_ANDROID_LOG_DIR/webview-provider.txt" || true
)"
if [ -z "$WEBVIEW_LINE" ]; then
echo "The AOSP WebView provider is not current:" >&2
cat "$PYBLE_ANDROID_LOG_DIR/webview-provider.txt" >&2
exit 1
fi
printf '%s\n' "$WEBVIEW_LINE"
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" logcat -c
- name: Real About + Blockly integration (Android API 34)
shell: bash
run: |
set -euo pipefail
TEST_STATUS=0
timeout --signal=TERM --kill-after=30s 12m \
flutter drive \
--no-pub \
--driver test_driver/integration_test.dart \
--target integration_test/android_smoke_test.dart \
--use-application-binary build/app/outputs/flutter-apk/app-debug.apk \
--timeout 660 \
-d "$PYBLE_ANDROID_SERIAL" \
>"$PYBLE_ANDROID_LOG_DIR/flutter-drive.log" 2>&1 \
|| TEST_STATUS=$?
cat "$PYBLE_ANDROID_LOG_DIR/flutter-drive.log"
exit "$TEST_STATUS"
- name: Collect bounded Android diagnostics
if: always()
shell: bash
run: |
set +e
mkdir -p "$PYBLE_ANDROID_LOG_DIR"
{
date -u
uname -a
flutter --version
java -version
echo
echo "Installed Android package revisions:"
for PROPERTIES in \
"$PYBLE_ANDROID_SDK_ROOT/emulator/source.properties" \
"$PYBLE_ANDROID_SDK_ROOT/system-images/android-34/default/x86_64/source.properties"
do
if [ -f "$PROPERTIES" ]; then
echo "[$PROPERTIES]"
grep -E '^(Pkg\.(Path|Revision|BuildId)|AndroidVersion|SystemImage)' \
"$PROPERTIES"
fi
done
echo
"$PYBLE_ANDROID_ADB" devices -l
echo
echo "Runner disk usage:"
df -h "$PYBLE_ANDROID_SDK_ROOT"
} >"$PYBLE_ANDROID_LOG_DIR/ci-context.txt" 2>&1
if "$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" get-state \
>/dev/null 2>&1; then
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" logcat \
-d -v threadtime -t 3000 \
>"$PYBLE_ANDROID_LOG_DIR/logcat-tail.txt" 2>&1
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" shell \
dumpsys webviewupdate \
>"$PYBLE_ANDROID_LOG_DIR/webview-provider.txt" 2>&1
fi
if [ -f "$PYBLE_ANDROID_RAW_EMULATOR_LOG" ]; then
tail -n 2000 "$PYBLE_ANDROID_RAW_EMULATOR_LOG" \
>"$PYBLE_ANDROID_LOG_DIR/emulator-tail.log"
fi
AVD_CONFIG="$ANDROID_AVD_HOME/$PYBLE_ANDROID_AVD.avd/config.ini"
if [ -f "$AVD_CONFIG" ]; then
grep -E '^(AvdId|PlayStore.enabled|abi.type|disk\.dataPartition\.size|hw\.(cpu\.ncore|device\.name|gpu\.(enabled|mode)|initialOrientation|lcd\.(density|height|width)|ramSize)|image\.sysdir\.1|tag\.(display|id)|vm\.heapSize)[[:space:]]*=[[:space:]]*' \
"$AVD_CONFIG" >"$PYBLE_ANDROID_LOG_DIR/avd-config.txt"
fi
exit 0
- name: Upload Android integration diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: pyble-android-api34-webview-${{ github.run_attempt }}
path: /tmp/pyble-android-integration/
if-no-files-found: warn
retention-days: 7
- name: Terminate Android emulator
if: always()
shell: bash
run: |
set +e
if [ -n "${PYBLE_ANDROID_ADB:-}" ]; then
"$PYBLE_ANDROID_ADB" -s "$PYBLE_ANDROID_SERIAL" emu kill \
>/dev/null 2>&1
fi
PID_FILE="$PYBLE_ANDROID_LOG_DIR/emulator.pid"
if [ -f "$PID_FILE" ]; then
EMULATOR_PID="$(tr -dc '0-9' < "$PID_FILE")"
if [ -n "$EMULATOR_PID" ] && kill -0 "$EMULATOR_PID" 2>/dev/null; then
kill "$EMULATOR_PID" 2>/dev/null
for _ in $(seq 1 20); do
kill -0 "$EMULATOR_PID" 2>/dev/null || break
sleep 1
done
kill -9 "$EMULATOR_PID" 2>/dev/null || true
fi
fi
exit 0
build:
# The REAL 3-chip cross-compile. Installs the pinned ESP-IDF + initializes
# the pinned MicroPython submodule, then builds the agent per target.
# This is the toolchain job that the host gate environment cannot run.
needs: [no-leak, spdx, sha-drift, patches, firmware-host]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [esp32, esp32-s3, esp32-c3]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Fetch MicroPython lib submodules
run: git -C firmware/upstream/micropython submodule update --init --depth 1
- name: Install pinned ESP-IDF (BLD-11)
run: firmware/scripts/install_esp_idf.sh
- name: Build ${{ matrix.target }} (BLD-3/4/5)
run: firmware/scripts/build.sh ${{ matrix.target }}
- name: Collect artifacts (firmware.bin + bootloader + partition table)
uses: actions/upload-artifact@v4
with:
name: pyble-${{ matrix.target }}
path: |
firmware/build/${{ matrix.target }}/firmware.bin
firmware/build/${{ matrix.target }}/bootloader/bootloader.bin
firmware/build/${{ matrix.target }}/partition_table/partition-table.bin
if-no-files-found: error