Skip to content

Phase 3: Deployment Scripts Reference

Reference implementations for deployment scripts. Some of these exist in the codebase already, others are planned patterns for future use.

Existing Scripts

scripts/deployment-health-check.sh

Cloud deployment health check. Validates backend API, frontend, Docker container health, and disk usage. Used by cloud-deploy.yml.

scripts/device-health-check.sh

Device health check. Validates device service (port 3003), frontend (port 8080), Docker containers (device_service, frontend_smoker, watchtower), Tailscale connectivity, cloud backend connectivity, and system resources. Used by device-deploy.yml.

scripts/validate-virtual-smoker.sh

Infrastructure validation for the virtual smoker device. Checks SSH connectivity, CPU cores (expects 4), memory (~1GB), swap (~100MB), Docker version (expects 24.x), Tailscale hostname, directory structure, and Docker Compose availability.

Reference Patterns

The following scripts are reference patterns from the original planning doc. They may be useful when implementing remaining stories.

Cloud Health Check Pattern

#!/bin/bash
# scripts/health-check.sh

HOST=$1
if [ -z "$HOST" ]; then
    echo "Usage: $0 <hostname>"
    exit 1
fi

echo "Running health checks for $HOST..."

check_service() {
    local service=$1
    local port=$2
    local path=${3:-"/"}

    if curl -f -s --max-time 10 "http://${HOST}:${port}${path}" > /dev/null; then
        echo "OK: $service is healthy"
        return 0
    else
        echo "FAIL: $service is unhealthy"
        return 1
    fi
}

HEALTH_CHECKS=(
    "backend:3001:/health"
    "frontend:80:/"
    "device-service:3002:/health"
)

failed_checks=0

for check in "${HEALTH_CHECKS[@]}"; do
    IFS=':' read -r service port path <<< "$check"
    if ! check_service "$service" "$port" "$path"; then
        failed_checks=$((failed_checks + 1))
    fi
    sleep 2
done

container_status=$(ssh root@${HOST} "docker-compose ps --services --filter 'status=running'" 2>/dev/null | wc -l)
expected_containers=4

if [ "$container_status" -ge "$expected_containers" ]; then
    echo "OK: All containers are running"
else
    echo "FAIL: Some containers are not running (expected: $expected_containers, running: $container_status)"
    failed_checks=$((failed_checks + 1))
fi

memory_usage=$(ssh root@${HOST} "free | grep Mem | awk '{printf \"%.1f\", \$3/\$2 * 100.0}'")
disk_usage=$(ssh root@${HOST} "df / | tail -1 | awk '{print \$5}' | sed 's/%//'")

echo "Memory usage: ${memory_usage}%"
echo "Disk usage: ${disk_usage}%"

if [ $failed_checks -eq 0 ]; then
    echo "All health checks passed for $HOST"
    exit 0
else
    echo "$failed_checks health check(s) failed for $HOST"
    exit 1
fi

Raspberry Pi Device Update Pattern

#!/bin/bash
# scripts/update-single-device.sh

DEVICE=$1
if [ -z "$DEVICE" ]; then
    echo "Usage: $0 <device-hostname>"
    exit 1
fi

echo "Updating Raspberry Pi device: $DEVICE"

# Pre-update health check
if ! ./scripts/health-check-device.sh "$DEVICE"; then
    echo "WARNING: Device $DEVICE is not healthy before update, proceeding anyway..."
fi

# Backup current state
ssh pi@$DEVICE "docker-compose ps > /tmp/pre-update-status.txt"

# Trigger Watchtower one-shot update
ssh pi@$DEVICE "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once --cleanup"

sleep 60

# Verify services restarted
max_attempts=10
attempt=0

while [ $attempt -lt $max_attempts ]; do
    if ssh pi@$DEVICE "docker-compose ps | grep -q 'Up'"; then
        echo "Services are running"
        break
    fi
    echo "Waiting for services to start (attempt $((attempt + 1))/$max_attempts)..."
    sleep 30
    attempt=$((attempt + 1))
done

if [ $attempt -eq $max_attempts ]; then
    echo "Services failed to start after update"
    ssh pi@$DEVICE "cd /opt/smart-smoker && docker-compose down && docker-compose up -d"
    sleep 30
    if ! ssh pi@$DEVICE "docker-compose ps | grep -q 'Up'"; then
        echo "Recovery failed for device $DEVICE"
        exit 1
    fi
fi

# Post-update verification
if ./scripts/health-check-device.sh "$DEVICE"; then
    echo "Device $DEVICE updated successfully"
else
    echo "Device $DEVICE failed post-update health check"
    exit 1
fi

Integration Test Pattern (Story 7)

#!/bin/bash
# scripts/run-integration-tests.sh

VIRTUAL_DEVICE=$1
if [ -z "$VIRTUAL_DEVICE" ]; then
    echo "Usage: $0 <virtual-device-hostname>"
    exit 1
fi

echo "Running integration tests on virtual device: $VIRTUAL_DEVICE"

# Test 1: Device Service Connectivity
echo "Test 1: Device Service Connectivity"
if curl -f -s --max-time 10 "http://${VIRTUAL_DEVICE}:3002/health" > /dev/null; then
    echo "PASS: Device service is reachable"
else
    echo "FAIL: Device service connectivity test failed"
    exit 1
fi

# Test 2: Mock Hardware Integration
echo "Test 2: Mock Hardware Integration"
temp_response=$(curl -s "http://${VIRTUAL_DEVICE}:5000/temperature/28-000000000001")
if echo "$temp_response" | jq -e '.temperature' > /dev/null 2>&1; then
    echo "PASS: Mock temperature sensor responding"
else
    echo "FAIL: Mock hardware integration test failed"
    exit 1
fi

# Test 3: WebSocket Communication
echo "Test 3: WebSocket Communication"
cat > /tmp/websocket-test.js << 'WSEOF'
const WebSocket = require('ws');

const ws = new WebSocket('ws://VIRTUAL_DEVICE:8765');

ws.on('open', function open() {
    ws.send(JSON.stringify({ type: 'test', message: 'integration test' }));
});

ws.on('message', function message(data) {
    const response = JSON.parse(data);
    if (response.type === 'command_ack') {
        console.log('PASS: WebSocket communication test passed');
        process.exit(0);
    }
});

ws.on('error', function error(err) {
    console.log('FAIL: WebSocket test failed:', err.message);
    process.exit(1);
});

setTimeout(() => {
    console.log('FAIL: WebSocket test timeout');
    process.exit(1);
}, 10000);
WSEOF

sed -i "s/VIRTUAL_DEVICE/${VIRTUAL_DEVICE}/g" /tmp/websocket-test.js
if ssh pi@$VIRTUAL_DEVICE "cd /tmp && node /tmp/websocket-test.js"; then
    echo "PASS: WebSocket communication test passed"
else
    echo "FAIL: WebSocket communication test failed"
    exit 1
fi

# Test 4: End-to-End Smoke Test
echo "Test 4: End-to-End Smoke Test"
curl -X POST "http://${VIRTUAL_DEVICE}:5000/temperature/28-000000000001/target/250"
sleep 5
new_temp=$(curl -s "http://${VIRTUAL_DEVICE}:5000/temperature/28-000000000001" | jq -r '.temperature')

if (( $(echo "$new_temp > 230" | bc -l) )); then
    echo "PASS: End-to-end smoke test passed (temp: $new_temp)"
else
    echo "FAIL: End-to-end smoke test failed (temp: $new_temp)"
    exit 1
fi

echo "All integration tests passed on $VIRTUAL_DEVICE"