From 29313c4104e1aa303678fc4607b58447b1d57bc9 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Sat, 15 Nov 2025 19:50:35 -0500 Subject: [PATCH] Add deployment workflow for Raspberry Pi This workflow automates the deployment of code to multiple Raspberry Pi devices and validates the BLE interface after deployment. It includes setup, deployment, validation, and summary steps. --- .github/workflows/deploy.sh | 333 ++++++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 .github/workflows/deploy.sh diff --git a/.github/workflows/deploy.sh b/.github/workflows/deploy.sh new file mode 100644 index 0000000..60f5a7e --- /dev/null +++ b/.github/workflows/deploy.sh @@ -0,0 +1,333 @@ +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 stop rnsd || exit 1 + echo ' ✓ rnsd stopped via systemd' + else + pkill -9 rnsd 2>/dev/null || true + sleep 1 + fi + # Clear the log file for clean validation + echo '' > ~/.reticulum/logfile + echo ' ✓ Log file cleared' + + echo ' [8/8] Starting rnsd...' + if systemctl is-active --quiet rnsd.service 2>/dev/null || systemctl is-enabled --quiet rnsd.service 2>/dev/null; then + sudo systemctl start rnsd || exit 1 + echo ' ✓ rnsd started via systemd' + else + 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 + STARTUP_LOGS=$(head -200 "$LOG_FILE" 2>/dev/null || echo "") + + # Check for critical errors + if echo "$STARTUP_LOGS" | grep -qE "(failed to start driver|Timeout waiting for Transport)"; then + echo " ✗ BLE driver/identity error detected" + echo "" + echo " Startup error logs:" + head -100 "$LOG_FILE" | grep -E "(BLE|ERROR)" + exit 1 + fi + + # Check for success + if echo "$STARTUP_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 " Startup logs:" + head -100 "$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