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
85 changes: 85 additions & 0 deletions setup-modules/migrations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -963,3 +963,88 @@ migrate_pulse_repos_to_repos_json() {

return 0
}

# Migrate orphaned supervisor-helper.sh and supervisor/ modules (GH#5147)
# After the supervisor-to-pulse-wrapper migration (PR #2291, PR #2475), the
# upstream repo moved supervisor files to supervisor-archived/. But aidevops
# update (rsync) only adds/overwrites — it doesn't delete files that no longer
# exist in the source. Users who installed before the migration retain:
# - ~/.aidevops/agents/scripts/supervisor-helper.sh (old entry point)
# - ~/.aidevops/agents/scripts/supervisor/ (old module directory)
# - cron/launchd entries invoking supervisor-helper.sh pulse
# These orphaned files shadow the new pulse-wrapper.sh architecture.
# This migration removes the orphaned files and rewrites scheduler entries.
migrate_orphaned_supervisor() {
local agents_dir="$HOME/.aidevops/agents"
local scripts_dir="$agents_dir/scripts"
local cleaned=0

# 1. Remove orphaned supervisor-helper.sh from deployed scripts
# The canonical location is now supervisor-archived/supervisor-helper.sh
# (shipped for reference/tests only, not as an active entry point)
if [[ -f "$scripts_dir/supervisor-helper.sh" ]]; then
rm -f "$scripts_dir/supervisor-helper.sh"
print_info "Removed orphaned supervisor-helper.sh from deployed scripts"
((++cleaned))
fi

# 2. Remove orphaned supervisor/ module directory
# The canonical location is now supervisor-archived/ (shipped by rsync)
# Only remove if it's the old modules dir (contains pulse.sh, dispatch.sh, etc.)
# Do NOT remove supervisor-archived/ — that's the intentional archive
if [[ -d "$scripts_dir/supervisor" && ! -L "$scripts_dir/supervisor" ]]; then
# Verify it's the old module directory (not something user-created)
if [[ -f "$scripts_dir/supervisor/pulse.sh" ]] ||
[[ -f "$scripts_dir/supervisor/dispatch.sh" ]] ||
[[ -f "$scripts_dir/supervisor/_common.sh" ]]; then
rm -rf "$scripts_dir/supervisor"
print_info "Removed orphaned supervisor/ module directory from deployed scripts"
((++cleaned))
fi
fi

# 3. Migrate cron entries from supervisor-helper.sh to pulse-wrapper.sh
# Old pattern: */2 * * * * ... supervisor-helper.sh pulse ...
# New pattern: already installed by setup.sh's pulse section
# Strategy: remove old entries; setup.sh will install the new one if pulse is enabled
local current_crontab
current_crontab=$(crontab -l 2>/dev/null) || current_crontab=""
if echo "$current_crontab" | grep -qF "supervisor-helper.sh"; then
# Remove all cron lines referencing supervisor-helper.sh
local new_crontab
new_crontab=$(echo "$current_crontab" | grep -v "supervisor-helper.sh")
if [[ -n "$new_crontab" ]]; then
printf '%s\n' "$new_crontab" | crontab - 2>/dev/null || true
else
# All entries were supervisor-helper.sh — remove crontab entirely
crontab -r 2>/dev/null || true
fi
print_info "Removed orphaned supervisor-helper.sh cron entries"
print_info " pulse-wrapper.sh will be installed by setup.sh if supervisor pulse is enabled"
((++cleaned))
fi

# 4. Migrate launchd entries from old supervisor label (macOS only)
# Old label: com.aidevops.supervisor-pulse (from cron.sh/launchd.sh)
# New label: com.aidevops.aidevops-supervisor-pulse (from setup.sh)
# setup.sh already handles the new label cleanup at line ~1000, but
# the old label from cron.sh may also be present
if [[ "$(uname -s)" == "Darwin" ]]; then
local old_label="com.aidevops.supervisor-pulse"
local old_plist="$HOME/Library/LaunchAgents/${old_label}.plist"
if [[ -f "$old_plist" ]] || _launchd_has_agent "$old_label" 2>/dev/null; then
# Use launchctl remove by label — works even when the plist file is
# missing (orphaned agent loaded without a backing file on disk)
launchctl remove "$old_label" 2>/dev/null || true
rm -f "$old_plist"
print_info "Removed orphaned supervisor-pulse LaunchAgent ($old_label)"
((++cleaned))
fi
Comment on lines +1035 to +1042
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a potential issue in the launchd cleanup logic. If an agent is loaded but its corresponding .plist file is missing (an orphaned agent), launchctl unload "$old_plist" will fail because the path doesn't exist, and the agent won't be removed.

A more robust approach is to use launchctl remove "$old_label", which unloads and removes the agent by its label, working correctly even if the .plist file is gone. This ensures that orphaned agents are properly cleaned up. I've removed 2>/dev/null from the launchctl remove command and the _launchd_has_agent check in the suggestion to avoid blanket suppression of errors, allowing potential issues to be visible for debugging, as per repository guidelines.

Suggested change
if [[ -f "$old_plist" ]] || _launchd_has_agent "$old_label" 2>/dev/null; then
launchctl unload "$old_plist" 2>/dev/null || true
rm -f "$old_plist"
print_info "Removed orphaned supervisor-pulse LaunchAgent ($old_label)"
((++cleaned))
fi
if _launchd_has_agent "$old_label" || [[ -f "$old_plist" ]]; then
# Use `remove` by label, which is more robust as it works even if the plist file is missing.
launchctl remove "$old_label" || true
# Also remove the plist file if it exists.
rm -f "$old_plist"
print_info "Removed orphaned supervisor-pulse LaunchAgent ($old_label)"
((++cleaned))
fi
References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

fi

if [[ $cleaned -gt 0 ]]; then
print_success "Cleaned up $cleaned orphaned supervisor artifact(s) — pulse-wrapper.sh is the active system"
fi

return 0
}
2 changes: 2 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ main() {
migrate_mcp_env_to_credentials
migrate_pulse_repos_to_repos_json
cleanup_deprecated_paths
migrate_orphaned_supervisor
cleanup_deprecated_mcps
cleanup_stale_bun_opencode
validate_opencode_config
Expand Down Expand Up @@ -795,6 +796,7 @@ main() {
confirm_step "Migrate mcp-env.sh -> credentials.sh" && migrate_mcp_env_to_credentials
confirm_step "Migrate pulse-repos.json into repos.json" && migrate_pulse_repos_to_repos_json
confirm_step "Cleanup deprecated agent paths" && cleanup_deprecated_paths
confirm_step "Migrate orphaned supervisor to pulse-wrapper" && migrate_orphaned_supervisor
confirm_step "Cleanup deprecated MCP entries (hetzner, serper, etc.)" && cleanup_deprecated_mcps
confirm_step "Cleanup stale bun opencode install" && cleanup_stale_bun_opencode
confirm_step "Validate and repair OpenCode config schema" && validate_opencode_config
Expand Down
235 changes: 235 additions & 0 deletions tests/test-migrate-orphaned-supervisor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#!/usr/bin/env bash
# Tests for migrate_orphaned_supervisor() (GH#5147)
# Verifies cleanup of orphaned supervisor-helper.sh and supervisor/ modules

set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MIGRATIONS_SCRIPT="$REPO_DIR/setup-modules/migrations.sh"

PASS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0

pass() {
local name="$1"
PASS_COUNT=$((PASS_COUNT + 1))
TOTAL_COUNT=$((TOTAL_COUNT + 1))
printf "\033[0;32mPASS\033[0m %s\n" "$name"
return 0
}

fail() {
local name="$1"
local detail="${2:-}"
FAIL_COUNT=$((FAIL_COUNT + 1))
TOTAL_COUNT=$((TOTAL_COUNT + 1))
printf "\033[0;31mFAIL\033[0m %s\n" "$name"
if [[ -n "$detail" ]]; then
printf " %s\n" "$detail"
fi
return 0
}

section() {
echo ""
echo "=== $1 ==="
return 0
}

# Create a temporary HOME to isolate tests from the real system
TEST_HOME=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$TEST_HOME'" EXIT

# Override HOME for the duration of the test
export HOME="$TEST_HOME"

# Create the deployed agents directory structure (simulating pre-migration state)
AGENTS_DIR="$TEST_HOME/.aidevops/agents"
SCRIPTS_DIR="$AGENTS_DIR/scripts"
mkdir -p "$SCRIPTS_DIR"

# Stub functions that migrations.sh expects from setup.sh
print_info() { echo "[INFO] $1"; }
print_success() { echo "[SUCCESS] $1"; }
print_warning() { echo "[WARNING] $1"; }
print_error() { echo "[ERROR] $1"; }
# _launchd_has_agent is defined in setup.sh; stub it for Linux tests
_launchd_has_agent() { return 1; }
# Stubs for other functions referenced by migrations.sh
find_opencode_config() { return 1; }
create_backup_with_rotation() { return 0; }
should_overwrite_user_file() { return 0; }
resolve_mcp_binary_path() {
echo ""
return 1
}
update_mcp_paths_in_opencode() { return 0; }
sanitize_plugin_namespace() {
echo "$1"
return 0
}
# Export stubs so sourced script can find them
export -f print_info print_success print_warning print_error _launchd_has_agent
export -f find_opencode_config create_backup_with_rotation should_overwrite_user_file
export -f resolve_mcp_binary_path update_mcp_paths_in_opencode sanitize_plugin_namespace

# Source the migrations script to get the function
# shellcheck disable=SC1090
source "$MIGRATIONS_SCRIPT"

# ============================================================================
section "Test: removes orphaned supervisor-helper.sh"
# ============================================================================

# Create orphaned file
echo '#!/usr/bin/env bash' >"$SCRIPTS_DIR/supervisor-helper.sh"

if [[ -f "$SCRIPTS_DIR/supervisor-helper.sh" ]]; then
pass "Setup: orphaned supervisor-helper.sh exists before migration"
else
fail "Setup: orphaned supervisor-helper.sh should exist"
fi

migrate_orphaned_supervisor >/dev/null 2>&1

if [[ ! -f "$SCRIPTS_DIR/supervisor-helper.sh" ]]; then
pass "supervisor-helper.sh removed after migration"
else
fail "supervisor-helper.sh should have been removed"
fi

# ============================================================================
section "Test: removes orphaned supervisor/ module directory"
# ============================================================================

# Create orphaned module directory with recognizable files
mkdir -p "$SCRIPTS_DIR/supervisor"
echo '#!/usr/bin/env bash' >"$SCRIPTS_DIR/supervisor/pulse.sh"
echo '#!/usr/bin/env bash' >"$SCRIPTS_DIR/supervisor/dispatch.sh"
echo '#!/usr/bin/env bash' >"$SCRIPTS_DIR/supervisor/_common.sh"

if [[ -d "$SCRIPTS_DIR/supervisor" ]]; then
pass "Setup: orphaned supervisor/ directory exists before migration"
else
fail "Setup: orphaned supervisor/ directory should exist"
fi

migrate_orphaned_supervisor >/dev/null 2>&1

if [[ ! -d "$SCRIPTS_DIR/supervisor" ]]; then
pass "supervisor/ directory removed after migration"
else
fail "supervisor/ directory should have been removed"
fi

# ============================================================================
section "Test: does NOT remove supervisor-archived/"
# ============================================================================

mkdir -p "$SCRIPTS_DIR/supervisor-archived"
echo '#!/usr/bin/env bash' >"$SCRIPTS_DIR/supervisor-archived/supervisor-helper.sh"

migrate_orphaned_supervisor >/dev/null 2>&1

if [[ -d "$SCRIPTS_DIR/supervisor-archived" ]]; then
pass "supervisor-archived/ preserved after migration"
else
fail "supervisor-archived/ should NOT have been removed"
fi

# ============================================================================
section "Test: does NOT remove unrelated supervisor/ directory"
# ============================================================================

# Create a supervisor/ directory without the expected module files
mkdir -p "$SCRIPTS_DIR/supervisor"
echo 'custom content' >"$SCRIPTS_DIR/supervisor/my-custom-file.txt"

migrate_orphaned_supervisor >/dev/null 2>&1

if [[ -d "$SCRIPTS_DIR/supervisor" ]]; then
pass "Unrelated supervisor/ directory preserved (no module files)"
else
fail "Unrelated supervisor/ directory should NOT have been removed"
fi

# Clean up for next test
rm -rf "$SCRIPTS_DIR/supervisor"

# ============================================================================
section "Test: idempotent — no errors when nothing to clean"
# ============================================================================

# Run migration when there's nothing to clean
output=$(migrate_orphaned_supervisor 2>&1)
exit_code=$?

if [[ $exit_code -eq 0 ]]; then
pass "Migration succeeds when nothing to clean (exit 0)"
else
fail "Migration should succeed when nothing to clean" "exit code: $exit_code"
fi

# Should NOT print the success message when nothing was cleaned
if [[ "$output" != *"Cleaned up"* ]]; then
pass "No 'Cleaned up' message when nothing to clean"
else
fail "Should not print 'Cleaned up' when nothing was cleaned" "$output"
fi

# ============================================================================
section "Test: removes cron entries referencing supervisor-helper.sh"
# ============================================================================

# Install a fake cron entry (only if crontab is available)
if command -v crontab &>/dev/null; then
# Save current crontab
original_crontab=$(crontab -l 2>/dev/null) || original_crontab=""

# Install test crontab with supervisor-helper.sh entry
{
echo "# aidevops: test-keep-this"
echo "*/5 * * * * echo keep-this # aidevops: test-keep"
echo "*/2 * * * * /bin/bash /home/test/.aidevops/agents/scripts/supervisor-helper.sh pulse >> /tmp/test.log 2>&1 # aidevops: supervisor-pulse"
} | crontab - 2>/dev/null

migrate_orphaned_supervisor >/dev/null 2>&1

new_crontab=$(crontab -l 2>/dev/null) || new_crontab=""

if echo "$new_crontab" | grep -qF "supervisor-helper.sh"; then
fail "Cron entry referencing supervisor-helper.sh should have been removed"
else
pass "Cron entry referencing supervisor-helper.sh removed"
fi

if echo "$new_crontab" | grep -qF "keep-this"; then
pass "Non-supervisor cron entries preserved"
else
fail "Non-supervisor cron entries should have been preserved"
fi

# Restore original crontab
if [[ -n "$original_crontab" ]]; then
printf '%s\n' "$original_crontab" | crontab - 2>/dev/null || true
else
crontab -r 2>/dev/null || true
fi
else
pass "Cron test skipped (crontab not available)"
fi

# ============================================================================
# Summary
# ============================================================================
echo ""
echo "================================"
echo "Results: $PASS_COUNT passed, $FAIL_COUNT failed, $TOTAL_COUNT total"
echo "================================"

if [[ $FAIL_COUNT -gt 0 ]]; then
exit 1
fi
exit 0
Loading