Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 228 additions & 30 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,237 @@
# This is a basic workflow to help you get started with Actions
# Copyright 2020-2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: CI
name: Cirque Quality Gate CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ master ]
branches:
- master
- main
- 'feature/**'
pull_request:
branches: [ master ]
workflow_dispatch:

permissions:
contents: read

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
virtual-rf-unit-and-coverage:
name: Virtual RF & Home Topology Unit Tests (>=95% Coverage Gate)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Install system and Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-venv python3-dbus python3-gi dbus socat psmisc
python3 -m venv --system-site-packages .venv
.venv/bin/pip install --upgrade pip setuptools wheel
.venv/bin/pip install coverage docker pyroute2 flask requests protobuf

- name: Run Virtual BT, Virtual Wi-Fi & Home Topology Unit Tests with >=95% Coverage Gate
env:
PYTHONPATH: ${{ github.workspace }}
run: |
.venv/bin/python -m unittest -v cirque/capabilities/test/test_bluetooth_capability.py
.venv/bin/python -m unittest -v cirque/capabilities/test/test_pcap_capability.py
.venv/bin/python -m unittest -v cirque/nodes/test/test_androiddockernode.py
.venv/bin/python -m unittest -v cirque/mcp/test/test_mcp_server.py
.venv/bin/python -m coverage run \
--source=cirque/virtual_wifi,cirque/home,cirque/common,cirque/mcp \
-m unittest -v \
cirque/virtual_wifi/test_virtual_wifi_server.py \
cirque/virtual_wifi/test_virtual_wifi_server_edges.py \
cirque/virtual_wifi/test_docker_wifi_bridge.py \
cirque/virtual_wifi/test_wpa2_handshake.py \
cirque/virtual_wifi/test_wpa_dbus_daemon_edges.py \
cirque/virtual_wifi/test_virtual_wifi_pcap.py \
cirque/home/test_home_ap_ordering.py \
cirque/home/test_virtual_home_topology.py \
cirque/common/test_docker_transport.py \
cirque/mcp/test/test_mcp_server.py
.venv/bin/python -m coverage report \
--include="*/cirque/virtual_wifi/*,*/cirque/home/virtual_home_topology.py,*/cirque/home/__init__.py,*/cirque/common/docker_transport.py,*/cirque/mcp/server.py,*/cirque/mcp/__init__.py" \
--omit="*/test_*" \
--fail-under=95 -m


virtual-home-docker-e2e:
name: Virtual Home 2-Docker E2E (Virtual BT + Virtual Wi-Fi)
runs-on: ubuntu-latest
needs: virtual-rf-unit-and-coverage
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Install system and Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-venv python3-dbus python3-gi dbus socat psmisc iproute2 iputils-ping
python3 -m venv --system-site-packages .venv
.venv/bin/pip install --upgrade pip setuptools wheel
.venv/bin/pip install coverage docker pyroute2 flask requests protobuf

- name: Build Virtual RF Node Docker Image
run: |
docker build -t cirque-device-base:latest \
-t cirque-virtual-rf-node:latest \
-f cirque/resources/Dockerfile.virtual_rf_node cirque/resources

- name: Run Virtual Wi-Fi Capability Docker Tests
env:
PYTHONPATH: ${{ github.workspace }}
run: |
.venv/bin/python -m unittest -v cirque/capabilities/test/test_wifi_capability.py

# Steps represent a sequence of tasks that will be executed as part of the job
- name: Run 2-Docker Virtual Home E2E (BLE Discovery + GATT + WPA2 + CLI Validation)
env:
PYTHONPATH: ${{ github.workspace }}
run: |
.venv/bin/python -m unittest -v examples/test_virtual_home_ble_wifi_e2e.py
.venv/bin/python examples/run_virtual_home_interactive.py --help

android-emulator-virtual-home:
name: Android Emulator Virtual Home (pty_bridge, hermetic tests, KVM smoke)
runs-on: ubuntu-latest
needs: virtual-rf-unit-and-coverage
env:
AVD_NAME: Pixel_6_API_34
SYSTEM_IMAGE: system-images;android-34;google_apis;x86_64
PYTHONPATH: ${{ github.workspace }}
PY: .venv/bin/python
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

# Runs a single command using the runners shell
- name: Cirque Presubmit
run: |
sudo apt install curl gnupg
curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg
sudo mv bazel.gpg /etc/apt/trusted.gpg.d/
echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
sudo apt update && sudo apt install bazel
sudo apt-get install socat psmisc tigervnc-standalone-server tigervnc-viewer python3-pip python3-venv python3-setuptools
# sudo pip3 install pycodestyle==2.5.0
sudo pip3 install yapf==0.31.0
sudo pip3 install protobuf==3.12.0
make pretty-check
make install-develop
sudo ./run_tests.sh
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Install system and Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-venv python3-dbus python3-gi dbus \
socat psmisc iproute2 iputils-ping file tshark
python3 -m venv --system-site-packages .venv
"$PY" -m pip install --upgrade pip setuptools wheel
"$PY" -m pip install coverage docker pyroute2 flask requests protobuf

# Hermetic: no emulator, no KVM, no docker daemon calls. Covers
# AndroidDockerNode (emulator command line, pty_bridge launch, guest
# Wi-Fi routing, UI automation helpers), the pcap capability, and the
# Android topology config.
- name: Run hermetic Android node and topology unit tests
run: |
"$PY" -m unittest -v cirque/nodes/test/test_androiddockernode.py
"$PY" -m unittest -v cirque/capabilities/test/test_pcap_capability.py
"$PY" -m unittest -v cirque/home/test_virtual_home_topology.py
"$PY" examples/run_android_ci_smoke.py --help
./examples/validate_virtual_android_home.sh --help

# The runner image ships an NDK (ANDROID_NDK_LATEST_HOME); build the
# guest-side bridge with it and prove the artifact is the static x86_64
# ELF the emulator image needs. Runner-image variables such as
# ANDROID_HOME are process environment, not the workflow `env` context,
# so they are read in the shell.
- name: Cross-compile pty_bridge with the Android NDK
run: |
export ANDROID_NDK_HOME="${ANDROID_NDK_LATEST_HOME:-}"
./cirque/virtual_bt/android/build_pty_bridge.sh --help
./cirque/virtual_bt/android/build_pty_bridge.sh
file cirque/virtual_bt/android/pty_bridge | tee /tmp/pty_bridge.file
grep -q 'ELF 64-bit' /tmp/pty_bridge.file
grep -q 'x86-64' /tmp/pty_bridge.file
grep -q 'statically linked' /tmp/pty_bridge.file

- name: Build cirque-android-runner and Virtual RF node images
run: |
docker build -t cirque-android-runner:latest \
-f cirque/nodes/Dockerfile.android_runner cirque/nodes
docker build -t cirque-device-base:latest \
-t cirque-virtual-rf-node:latest \
-f cirque/resources/Dockerfile.virtual_rf_node cirque/resources

# GitHub-hosted Linux runners expose /dev/kvm but only to root until
# the udev rule below is applied. Forks or self-hosted runners without
# nested virtualization simply skip the live part with a clear notice.
- name: Enable KVM for the runner user (if the host provides it)
id: kvm
run: |
if [ -e /dev/kvm ]; then
rule='KERNEL=="kvm", GROUP="kvm", MODE="0666"'
rule="$rule, OPTIONS+=\"static_node=kvm\""
echo "$rule" | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -l /dev/kvm
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::/dev/kvm not present; skipping live emulator smoke"
echo "available=false" >> "$GITHUB_OUTPUT"
fi

- name: Install emulator, platform-tools and the x86_64 system image
if: steps.kvm.outputs.available == 'true'
run: |
SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKMANAGER" --licenses > /dev/null || true
"$SDKMANAGER" --install "emulator" "platform-tools" "$SYSTEM_IMAGE"
"$ANDROID_HOME/emulator/emulator" -version | head -n 2

# avdmanager does not derive its AVD directory from $HOME (it uses the
# JVM user.home and ANDROID_* overrides), so on hosted runners the AVD
# may not land in $HOME/.android/avd. Pin ANDROID_AVD_HOME so
# avdmanager, the emulator and AndroidDockerNode all agree.
- name: Create the ${{ env.AVD_NAME }} AVD
if: steps.kvm.outputs.available == 'true'
run: |
export ANDROID_AVD_HOME="${ANDROID_AVD_HOME:-$HOME/.android/avd}"
mkdir -p "$ANDROID_AVD_HOME"
echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" >> "$GITHUB_ENV"
AVDMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager"
echo no | "$AVDMANAGER" create avd --force --name "$AVD_NAME" \
--package "$SYSTEM_IMAGE" --device pixel_6
echo "hw.cpu.ncore=2" >> "$ANDROID_AVD_HOME/$AVD_NAME.avd/config.ini"
"$AVDMANAGER" list avd
ls -l "$ANDROID_AVD_HOME/"
test -f "$ANDROID_AVD_HOME/$AVD_NAME.ini"
test -d "$ANDROID_AVD_HOME/$AVD_NAME.avd"

# Boots the real emulator in cirque-android-runner, bridges its
# Bluetooth HAL through pty_bridge to VirtualBluetoothServer and its
# Wi-Fi through the tap into VirtualWiFiServer, asserts HCI frames and
# 10.0.1.x leases on both sides, and then runs the same
# validate_virtual_android_home.sh a developer runs by hand. No
# CHIPTool.apk or Matter binaries are required for this stage.
- name: Live Android emulator radio-path smoke (pty_bridge, Wi-Fi tap)
if: steps.kvm.outputs.available == 'true'
run: |
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export ANDROID_AVD_HOME="${ANDROID_AVD_HOME:-$HOME/.android/avd}"
"$PY" examples/run_android_ci_smoke.py \
--out-dir "$RUNNER_TEMP/android_smoke" --boot-timeout 480 --radio-timeout 90

- name: Upload Android smoke evidence (logcat, device log, pcaps)
if: always() && steps.kvm.outputs.available == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: android-emulator-smoke-evidence
path: ${{ runner.temp }}/android_smoke
if-no-files-found: ignore
retention-days: 7
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ venv*
build
dist
cirque.egg-info
.coverage
cirque/virtual_bt/android/pty_bridge
Loading
Loading