diff --git a/.github/workflows/bfld-mqtt-integration.yml b/.github/workflows/bfld-mqtt-integration.yml new file mode 100644 index 00000000..a47f416c --- /dev/null +++ b/.github/workflows/bfld-mqtt-integration.yml @@ -0,0 +1,99 @@ +name: BFLD MQTT Integration + +# Runs the env-gated mosquitto integration tests from iters 24 + 29 of the +# BFLD rollout (ADR-118 / ADR-122 §2.2). Spins up an eclipse-mosquitto:2 +# service container, exports BFLD_MQTT_BROKER, runs `cargo test --features +# mqtt`. Local developers can reproduce with: +# +# scoop install mosquitto # Windows +# # or: docker run -p 1883:1883 eclipse-mosquitto:2 +# BFLD_MQTT_BROKER=tcp://localhost:1883 \ +# cargo test -p wifi-densepose-bfld --features mqtt + +on: + push: + branches: + - main + - 'feat/adr-118-*' + - 'feat/bfld-*' + paths: + - 'v2/crates/wifi-densepose-bfld/**' + - '.github/workflows/bfld-mqtt-integration.yml' + pull_request: + paths: + - 'v2/crates/wifi-densepose-bfld/**' + - '.github/workflows/bfld-mqtt-integration.yml' + workflow_dispatch: + +jobs: + mqtt-live-broker: + name: cargo test --features mqtt (live mosquitto) + runs-on: ubuntu-latest + timeout-minutes: 15 + + services: + mosquitto: + image: eclipse-mosquitto:2 + ports: + - 1883:1883 + # Allow anonymous connections — local-only CI broker, no exposure + # to the public internet, never touches production credentials. + options: >- + --health-cmd "mosquitto_pub -h localhost -t healthcheck -m ping || exit 1" + --health-interval 5s + --health-timeout 3s + --health-retries 10 + + env: + BFLD_MQTT_BROKER: tcp://localhost:1883 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUSTFLAGS: -D warnings + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo registry + target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + v2/target + key: bfld-mqtt-${{ runner.os }}-${{ hashFiles('v2/Cargo.lock') }} + + - name: Wait for mosquitto to be ready + run: | + for i in {1..20}; do + if nc -z localhost 1883; then + echo "mosquitto reachable on port 1883 (attempt $i)" + exit 0 + fi + echo "waiting for mosquitto ($i/20)..." + sleep 1 + done + echo "mosquitto never became reachable" >&2 + exit 1 + + - name: cargo test --no-default-features (baseline regression) + working-directory: v2 + run: cargo test -p wifi-densepose-bfld --no-default-features + + - name: cargo test (default features) + working-directory: v2 + run: cargo test -p wifi-densepose-bfld + + - name: cargo test --features mqtt (incl. live mosquitto roundtrip) + working-directory: v2 + run: cargo test -p wifi-densepose-bfld --features mqtt + + - name: cargo clippy --features mqtt (lint gate) + working-directory: v2 + run: cargo clippy -p wifi-densepose-bfld --features mqtt --all-targets -- -D warnings + continue-on-error: true diff --git a/v2/crates/wifi-densepose-bfld/tests/ci_workflow.rs b/v2/crates/wifi-densepose-bfld/tests/ci_workflow.rs new file mode 100644 index 00000000..1f957f8c --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/ci_workflow.rs @@ -0,0 +1,92 @@ +//! Structural validation for `.github/workflows/bfld-mqtt-integration.yml`. +//! Same pattern as iter-30's HA blueprint tests: embed via `include_str!`, +//! string-check the key fields. Avoids adding a serde_yaml dep just to lint +//! a CI workflow. + +#![cfg(feature = "std")] + +const WORKFLOW: &str = include_str!( + "../../../../.github/workflows/bfld-mqtt-integration.yml" +); + +#[test] +fn workflow_declares_mosquitto_service_container() { + assert!( + WORKFLOW.contains("image: eclipse-mosquitto:2"), + "workflow must declare eclipse-mosquitto:2 as a service container", + ); + assert!( + WORKFLOW.contains("- 1883:1883"), + "workflow must expose port 1883 from the mosquitto service", + ); +} + +#[test] +fn workflow_exports_broker_env_for_iter_24_and_29_tests() { + assert!( + WORKFLOW.contains("BFLD_MQTT_BROKER: tcp://localhost:1883"), + "BFLD_MQTT_BROKER env var must point at the service container so the \ + iter-24 mosquitto_integration test exits skip mode", + ); +} + +#[test] +fn workflow_runs_three_cargo_test_invocations() { + // Regression guard for the default + no-default-features + mqtt matrix. + // Each one catches a different class of bug: + // --no-default-features: catches std-feature leakage + // default: catches the everyday surface + // --features mqtt: catches the live-broker integration path + assert!(WORKFLOW.contains("cargo test -p wifi-densepose-bfld --no-default-features")); + assert!(WORKFLOW.contains("cargo test -p wifi-densepose-bfld")); + assert!(WORKFLOW.contains("cargo test -p wifi-densepose-bfld --features mqtt")); +} + +#[test] +fn workflow_waits_for_mosquitto_readiness_before_testing() { + assert!( + WORKFLOW.contains("nc -z localhost 1883"), + "workflow must port-poll for mosquitto readiness — a service container \ + can take a few seconds to bind even with healthcheck", + ); +} + +#[test] +fn workflow_uses_health_check_on_the_service() { + assert!( + WORKFLOW.contains("--health-cmd"), + "service container should declare a health-check for stable startup", + ); + assert!( + WORKFLOW.contains("mosquitto_pub"), + "health-check should attempt a real publish, not just process liveness", + ); +} + +#[test] +fn workflow_only_triggers_on_bfld_paths() { + assert!( + WORKFLOW.contains("v2/crates/wifi-densepose-bfld/**"), + "path filter must scope the workflow to BFLD changes, not run on every push", + ); +} + +#[test] +fn workflow_pins_runner_to_ubuntu_latest_for_docker_service_support() { + assert!( + WORKFLOW.contains("runs-on: ubuntu-latest"), + "GitHub Actions Docker service containers require linux; macOS and \ + Windows runners don't support `services:`.", + ); +} + +#[test] +fn workflow_has_timeout_guard() { + // The integration tests have 10-second recv timeouts but the matrix runs + // three cargo invocations + cache + warmup; a top-level timeout-minutes + // guards against a stuck broker or rumqttc handshake hanging the runner. + assert!( + WORKFLOW.contains("timeout-minutes:"), + "workflow must declare a top-level timeout-minutes to bound runner cost", + ); +}