name: Deploy to Raspberry Pi on: workflow_run: workflows: ["Tests"] types: - completed workflow_dispatch: jobs: # ============================================================================ # JOB 1: Parse PI_HOSTS into matrix for parallel deployment # ============================================================================ setup: name: Setup Deployment Matrix runs-on: ubuntu-latest # Only run if tests passed (for workflow_run) or if manually triggered if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} branch: ${{ steps.get-branch.outputs.branch }} steps: - name: Validate required secrets run: | if [ -z "${{ secrets.PI_HOSTS }}" ]; then echo "Error: PI_HOSTS secret is not set" echo "Please set PI_HOSTS secret with comma-separated hostnames (e.g., 'pi1.local,pi2.local')" exit 1 fi if [ -z "${{ secrets.PI_REPO_PATH }}" ]; then echo "Error: PI_REPO_PATH secret is not set" echo "Please set PI_REPO_PATH secret with repository path (e.g., '/home/pi/ble-reticulum')" exit 1 fi if [ -z "${{ secrets.PI_USER }}" ]; then echo "Error: PI_USER secret is not set" echo "Please set PI_USER secret with SSH username (e.g., 'pi')" exit 1 fi if [ -z "${{ secrets.PI_SSH_KEY }}" ]; then echo "Error: PI_SSH_KEY secret is not set" echo "Please set PI_SSH_KEY secret with SSH private key for Pi access" exit 1 fi echo "✓ All required secrets are configured" - name: Get branch name id: get-branch run: | BRANCH="${{ github.event.workflow_run.head_branch || github.ref_name }}" echo "branch=$BRANCH" >> $GITHUB_OUTPUT echo "Deployment branch: $BRANCH" - name: Parse PI_HOSTS into deployment matrix id: set-matrix env: PI_HOSTS: ${{ secrets.PI_HOSTS }} run: | # Split comma-separated PI_HOSTS into array IFS=',' read -ra HOSTS <<< "$PI_HOSTS" # Build JSON array for matrix JSON='[' for i in "${!HOSTS[@]}"; do HOST=$(echo "${HOSTS[$i]}" | xargs) if [ $i -gt 0 ]; then JSON+=','; fi JSON+="{\"host\":\"$HOST\",\"index\":$i}" done JSON+=']' echo "matrix=$JSON" >> $GITHUB_OUTPUT echo "Deployment matrix created for ${#HOSTS[@]} Pi(s)" echo "$JSON" | jq '.' # ============================================================================ # JOB 2: Deploy to each Pi (parallel matrix execution) # ============================================================================ deploy: name: Deploy to Pi ${{ matrix.pi.index }} (${{ matrix.pi.host }}) runs-on: self-hosted needs: setup strategy: matrix: pi: ${{ fromJson(needs.setup.outputs.matrix) }} fail-fast: false # Continue deploying to other Pis if one fails steps: - name: Setup SSH key env: PI_SSH_KEY: ${{ secrets.PI_SSH_KEY }} run: | mkdir -p ~/.ssh chmod 700 ~/.ssh echo "$PI_SSH_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 cat >> ~/.ssh/config </dev/null; then sudo systemctl restart rnsd || exit 1 echo ' ✓ rnsd restarted via systemd' else pkill -9 rnsd 2>/dev/null || true sleep 1 nohup \"\$RNSD_BIN\" -s > /dev/null 2>&1 & sleep 2 if pgrep -x rnsd > /dev/null; then echo ' ✓ rnsd started successfully' else echo ' ✗ Failed to start rnsd' exit 1 fi fi echo ' ✓ Deployment successful!'" # Execute deployment via SSH if echo "$DEPLOY_SCRIPT" | ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$PI_USER@$PI_HOST" bash; then echo "" echo "✓ Successfully deployed to $PI_HOST" else echo "" echo "✗ Failed to deploy to $PI_HOST" exit 1 fi - name: Cleanup SSH key if: always() run: rm -f ~/.ssh/id_ed25519 # ============================================================================ # JOB 3: Validate BLE interface on each Pi (parallel matrix execution) # ============================================================================ validate: name: Validate Pi ${{ matrix.pi.index }} (${{ matrix.pi.host }}) runs-on: self-hosted needs: [setup, deploy] strategy: matrix: pi: ${{ fromJson(needs.setup.outputs.matrix) }} fail-fast: false steps: - name: Setup SSH key env: PI_SSH_KEY: ${{ secrets.PI_SSH_KEY }} run: | mkdir -p ~/.ssh chmod 700 ~/.ssh echo "$PI_SSH_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 - name: Validate BLE interface on ${{ matrix.pi.host }} env: PI_HOST: ${{ matrix.pi.host }} PI_USER: ${{ secrets.PI_USER }} run: | echo "===================================" echo "Validating Pi ${{ matrix.pi.index }}" echo "===================================" echo "Host: $PI_HOST" echo "===================================" echo "" # Validation script VALIDATION_SCRIPT='set -e echo " [1/4] Waiting for startup (5s)..." sleep 5 echo " [2/4] Checking rnsd process..." if ! pgrep -x rnsd > /dev/null; then echo " ✗ rnsd process not running" exit 1 fi echo " ✓ rnsd is running (PID: $(pgrep -x rnsd))" echo " [3/4] Checking BLE interface logs..." LOG_FILE="$HOME/.reticulum/logfile" if [ ! -f "$LOG_FILE" ]; then echo " ✗ Log file not found at $LOG_FILE" exit 1 fi # Retry 3 times with 3s delay SUCCESS=false for attempt in 1 2 3; do RECENT_LOGS=$(tail -100 "$LOG_FILE" 2>/dev/null || echo "") # Check for critical errors if echo "$RECENT_LOGS" | grep -qE "(failed to start driver|Timeout waiting for Transport)"; then echo " ✗ BLE driver/identity error detected" echo "" echo " Recent error logs:" tail -30 "$LOG_FILE" | grep -E "(BLE|ERROR)" exit 1 fi # Check for success if echo "$RECENT_LOGS" | grep -q "interface online"; then echo " ✓ BLE interface online" SUCCESS=true break fi if [ $attempt -lt 3 ]; then echo " Retry $attempt/3 (waiting 3s)..." sleep 3 fi done if [ "$SUCCESS" = false ]; then echo " ✗ Interface did not come online after 3 attempts" echo "" echo " Recent logs:" tail -30 "$LOG_FILE" | grep -E "(BLE|ERROR|WARNING)" exit 1 fi echo " [4/4] Checking Bluetooth adapter..." if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then ADAPTER_MAC=$(bluetoothctl show 2>/dev/null | grep "Address:" | awk "{print \$2}") echo " ✓ Bluetooth adapter powered ($ADAPTER_MAC)" else echo " ⚠ Bluetooth adapter status unknown" fi echo "" echo " ✓ Validation successful!" ' # Execute validation via SSH if echo "$VALIDATION_SCRIPT" | ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$PI_USER@$PI_HOST" bash; then echo "" echo "✓ $PI_HOST validation passed" else echo "" echo "✗ $PI_HOST validation failed" exit 1 fi - name: Cleanup SSH key if: always() run: rm -f ~/.ssh/id_ed25519 # ============================================================================ # JOB 4: Summary (runs after all deploy + validate jobs complete) # ============================================================================ summary: name: Deployment Summary runs-on: ubuntu-latest needs: [setup, deploy, validate] if: always() steps: - name: Generate summary run: | echo "## 🎉 Deployment Complete" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Branch:** ${{ needs.setup.outputs.branch }}" >> $GITHUB_STEP_SUMMARY echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ needs.deploy.result }}" == "success" ] && [ "${{ needs.validate.result }}" == "success" ]; then echo "### ✅ All Pis Deployed and Validated Successfully" >> $GITHUB_STEP_SUMMARY else echo "### ⚠️ Some Pis Failed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ needs.deploy.result }}" != "success" ]; then echo "- **Deploy:** ${{ needs.deploy.result }}" >> $GITHUB_STEP_SUMMARY fi if [ "${{ needs.validate.result }}" != "success" ]; then echo "- **Validate:** ${{ needs.validate.result }}" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo "Check individual job logs for details." >> $GITHUB_STEP_SUMMARY fi