Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4ceb0d7
Restructure Settings Logging section
tashda Apr 24, 2026
0e93eb3
Redesign Permit Join active sheet for native iOS feel
tashda Apr 24, 2026
1c5a46b
Redesign Fan card with FeatureCatalog and section layout
tashda Apr 26, 2026
da103e9
Install colima on macos-15 runners so Full CI has Docker
tashda Apr 26, 2026
8ebc754
Use setup-docker-macos-action instead of brew/colima
tashda Apr 26, 2026
839c31f
Run mock Z2M stack natively on macos runners (drop Docker)
tashda Apr 26, 2026
c7d4501
Add native mock Z2M bridge launcher script
tashda Apr 26, 2026
b59c987
Anchor scripts/ ignore to repo root only
tashda Apr 26, 2026
e1f89f4
Use venv for mock bridge Python deps (PEP 668)
tashda Apr 26, 2026
1ad0041
Restore Expose memberwise init suppressed by custom decoder
tashda Apr 26, 2026
b0a313d
Mark Expose memberwise init nonisolated
tashda Apr 26, 2026
ccd7d8c
Skip Full CI tests known to fail under GitHub runner
tashda Apr 26, 2026
299ec6f
Skip Keychain-dependent integration test; cancel sibling on failure
tashda Apr 26, 2026
ff23df1
Add optimistic device rename, Recently Added section, and interview i…
tashda Apr 26, 2026
b7245dd
Add HTTP test center to mock z2m seeder
tashda Apr 26, 2026
62986bf
Use simctl bootstatus -b in Full CI; grant actions:write for cancel step
tashda Apr 26, 2026
408319d
Update Z2MMessageRouter tests; extract DeviceListContent subview
tashda Apr 27, 2026
c00ac76
Drop UI test parallelism in Full CI to debug 30-min hang
tashda Apr 27, 2026
4254be4
Diagnose UI test hang: pseudo-TTY + smoke test first
tashda Apr 27, 2026
e941e78
Re-enable UI test parallelism, bump timeout to 60/75 min
tashda Apr 27, 2026
e6d23c5
Add optimistic device-remove flow with Deleting badge
tashda Apr 27, 2026
48e8747
Drop UI tests from Full CI while UI is in active redesign
tashda Apr 27, 2026
14c7b8d
Bump 1.2.0: proactive reachability + connection-lost notification
tashda Apr 27, 2026
e8e7b0b
Fix device detail title not updating after rename
tashda Apr 27, 2026
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
86 changes: 86 additions & 0 deletions .github/scripts/start-mock-bridge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Start the mock Z2M stack natively on the macOS GitHub runner.
#
# We can't run docker-compose on macos-15 ARM runners — Docker isn't
# preinstalled and nested virtualization is unavailable, so colima/Lima
# can't boot a Linux VM. The stack is just mosquitto + two Python
# scripts, so we run them directly on the host instead. The simulator
# still reaches them on localhost:1883 / localhost:8080 the same way it
# would with docker port forwarding.
#
# Logs are tee'd into $RUNNER_TEMP so failure artifacts can scoop them up.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LOG_DIR="${RUNNER_TEMP:-/tmp}"

echo "==> Installing mosquitto and Python deps"
brew install mosquitto

# Homebrew Python on macOS GitHub runners is PEP 668 externally-managed,
# so install Python deps into a dedicated venv and use that interpreter.
VENV="$LOG_DIR/z2m-venv"
python3 -m venv "$VENV"
PYTHON="$VENV/bin/python"
"$PYTHON" -m pip install --upgrade --quiet pip
"$PYTHON" -m pip install --quiet paho-mqtt websockets

echo "==> Writing mosquitto config (anonymous, no persistence)"
MOSQ_CONF="$LOG_DIR/mosquitto-ci.conf"
cat > "$MOSQ_CONF" <<EOF
listener 1883
allow_anonymous true
persistence false
log_type error
log_type warning
log_type notice
EOF

echo "==> Starting mosquitto"
mosquitto -c "$MOSQ_CONF" -d
for i in $(seq 1 20); do
if nc -z localhost 1883; then
echo "mosquitto up after ${i}s"
break
fi
sleep 1
done
if ! nc -z localhost 1883; then
echo "mosquitto did not come up on 1883" >&2
exit 1
fi

echo "==> Starting Z2M WebSocket bridge"
(
cd "$REPO_ROOT/docker/z2m-ws-bridge"
MQTT_HOST=localhost MQTT_PORT=1883 Z2M_TOPIC=zigbee2mqtt \
WS_PORT=8080 HEALTH_PORT=8081 AUTH_TOKEN=shellbee-integration-token \
nohup "$PYTHON" -u bridge.py >"$LOG_DIR/z2m-bridge.log" 2>&1 &
echo $! > "$LOG_DIR/z2m-bridge.pid"
)

echo "==> Starting seeder"
(
cd "$REPO_ROOT/docker/seeder"
MQTT_HOST=localhost MQTT_PORT=1883 Z2M_TOPIC=zigbee2mqtt \
MODE=continuous SEED_INTERVAL=10 \
nohup "$PYTHON" -u seeder.py >"$LOG_DIR/z2m-seeder.log" 2>&1 &
echo $! > "$LOG_DIR/z2m-seeder.pid"
)

echo "==> Waiting for WebSocket bridge on localhost:8080"
for i in $(seq 1 30); do
if nc -z localhost 8080; then
echo "Mock Z2M bridge is up after ${i}s"
exit 0
fi
sleep 1
done

echo "Mock Z2M bridge did not come up on localhost:8080" >&2
echo "--- bridge log ---" >&2
cat "$LOG_DIR/z2m-bridge.log" >&2 || true
echo "--- seeder log ---" >&2
cat "$LOG_DIR/z2m-seeder.log" >&2 || true
exit 1
228 changes: 38 additions & 190 deletions .github/workflows/ci-full.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
name: CI (Full)

# Complete suite: unit tests + UI tests. Runs in three situations:
# Unit + Integration suite, run against a live mock z2m bridge. Triggers:
# 1. Nightly at 03:00 UTC (catches environmental / flaky issues)
# 2. On-demand: manually from the Actions tab, or by labeling a PR
# with `run-ui-tests` when you want extra confidence before merge
# 2. On-demand from the Actions tab
# 3. Labeling a PR with `run-ui-tests` (kept for parity with the existing
# flow even though the UI job itself was removed — see below)
#
# Typical duration: 15-25 min. Not required for PR merge — see ci-fast.yml
# for the required check. Intentionally does NOT run on every push to main,
# to avoid a red check appearing on the trunk for transient UI flakes.
# UI tests previously ran here too but were removed while the device list /
# settings / logs UIs are in active redesign — the suite was reliably red on
# tests-vs-UI mismatches that aren't real regressions, training us to ignore
# the nightly badge. Restore the ui-tests job (git history) once the UI
# settles and we have a green baseline to defend.
#
# Typical duration: ~10 min. Not required for PR merge — see ci-fast.yml
# for the required check.

on:
schedule:
Expand Down Expand Up @@ -63,22 +69,8 @@ jobs:
restore-keys: |
spm-${{ runner.os }}-

- name: Start mock Z2M bridge (docker compose)
run: |
# Full CI runs the Z2MIntegrationTests against the real mock bridge.
# Start it in the background; Z2MIntegrationTests.skipIfZ2MUnavailable
# gives us a 5s grace period before skipping, so we wait explicitly.
docker compose up -d
for i in $(seq 1 30); do
if nc -z localhost 8080; then
echo "Mock Z2M bridge is up after ${i}s"
exit 0
fi
sleep 1
done
echo "Mock Z2M bridge did not come up on localhost:8080" >&2
docker compose logs
exit 1
- name: Start mock Z2M bridge (native)
run: ./.github/scripts/start-mock-bridge.sh

- name: Select latest Xcode
run: |
Expand All @@ -100,23 +92,16 @@ jobs:
echo "device_id=$DEVICE_ID" >> "$GITHUB_OUTPUT"
echo "device_name=$NAME" >> "$GITHUB_OUTPUT"

- name: Pre-boot simulator
timeout-minutes: 3
- name: Boot simulator and wait for readiness
timeout-minutes: 4
run: |
# `simctl bootstatus -b` boots (if not already) and BLOCKS until the
# device is actually ready (springboard up, services responding) —
# not just "Booted" state. The previous state-check loop returned
# too early, leaving xcodebuild waiting on a not-yet-ready simulator
# which silently hung for the full 30-minute test timeout.
DEVICE_ID="${{ steps.sim.outputs.device_id }}"
xcrun simctl boot "$DEVICE_ID" || true
for i in $(seq 1 60); do
STATE=$(xcrun simctl list devices --json | jq -r \
--arg id "$DEVICE_ID" \
'[.devices | to_entries[].value[] | select(.udid==$id)][0].state')
if [ "$STATE" = "Booted" ]; then
echo "Simulator booted after ${i}x2s"
exit 0
fi
sleep 2
done
echo "Simulator did not reach Booted state" >&2
exit 1
xcrun simctl bootstatus "$DEVICE_ID" -b

- name: Resolve Swift packages
run: |
Expand All @@ -140,22 +125,30 @@ jobs:
env:
DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }}
run: |
# Skip the same tests that Shellbee-CI.xctestplan skips on Fast CI
# (keychain/concurrency issues that only fail on GitHub runners) —
# except Z2MIntegrationTests, which Full CI explicitly runs because
# we now have the mock bridge up. See xctestplans/README.md.
set -o pipefail
xcodebuild test-without-building \
-project Shellbee.xcodeproj -scheme "$SCHEME" \
-testPlan "$TEST_PLAN" \
-destination "$DESTINATION" -derivedDataPath "$DERIVED_DATA" \
-only-testing:"$TEST_TARGET" \
-skip-testing:"ShellbeeTests/ConnectionHistoryTests" \
-skip-testing:"ShellbeeTests/HomeLayoutStoreTests" \
-skip-testing:"ShellbeeTests/NotificationPreferencesTests" \
-skip-testing:"ShellbeeTests/ConnectionConfigTests/testSaveAndLoad" \
-skip-testing:"ShellbeeTests/ConnectionConfigTests/testSecondLoadAfterLegacyMigrationStillReturnsToken" \
-skip-testing:"ShellbeeTests/Z2MIntegrationTests/testReloadedPersistedConfigConnectsAndReceivesBridgeInfo" \
-resultBundlePath "$DERIVED_DATA/UnitTests.xcresult" \
CODE_SIGNING_ALLOWED=NO | tee test-unit.log

- name: Capture docker logs
- name: Capture mock bridge logs
if: always()
run: docker compose logs --no-color > docker-compose.log 2>&1 || true

- name: Stop docker compose
if: always()
run: docker compose down -v || true
run: |
cp "$RUNNER_TEMP/z2m-bridge.log" mock-bridge.log 2>/dev/null || true
cp "$RUNNER_TEMP/z2m-seeder.log" mock-seeder.log 2>/dev/null || true

- name: Summarize
if: always()
Expand Down Expand Up @@ -187,154 +180,9 @@ jobs:
path: |
build-unit.log
test-unit.log
mock-bridge.log
mock-seeder.log
${{ env.DERIVED_DATA }}/UnitTests.xcresult
if-no-files-found: ignore
retention-days: 14

ui-tests:
name: UI Tests
needs: gate
runs-on: macos-15
timeout-minutes: 45

env:
SCHEME: Shellbee
TEST_PLAN: Shellbee
TEST_TARGET: ShellbeeUITests
DERIVED_DATA: ${{ github.workspace }}/DerivedData

steps:
- uses: actions/checkout@v4

- name: Cache SwiftPM dependencies
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/DerivedData/SourcePackages/checkouts
~/Library/Caches/org.swift.swiftpm
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
spm-${{ runner.os }}-

- name: Select latest Xcode
run: |
LATEST=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -n1)
sudo xcode-select -s "$LATEST"
xcodebuild -version

- name: Pick simulator
id: sim
run: |
DEVICE_ID=$(xcrun simctl list devices available --json \
| jq -r '[.devices | to_entries[]
| select(.key | test("iOS|com.apple.CoreSimulator.SimRuntime.iOS"))
| .value[]
| select(.isAvailable and (.name | startswith("iPhone")))]
| sort_by(.name) | reverse | .[0].udid')
NAME=$(xcrun simctl list devices --json \
| jq -r --arg id "$DEVICE_ID" '[.devices | to_entries[].value[] | select(.udid==$id)][0].name')
echo "device_id=$DEVICE_ID" >> "$GITHUB_OUTPUT"
echo "device_name=$NAME" >> "$GITHUB_OUTPUT"

- name: Pre-boot simulator
timeout-minutes: 3
run: |
DEVICE_ID="${{ steps.sim.outputs.device_id }}"
xcrun simctl boot "$DEVICE_ID" || true
for i in $(seq 1 60); do
STATE=$(xcrun simctl list devices --json | jq -r \
--arg id "$DEVICE_ID" \
'[.devices | to_entries[].value[] | select(.udid==$id)][0].state')
if [ "$STATE" = "Booted" ]; then
echo "Simulator booted after ${i}x2s"
exit 0
fi
sleep 2
done
echo "Simulator did not reach Booted state" >&2
exit 1

- name: Start mock Z2M bridge
run: |
docker compose up -d
for i in $(seq 1 30); do
if nc -z localhost 8080; then
echo "Mock Z2M bridge is up after ${i}s"
exit 0
fi
sleep 1
done
docker compose logs
exit 1

- name: Resolve Swift packages
run: |
xcodebuild -resolvePackageDependencies \
-project Shellbee.xcodeproj -scheme "$SCHEME" \
-derivedDataPath "$DERIVED_DATA"

- name: Build for testing
env:
DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }}
run: |
set -o pipefail
xcodebuild build-for-testing \
-project Shellbee.xcodeproj -scheme "$SCHEME" \
-testPlan "$TEST_PLAN" \
-destination "$DESTINATION" -derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO | tee build-ui.log

- name: Run UI tests (2 parallel workers)
timeout-minutes: 30
env:
DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }}
run: |
set -o pipefail
xcodebuild test-without-building \
-project Shellbee.xcodeproj -scheme "$SCHEME" \
-testPlan "$TEST_PLAN" \
-destination "$DESTINATION" -derivedDataPath "$DERIVED_DATA" \
-only-testing:"$TEST_TARGET" \
-parallel-testing-enabled YES \
-parallel-testing-worker-count 2 \
-resultBundlePath "$DERIVED_DATA/UITests.xcresult" \
CODE_SIGNING_ALLOWED=NO | tee test-ui.log

- name: Stop docker compose
if: always()
run: docker compose down -v || true

- name: Summarize
if: always()
env:
DEVICE_NAME: ${{ steps.sim.outputs.device_name }}
run: |
{
echo "## UI Tests"
echo
echo "| Field | Value |"
echo "| --- | --- |"
echo "| Simulator | $DEVICE_NAME |"
echo "| Target | $TEST_TARGET |"
echo "| Workers | 2 parallel |"
echo
if [ -d "$DERIVED_DATA/UITests.xcresult" ]; then
xcrun xcresulttool get --path "$DERIVED_DATA/UITests.xcresult" --format json 2>/dev/null \
| jq -r '.metrics | "**Tests: \(.testsCount._value // 0) • Failed: \(.testsFailedCount._value // 0) • Skipped: \(.testsSkippedCount._value // 0)**"' \
|| echo "_Result bundle present but could not be parsed._"
else
echo "_No result bundle produced._"
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: ci-full-ui-logs
path: |
build-ui.log
test-ui.log
${{ env.DERIVED_DATA }}/UITests.xcresult
if-no-files-found: ignore
retention-days: 14
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ REFINEMENT_PLAN.md
SETTINGS_ARCHITECTURE.md

# Local developer scripts (per-developer worktree/build helpers)
scripts/
/scripts/

# Local logs and diagnostics
Z2Mlog.txt
Expand Down
Loading
Loading