Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
182b5f4
Bump MARKETING_VERSION to 1.6.0
tashda May 2, 2026
08d3193
Add stable id to ConnectionConfig and promote ConnectionHistory
tashda May 2, 2026
5998853
Carry bridge display name through Connection Live Activity
tashda May 2, 2026
2a558b3
Partition deviceFirstSeen by bridge id
tashda May 2, 2026
227e215
Add Saved Bridges screen in Settings
tashda May 2, 2026
c33b04c
Add bridge switcher toolbar item to top-level views
tashda May 2, 2026
42f9bf2
Defer store reset until handshake succeeds; auto-connect to default b…
tashda May 2, 2026
a33624f
Add dual-bridge docker stack for multi-bridge testing
tashda May 2, 2026
e289e5b
Phase 2 foundation: BridgeRegistry + per-bridge sessions
tashda May 2, 2026
505fd10
SavedBridgesView: per-row Connect/Disconnect; switcher becomes focus-…
tashda May 2, 2026
73ce0b4
Per-bridge OTA Live Activity attribution
tashda May 2, 2026
2068c70
Phase 2 tests: BridgeRegistry + ConnectionConfig.id semantics
tashda May 2, 2026
78ae5d9
Sentry breadcrumbs include bridge name
tashda May 2, 2026
f20e98c
Merged-bridge accessors and BridgeBadge component
tashda May 2, 2026
817a6ba
DeviceListView: merged multi-bridge mode
tashda May 3, 2026
7b32075
Merged multi-bridge mode for Logs, Home, Groups
tashda May 3, 2026
01e3087
Tests for merged multi-bridge aggregation
tashda May 3, 2026
427ad44
Multi-bridge correctness pass + UX revision
tashda May 3, 2026
8fde4d6
Per-bridge Settings pages (multi-bridge UX)
tashda May 3, 2026
ab6b122
Multi-bridge: bridge pickers on create-flows + per-bridge action routing
tashda May 3, 2026
0eb8419
Settings: rich Bridges section as the multi-bridge top entry
tashda May 3, 2026
f40a68a
Multi-bridge Phase 2.9 polish: typed bridge routes, attribution, colo…
tashda May 3, 2026
5d8ed2b
Merge remote-tracking branch 'origin/main' into dev
tashda May 3, 2026
39c0297
v1.6.0 CI hardening: dual-bridge integration, UI tests, lint, coverage
tashda May 3, 2026
2d006bc
Skip BridgeRegistryTests on CI runners
tashda May 3, 2026
9be4d63
Drain BridgeRegistry sessions in tearDown to fix CI malloc crashes
tashda May 3, 2026
e5afb1d
Fix awaitOpen continuation double-resume; skip Keychain-only tests
tashda May 3, 2026
1f92de0
Skip ConnectionHistoryTests on CI: every test hits Keychain transitively
tashda May 4, 2026
e795e34
Re-skip HomeLayoutStoreTests + NotificationPreferencesTests on CI
tashda May 4, 2026
82f2272
CI: fix integration tests; gate UI tests behind run-ui-tests label
tashda May 4, 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
54 changes: 54 additions & 0 deletions .github/scripts/lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Mechanical enforcement of the rules in CLAUDE.md that have an obvious
# regex form. Cheap, fast, runs in seconds. Fails the build if anything
# matches.
#
# What's checked:
# 1. SwiftUI Stepper( — never used; we have InlineIntField/Slider/TextField.
# 2. Trailing ellipsis — `…` or three literal dots before a closing quote
# in user-facing string literals. Catches "Loading…" / "Loading...".
#
# Scope: Shellbee/ (app sources) only. Tests, docker scripts, docs, and
# the windfront reference project are excluded.

set -uo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"

FAIL=0

# Filter that drops grep -Rn output lines whose source content is a comment.
# Format: <path>:<lineno>:<source>. Match comment-only lines (//, ///, /*) at
# the start of <source> after optional whitespace.
not_a_comment() {
grep -vE '^[^:]+:[0-9]+:[[:space:]]*(//|/\*)'
}

# 1. Stepper
echo "==> Checking for SwiftUI Stepper(…)"
STEPPER_HITS=$(grep -RnE '\bStepper[[:space:]]*\(' Shellbee/ --include='*.swift' \
| not_a_comment || true)
if [[ -n "$STEPPER_HITS" ]]; then
echo "::error::Forbidden SwiftUI Stepper found. Use Slider, InlineIntField, or TextField (numberPad). See CLAUDE.md."
echo "$STEPPER_HITS"
FAIL=1
fi

# 2. Trailing ellipsis in string literals — both forms.
# Heuristic: `..."` or `…"` inside a Swift file, in non-comment lines, is
# almost always user-facing copy. Annotate `// lint-allow-ellipsis` to opt out.
echo "==> Checking for trailing ellipsis in UI strings"
ELLIPSIS_HITS=$(grep -RnE '\.{3}"|…"' Shellbee/ --include='*.swift' \
| not_a_comment \
| grep -v 'lint-allow-ellipsis' || true)
if [[ -n "$ELLIPSIS_HITS" ]]; then
echo "::error::Trailing ellipsis (… or ...) found in UI strings. CLAUDE.md forbids this. Annotate '// lint-allow-ellipsis' on the line if a literal ellipsis is genuinely required (e.g. a regex)."
echo "$ELLIPSIS_HITS"
FAIL=1
fi

if [[ "$FAIL" == "0" ]]; then
echo "==> Lint passed."
fi
exit "$FAIL"
116 changes: 82 additions & 34 deletions .github/scripts/start-mock-bridge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,23 @@
# would with docker port forwarding.
#
# Logs are tee'd into $RUNNER_TEMP so failure artifacts can scoop them up.
#
# Set MULTI_BRIDGE=1 (or pass --dual) to also start a second isolated stack
# on ports 1884/8082 with token `shellbee-integration-token-2` and
# FIXTURE_PREFIX=Lab so it's visibly distinct from the primary bridge.
# Used by MultiBridgeIntegrationTests to exercise BridgeRegistry against
# two real WebSocket peers.

set -euo pipefail

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

DUAL=0
if [[ "${MULTI_BRIDGE:-0}" == "1" ]] || [[ "${1:-}" == "--dual" ]]; then
DUAL=1
fi

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

Expand All @@ -26,32 +37,39 @@ 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
write_mosq_conf() {
local port="$1" path="$2"
cat > "$path" <<EOF
listener $port
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
wait_port() {
local port="$1" timeout="${2:-30}"
for i in $(seq 1 "$timeout"); do
if nc -z localhost "$port"; then
echo "port $port up after ${i}s"
return 0
fi
sleep 1
done
return 1
}

# ── Primary bridge ──────────────────────────────────────────────────────────
echo "==> [primary] mosquitto config"
write_mosq_conf 1883 "$LOG_DIR/mosquitto-ci.conf"

echo "==> Starting Z2M WebSocket bridge"
echo "==> [primary] Starting mosquitto on 1883"
mosquitto -c "$LOG_DIR/mosquitto-ci.conf" -d
wait_port 1883 20 || { echo "mosquitto did not come up on 1883" >&2; exit 1; }

echo "==> [primary] Starting Z2M WebSocket bridge on 8080"
(
cd "$REPO_ROOT/docker/z2m-ws-bridge"
MQTT_HOST=localhost MQTT_PORT=1883 Z2M_TOPIC=zigbee2mqtt \
Expand All @@ -60,7 +78,7 @@ echo "==> Starting Z2M WebSocket bridge"
echo $! > "$LOG_DIR/z2m-bridge.pid"
)

echo "==> Starting seeder"
echo "==> [primary] Starting seeder"
(
cd "$REPO_ROOT/docker/seeder"
MQTT_HOST=localhost MQTT_PORT=1883 Z2M_TOPIC=zigbee2mqtt \
Expand All @@ -69,18 +87,48 @@ echo "==> Starting seeder"
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
echo "==> [primary] Waiting for WebSocket on 8080"
wait_port 8080 30 || {
echo "primary mock Z2M did not come up on 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
}

# ── Secondary bridge (optional) ─────────────────────────────────────────────
if [[ "$DUAL" == "1" ]]; then
echo "==> [secondary] mosquitto config (port 1884)"
write_mosq_conf 1884 "$LOG_DIR/mosquitto-ci-2.conf"

echo "==> [secondary] Starting mosquitto on 1884"
mosquitto -c "$LOG_DIR/mosquitto-ci-2.conf" -d
wait_port 1884 20 || { echo "mosquitto did not come up on 1884" >&2; exit 1; }

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

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

echo "==> [secondary] Waiting for WebSocket on 8082"
wait_port 8082 30 || {
echo "secondary mock Z2M did not come up on 8082" >&2
echo "--- bridge log ---" >&2; cat "$LOG_DIR/z2m-bridge-2.log" >&2 || true
echo "--- seeder log ---" >&2; cat "$LOG_DIR/z2m-seeder-2.log" >&2 || true
exit 1
}
fi

echo "==> Mock Z2M stack(s) ready."
27 changes: 27 additions & 0 deletions .github/workflows/ci-fast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Lint (CLAUDE.md mechanical rules)
run: ./.github/scripts/lint.sh

- name: Cache SwiftPM dependencies
uses: actions/cache@v4
with:
Expand Down Expand Up @@ -122,6 +125,21 @@ jobs:
-scheme "$SCHEME" \
-derivedDataPath "$DERIVED_DATA"

- name: Build widget extension
env:
DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }}
run: |
# Catches the "new file imports AppEnvironment but isn't in the
# widget target's membershipExceptions" trap that the main scheme
# never surfaces.
set -o pipefail
xcodebuild build \
-project Shellbee.xcodeproj \
-scheme ShellbeeWidgetsExtension \
-destination "$DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO | tee build-widget.log

- name: Build for testing
env:
DESTINATION: platform=iOS Simulator,id=${{ steps.sim.outputs.device_id }}
Expand Down Expand Up @@ -175,6 +193,15 @@ jobs:
SUMMARY=$(xcrun xcresulttool get --path "$XCRESULT" --format json 2>/dev/null \
| jq -r '.metrics | "Tests: \(.testsCount._value // 0) • Failed: \(.testsFailedCount._value // 0) • Skipped: \(.testsSkippedCount._value // 0)"' 2>/dev/null || echo "Results bundle present but could not be parsed.")
echo "**$SUMMARY**"
echo
# Best-effort line coverage. xcresulttool's coverage subcommand
# is the modern path; the CLI shape varies between Xcode versions
# so we tolerate failure rather than block CI on a parse change.
COV=$(xcrun xccov view --report --only-targets "$XCRESULT" 2>/dev/null \
| awk '/^Shellbee\.app/ { print $2 }' | head -n1 || true)
if [ -n "$COV" ]; then
echo "Line coverage (Shellbee.app): **$COV**"
fi
else
echo "_No result bundle produced (build may have failed before tests ran)._"
fi
Expand Down
Loading
Loading