name: Lint & Build on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] workflow_dispatch: inputs: deploy: description: 'Deploy to ESP fleet after build' required: false default: 'false' type: choice options: - 'false' - 'true' jobs: build: name: Build Firmware needs: [cppcheck, flawfinder, gitleaks] runs-on: anvil container: image: docker.io/espressif/idf:v5.3 volumes: - /var/cache/ccache:/ccache env: CCACHE_DIR: /ccache IDF_CCACHE_ENABLE: 1 steps: - name: Checkout run: | git clone --depth=1 --branch=${{ github.ref_name }} \ https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . - name: Setup ccache run: | apt-get update && apt-get install -y --no-install-recommends ccache ccache --zero-stats ccache --show-config | grep -E "(cache_dir|max_size)" - name: Build firmware run: | . /opt/esp/idf/export.sh cd get-started/csi_recv_router idf.py build - name: Show ccache stats run: ccache --show-stats - name: Show binary size run: | ls -lh get-started/csi_recv_router/build/*.bin - name: Check firmware size run: | BIN="get-started/csi_recv_router/build/csi_recv_router.bin" MAX_SIZE=1966080 # 0x1E0000 = 1920 KB partition WARN_PERCENT=85 SIZE=$(stat -c%s "$BIN") PERCENT=$((SIZE * 100 / MAX_SIZE)) echo "Firmware: $((SIZE/1024)) KB / $((MAX_SIZE/1024)) KB ($PERCENT%)" if [ $SIZE -gt $MAX_SIZE ]; then echo "::error::Firmware exceeds partition size!" exit 1 fi if [ $PERCENT -gt $WARN_PERCENT ]; then echo "::warning::Firmware using $PERCENT% of partition" fi - name: Upload firmware artifact run: | mkdir -p /tmp/artifacts cp get-started/csi_recv_router/build/csi_recv_router.bin /tmp/artifacts/ cp get-started/csi_recv_router/build/bootloader/bootloader.bin /tmp/artifacts/ cp get-started/csi_recv_router/build/partition_table/partition-table.bin /tmp/artifacts/ cp get-started/csi_recv_router/build/ota_data_initial.bin /tmp/artifacts/ echo "Artifacts ready in /tmp/artifacts" ls -la /tmp/artifacts/ deploy: name: Deploy to ESP Fleet runs-on: anvil needs: build if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true' || startsWith(github.ref, 'refs/tags/v') container: image: docker.io/espressif/idf:v5.3 options: --network=host steps: - name: Install tools run: apt-get update && apt-get install -y --no-install-recommends git curl jq netcat-openbsd - name: Checkout run: | git clone --depth=1 --branch=${{ github.ref_name }} \ https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . - name: Build firmware run: | . /opt/esp/idf/export.sh cd get-started/csi_recv_router idf.py build - name: Validate version tag run: | TAG="${{ github.ref_name }}" # Extract version from binary metadata BIN_VER=$(strings get-started/csi_recv_router/build/csi_recv_router.bin | grep -oP '^v\d+\.\d+(\.\d+)?' | head -1) echo "Git tag: $TAG" echo "Binary version: $BIN_VER" if [ "$TAG" != "$BIN_VER" ]; then echo "::warning::Tag ($TAG) differs from binary ($BIN_VER)" fi - name: Create release and upload firmware env: GITEA_TOKEN: ${{ github.token }} run: | TAG="${{ github.ref_name }}" REPO="${{ github.repository }}" API_URL="https://git.mymx.me/api/v1" echo "Creating release for tag: $TAG" # Check if release exists RELEASE=$(curl -s -H "Authorization: token $GITEA_TOKEN" \ "$API_URL/repos/$REPO/releases/tags/$TAG") RELEASE_ID=$(echo "$RELEASE" | jq -r '.id // empty') if [ -z "$RELEASE_ID" ]; then # Create new release RELEASE=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"body\": \"Automated release from CI\"}" \ "$API_URL/repos/$REPO/releases") RELEASE_ID=$(echo "$RELEASE" | jq -r '.id') echo "Created release ID: $RELEASE_ID" else echo "Release exists with ID: $RELEASE_ID" fi # Upload firmware binary echo "Uploading firmware..." curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ -F "attachment=@get-started/csi_recv_router/build/csi_recv_router.bin" \ "$API_URL/repos/$REPO/releases/$RELEASE_ID/assets?name=csi_recv_router.bin" - name: Deploy via OTA run: | SENSORS="muddy-storm:192.168.129.29 amber-maple:192.168.129.30 hollow-acorn:192.168.129.31" OTA_PORT=8899 EXPECTED_VERSION="${{ github.ref_name }}" # Get runner IP (first non-loopback interface) RUNNER_IP=$(hostname -I | awk '{print $1}') echo "Runner IP: $RUNNER_IP" # Start HTTP server to serve firmware cd get-started/csi_recv_router/build python3 -m http.server $OTA_PORT & HTTP_PID=$! sleep 2 FIRMWARE_URL="http://${RUNNER_IP}:${OTA_PORT}/csi_recv_router.bin" echo "Firmware URL: $FIRMWARE_URL" # Verify server is running curl -sI "http://localhost:${OTA_PORT}/csi_recv_router.bin" | head -1 # Deploy to all sensors in parallel echo "=== Deploying to all sensors in parallel ===" for entry in $SENSORS; do NAME="${entry%%:*}" IP="${entry##*:}" echo "OTA $FIRMWARE_URL" | nc -u -w 2 "$IP" 5501 & done wait # Monitor progress echo "=== Monitoring OTA progress (timeout: 90s) ===" TIMEOUT=90 INTERVAL=5 ELAPSED=0 while [ $ELAPSED -lt $TIMEOUT ]; do sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) echo "--- Progress check at ${ELAPSED}s ---" ALL_UPDATED=true for entry in $SENSORS; do NAME="${entry%%:*}" IP="${entry##*:}" # Query sensor version via UDP STATUS command RESPONSE=$(echo "STATUS" | nc -u -w 1 "$IP" 5501 2>/dev/null || echo "") VERSION=$(echo "$RESPONSE" | grep -oP 'version=\K[^ ]+' || echo "offline") if [ "$VERSION" = "$EXPECTED_VERSION" ]; then echo " $NAME: ✓ $VERSION" elif [ "$VERSION" = "offline" ] || [ -z "$VERSION" ]; then echo " $NAME: ⟳ updating..." ALL_UPDATED=false else echo " $NAME: $VERSION (waiting for $EXPECTED_VERSION)" ALL_UPDATED=false fi done if [ "$ALL_UPDATED" = true ]; then echo "=== All sensors updated to $EXPECTED_VERSION ===" break fi done # Stop HTTP server kill $HTTP_PID 2>/dev/null || true # Final status echo "=== Final sensor status ===" for entry in $SENSORS; do NAME="${entry%%:*}" IP="${entry##*:}" RESPONSE=$(echo "STATUS" | nc -u -w 1 "$IP" 5501 2>/dev/null || echo "") VERSION=$(echo "$RESPONSE" | grep -oP 'version=\K[^ ]+' || echo "offline") echo " $NAME: $VERSION" done cppcheck: name: C/C++ Static Analysis runs-on: anvil container: image: docker.io/library/debian:bookworm-slim steps: - name: Install tools run: | apt-get update && apt-get install -y --no-install-recommends git cppcheck ca-certificates - name: Checkout run: | git clone --depth=1 --branch=${{ github.ref_name }} \ https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . - name: Run cppcheck run: | cppcheck --enable=warning,style,performance,portability \ --suppress=missingIncludeSystem \ --error-exitcode=1 \ --inline-suppr \ -I get-started/csi_recv_router/main \ get-started/csi_recv_router/main/*.c flawfinder: name: Security Flaw Analysis runs-on: anvil container: image: docker.io/library/python:3.12-slim steps: - name: Install tools run: | apt-get update && apt-get install -y --no-install-recommends git ca-certificates pip install --no-cache-dir flawfinder - name: Checkout run: | git clone --depth=1 --branch=${{ github.ref_name }} \ https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . - name: Run flawfinder run: | flawfinder --minlevel=2 --error-level=4 \ get-started/csi_recv_router/main/ gitleaks: name: Secret Scanning runs-on: anvil container: image: docker.io/zricethezav/gitleaks:latest steps: - name: Checkout run: | git clone --branch=${{ github.ref_name }} \ https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . - name: Run gitleaks run: gitleaks detect --source . --verbose --redact