#!/usr/bin/env bash # ============================================================================== # MotorSignal Operations Action Privileged Boundary Helper # File: /opt/motorsignal/current/backend/scripts/ops-action-helper.sh # Ownership: root:root, Mode: 0750 # ============================================================================== set -euo pipefail ACTION="${1:-}" if [[ -z "$ACTION" ]]; then echo '{"status":"failed","failure_code":"missing_action","status_message":"No action specified."}' exit 2 fi WEBSITE_DIR="/opt/motorsignal/current/website" CURRENT_LINK="/opt/motorsignal/current" INTERNAL_WEBSITE_URL="http://127.0.0.1:3001/api/version" PUBLIC_WEBSITE_URL="https://motorsignal.app/api/version" INTERNAL_API_URL="http://127.0.0.1:8080/api/v1/health" restart_website_service() { if sudo -n systemctl restart motorsignal-website.service >/dev/null 2>&1; then return 0 elif systemctl restart motorsignal-website.service >/dev/null 2>&1; then return 0 else return 1 fi } case "$ACTION" in deploy_website) echo ">>> [OPS-HELPER] Initiating website deployment..." # 1. Preflight: Check website working tree or directory exists if [[ ! -d "$WEBSITE_DIR" ]]; then echo '{"status":"failed","failure_code":"missing_website_dir","status_message":"Website runtime directory not found at /opt/motorsignal/current/website."}' exit 1 fi # Retain previous release pointer for automatic rollback gate PREV_RELEASE=$(readlink -f "$CURRENT_LINK" 2>/dev/null || echo "") if [[ -d "$WEBSITE_DIR/.git" ]] || git -C "$WEBSITE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then DIRTY_CHECK=$(git -C "$WEBSITE_DIR" status --porcelain 2>/dev/null || true) if [[ -n "$DIRTY_CHECK" ]]; then echo '{"status":"failed","failure_code":"dirty_source","status_message":"Cannot deploy: working tree has uncommitted changes."}' exit 1 fi # Fetch latest canonical master if remote is reachable if git -C "$WEBSITE_DIR" fetch origin master --quiet 2>/dev/null; then CURRENT_SHA=$(git -C "$WEBSITE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown") TARGET_SHA=$(git -C "$WEBSITE_DIR" rev-parse origin/master 2>/dev/null || echo "unknown") if [[ "$CURRENT_SHA" != "unknown" && "$CURRENT_SHA" == "$TARGET_SHA" ]]; then echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$CURRENT_SHA\",\"status_message\":\"Website is already up to date with origin/master ($CURRENT_SHA).\"}" exit 0 fi fi fi # 2. Restart website service to activate deployed runtime if ! restart_website_service; then echo '{"status":"failed","failure_code":"service_restart_failed","status_message":"Failed to restart motorsignal-website.service."}' exit 1 fi # 3. Verification Gate (Loopback & Public) sleep 2 VERIFY_OK=0 if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then VERIFY_OK=1 fi if [[ "$VERIFY_OK" -eq 1 ]]; then VER_JSON=$(curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" 2>/dev/null || echo '{"revision":"unknown"}') DEPLOYED_SHA=$(echo "$VER_JSON" | grep -o '"revision":"[^"]*' | cut -d'"' -f4 || echo "unknown") echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$DEPLOYED_SHA\",\"status_message\":\"Website deployment active and health verified on revision $DEPLOYED_SHA.\"}" exit 0 else # Automatic Rollback Gate echo ">>> [OPS-HELPER] Health check failed! Rolling back to previous release..." if [[ -n "$PREV_RELEASE" && -d "$PREV_RELEASE" ]]; then restart_website_service || true echo '{"status":"rolled_back","rollback_status":"rolled_back","failure_code":"health_check_failed","status_message":"Website deployment health check failed; restored previous release."}' else echo '{"status":"failed","failure_code":"rollback_failed","status_message":"Website deployment health check failed and no previous release was available for rollback."}' fi exit 1 fi ;; restart_website) echo ">>> [OPS-HELPER] Restarting motorsignal-website..." if ! restart_website_service; then echo '{"status":"failed","failure_code":"service_restart_failed","status_message":"Failed to restart motorsignal-website.service."}' exit 1 fi sleep 2 if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then echo '{"status":"succeeded","health_result":"healthy","status_message":"motorsignal-website restarted and verified active."}' exit 0 else echo '{"status":"failed","health_result":"unhealthy","failure_code":"service_unhealthy","status_message":"motorsignal-website was restarted but failed internal health probe."}' exit 1 fi ;; restart_api) echo ">>> [OPS-HELPER] Scheduling detached API restart..." systemd-run --unit="motorsignal-api-restart-$(date +%s)" \ --description="MotorSignal API Delayed Restart" \ bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 || \ sudo -n systemd-run --unit="motorsignal-api-restart-$(date +%s)" \ --description="MotorSignal API Delayed Restart" \ bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 || \ (nohup bash -c 'sleep 2; sudo -n systemctl restart motorsignal-api.service || systemctl restart motorsignal-api.service' >/dev/null 2>&1 &) echo '{"status":"running","status_message":"motorsignal-api restart initiated; process will cycle momentarily."}' exit 0 ;; reboot_server) echo ">>> [OPS-HELPER] Scheduling detached server reboot..." systemd-run --unit="motorsignal-server-reboot-$(date +%s)" \ --description="MotorSignal Server Delayed Reboot" \ bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 || \ sudo -n systemd-run --unit="motorsignal-server-reboot-$(date +%s)" \ --description="MotorSignal Server Delayed Reboot" \ bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 || \ (nohup bash -c 'sleep 3; sudo -n systemctl reboot || systemctl reboot' >/dev/null 2>&1 &) echo '{"status":"running","status_message":"Server reboot initiated; system will restart in 3 seconds."}' exit 0 ;; *) echo "{\"status\":\"failed\",\"failure_code\":\"unknown_action\",\"status_message\":\"Invalid action: $ACTION\"}" exit 2 ;; esac