Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/cmd_db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ cmd_migrate_schema() {

case "$migrate_target" in
neo4j)
# Guard: skip if required Neo4j vars are not configured (strut#518)
if [ -z "${NEO4J_URI:-}" ] || [ -z "${NEO4J_PASSWORD:-}" ]; then
log "Skipping Neo4j migration — NEO4J_URI/NEO4J_PASSWORD not configured"
return 0
fi
if [ -z "${MIGRATION_IMAGE:-}" ]; then
log "Skipping Neo4j migration — MIGRATION_IMAGE not set"
return 0
fi
log "Running Neo4j schema migration ($migrate_action)..."
local migrate_cmd
if [ "$migrate_action" = "--down" ]; then
Expand All @@ -155,6 +164,15 @@ cmd_migrate_schema() {
$migrate_cmd
;;
postgres)
# Guard: skip if required Postgres vars are not configured (strut#518)
if [ -z "${POSTGRES_USER:-}" ] || [ -z "${POSTGRES_PASSWORD:-}" ] || [ -z "${POSTGRES_DB:-}" ]; then
log "Skipping Postgres migration — POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB not configured"
return 0
fi
if [ -z "${MIGRATION_IMAGE:-}" ]; then
log "Skipping Postgres migration — MIGRATION_IMAGE not set"
return 0
fi
local pg_action="${migrate_action:---up}"
log "Running Postgres schema migration ($pg_action)..."
local postgres_migrate_cmd
Expand Down
9 changes: 9 additions & 0 deletions lib/cmd_drift_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ drift_images_remote() {
cd '$deploy_dir' || exit 90

containers=\$(docker compose -f 'stacks/$stack/docker-compose.yml' --project-name '$project_name' ps -q 2>/dev/null)
# Fallback: if project-name query finds nothing, retry without it to
# catch containers deployed under a different project name (strut#517)
if [ -z \"\$containers\" ]; then
containers=\$(docker compose -f 'stacks/$stack/docker-compose.yml' ps -q 2>/dev/null)
fi
[ -n \"\$containers\" ] || exit 0

while IFS= read -r cid; do
Expand Down Expand Up @@ -180,6 +185,10 @@ _drift_images_detect() {

local containers
containers=$(docker compose -f "$compose_file" --project-name "$project_name" ps -q 2>/dev/null) || true
# Fallback: if project-name query finds nothing, retry without it (strut#517)
if [ -z "$containers" ]; then
containers=$(docker compose -f "$compose_file" ps -q 2>/dev/null) || true
fi
[ -n "$containers" ] || { log "No running containers for $stack"; return 0; }

local output=""
Expand Down
13 changes: 13 additions & 0 deletions lib/health.sh
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ health_check_containers() {
fi
fi

if [ -z "$rows" ]; then
# Fallback: the project-name query may not match if containers were
# deployed under a different project name (e.g., just the stack name
# or the directory name). Try querying by compose file only, which
# matches any container associated with that file. (strut#502)
local fallback_cmd
fallback_cmd="$(_docker_sudo)docker compose -f $compose_file ps --format json"
raw=$($fallback_cmd 2>/dev/null || true)
rows=$(printf '%s' "$raw" | jq -rs '
(if (length == 1 and (.[0] | type) == "array") then .[0] else . end)
| .[] | "\(.Name)|\(.State)|\(.Health // "")"' 2>/dev/null || true)
fi

if [ -z "$rows" ]; then
_health_record fail "Containers" "No containers running"
return 1
Expand Down
26 changes: 18 additions & 8 deletions lib/mcp/tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ _mcp_tools_list() {
{"name":"strut_backup_health","description":"Show backup health scores for a stack","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"}},"required":["stack"]}},
{"name":"strut_briefing","description":"One-call operational situation report: aggregates health, config drift, image staleness, pending diff, and backup health into an overall posture plus prioritized actions","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"env":{"type":"string","description":"Environment name (default: prod)"}},"required":["stack"]}},
{"name":"strut_preflight","description":"Deploy go/no-go verdict (GO/CAUTION/NO-GO): fuses pending diff, config drift, current health, and backup freshness into a release-safety decision with reasons","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"env":{"type":"string","description":"Environment name (default: prod)"}},"required":["stack"]}},
{"name":"strut_deploy","description":"Deploy a stack to its VPS (requires approval). Runs the full pipeline on the host the stack maps to: sync repo, run migrations, pull images, restart services, health-check, and auto-roll-back if unhealthy. Fails if the stack does not resolve to a remote host — it will never fall back to a local deploy.","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"env":{"type":"string","description":"Environment name (default: prod)"}},"required":["stack"]}},
{"name":"strut_deploy","description":"Deploy a stack to its VPS. Without confirm:true, runs in dry-run mode showing the execution plan. Pass confirm:true to execute the full pipeline: sync repo, run migrations, pull images, restart services, health-check, and auto-roll-back if unhealthy. Fails if the stack does not resolve to a remote host.","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"env":{"type":"string","description":"Environment name (default: prod)"},"confirm":{"type":"boolean","description":"Set to true to actually execute the deploy. Omit or false for a dry-run preview."}},"required":["stack"]}},
{"name":"strut_sync","description":"Bring a host checkout in sync with origin","inputSchema":{"type":"object","properties":{"host":{"type":"string","description":"Host alias from topology"}},"required":["host"]}},
{"name":"strut_backup","description":"Create a backup for a stack","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"target":{"type":"string","description":"Backup target (postgres, neo4j, mysql, sqlite, all). Default: all"}},"required":["stack"]}},
{"name":"strut_stop","description":"Stop containers for a stack (requires approval)","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"}},"required":["stack"]}}
{"name":"strut_stop","description":"Stop containers for a stack. Without confirm:true, runs in dry-run mode showing what would be stopped. Pass confirm:true to actually stop and remove containers.","inputSchema":{"type":"object","properties":{"stack":{"type":"string","description":"Stack name"},"confirm":{"type":"boolean","description":"Set to true to actually stop the stack. Omit or false for a dry-run preview."}},"required":["stack"]}}
]}
EOF
}
Expand Down Expand Up @@ -179,12 +179,16 @@ _mcp_tools_call() {
output=$("$strut_bin" "$stack" preflight --env "$env" --json 2>&1) || rc=$?
;;
strut_deploy)
local stack env
local stack env confirm
stack=$(_mcp_arg "$args" stack) || { _mcp_reject "invalid 'stack' argument"; return 0; }
env=$(_mcp_arg "$args" env prod) || { _mcp_reject "invalid 'env' argument"; return 0; }
# --require-remote, not a bare `deploy`: an agent calling this must never
# get a silent local deploy because the stack's VPS_HOST didn't resolve.
output=$("$strut_bin" "$stack" deploy --require-remote --env "$env" 2>&1) || rc=$?
confirm=$(printf '%s' "$args" | jq -r '.confirm // false')
# Default to dry-run unless explicitly confirmed (strut#516)
if [ "$confirm" = "true" ]; then
output=$("$strut_bin" "$stack" deploy --require-remote --env "$env" 2>&1) || rc=$?
else
output=$("$strut_bin" "$stack" deploy --require-remote --env "$env" --dry-run 2>&1) || rc=$?
fi
;;
strut_sync)
local host
Expand All @@ -198,9 +202,15 @@ _mcp_tools_call() {
output=$("$strut_bin" "$stack" backup "$target" --env prod 2>&1) || rc=$?
;;
strut_stop)
local stack
local stack confirm
stack=$(_mcp_arg "$args" stack) || { _mcp_reject "invalid 'stack' argument"; return 0; }
output=$("$strut_bin" "$stack" stop --env prod 2>&1) || rc=$?
confirm=$(printf '%s' "$args" | jq -r '.confirm // false')
# Default to dry-run unless explicitly confirmed (strut#516)
if [ "$confirm" = "true" ]; then
output=$("$strut_bin" "$stack" stop --env prod 2>&1) || rc=$?
else
output=$("$strut_bin" "$stack" stop --env prod --dry-run 2>&1) || rc=$?
fi
;;
*)
printf '{"content":[{"type":"text","text":"Unknown tool: %s"}],"isError":true}' "$tool"
Expand Down
Loading