diff --git a/manage_runner.sh b/manage_runner.sh
index 5f11a1e..a9d0fb9 100755
--- a/manage_runner.sh
+++ b/manage_runner.sh
@@ -41,7 +41,7 @@ EOF
# Parse a runner entry from runners.conf (INI format) by section name.
# Sets globals: RUNNER_NAME, RUNNER_HOST, RUNNER_TYPE, RUNNER_DATA_PATH,
# RUNNER_LABELS, RUNNER_DEFAULT_IMAGE, RUNNER_REPOS, RUNNER_CAPACITY,
-# RUNNER_CPU, RUNNER_MEMORY
+# RUNNER_CPU, RUNNER_MEMORY, RUNNER_BOOT
# Also resolves: RUNNER_SSH_HOST, RUNNER_SSH_USER, RUNNER_SSH_PORT,
# RUNNER_SSH_KEY (from .env or custom section keys)
# Returns 1 if not found.
@@ -70,6 +70,10 @@ parse_runner_entry() {
RUNNER_CAPACITY=$(ini_get "$RUNNERS_CONF" "$target_name" "capacity" "${RUNNER_DEFAULT_CAPACITY:-1}")
RUNNER_CPU=$(ini_get "$RUNNERS_CONF" "$target_name" "cpu" "")
RUNNER_MEMORY=$(ini_get "$RUNNERS_CONF" "$target_name" "memory" "")
+ # boot: controls launchd install location for native runners.
+ # "true" → /Library/LaunchDaemons/ (starts at boot, requires sudo)
+ # "false" (default) → ~/Library/LaunchAgents/ (starts at login)
+ RUNNER_BOOT=$(ini_get "$RUNNERS_CONF" "$target_name" "boot" "false")
# --- Host resolution ---
case "$RUNNER_HOST" in
@@ -342,7 +346,17 @@ add_native_runner() {
export RUNNER_DATA_PATH
local plist_name="com.gitea.runner.${RUNNER_NAME}.plist"
- local plist_path="$HOME/Library/LaunchAgents/${plist_name}"
+
+ # Route plist to LaunchDaemons (boot) or LaunchAgents (login) based on boot flag.
+ # LaunchDaemons start at boot before any user logs in — useful for headless Macs.
+ # LaunchAgents start when the user logs in — no elevated privileges needed.
+ local plist_dir
+ if [[ "$RUNNER_BOOT" == "true" ]]; then
+ plist_dir="/Library/LaunchDaemons"
+ else
+ plist_dir="$HOME/Library/LaunchAgents"
+ fi
+ local plist_path="${plist_dir}/${plist_name}"
# Check if launchd service is already loaded
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then
@@ -393,12 +407,28 @@ add_native_runner() {
cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
rm -f "$tmpfile"
- # Render launchd plist
+ # Render launchd plist.
+ # When boot=true, insert a UserName entry so the daemon runs as
+ # the deploying user instead of root (LaunchDaemons default to root).
+ # When boot=false (LaunchAgent), the block is empty — agents run as the user.
+ if [[ "$RUNNER_BOOT" == "true" ]]; then
+ RUNNER_PLIST_USERNAME_BLOCK="$(printf ' UserName\n %s\n' "$(whoami)")"
+ else
+ RUNNER_PLIST_USERNAME_BLOCK=""
+ fi
+ export RUNNER_PLIST_USERNAME_BLOCK
+
tmpfile=$(mktemp)
render_template "${SCRIPT_DIR}/templates/com.gitea.runner.plist.tpl" "$tmpfile" \
- "\${RUNNER_NAME} \${RUNNER_DATA_PATH}"
- mkdir -p "$HOME/Library/LaunchAgents"
- cp "$tmpfile" "$plist_path"
+ "\${RUNNER_NAME} \${RUNNER_DATA_PATH} \${RUNNER_PLIST_USERNAME_BLOCK}"
+ mkdir -p "$plist_dir"
+
+ # LaunchDaemons lives in a system directory — requires sudo to write.
+ if [[ "$RUNNER_BOOT" == "true" ]]; then
+ sudo cp "$tmpfile" "$plist_path"
+ else
+ cp "$tmpfile" "$plist_path"
+ fi
rm -f "$tmpfile"
# Install newsyslog config for log rotation (daily, 5 archives, 50 MB max each).
@@ -414,9 +444,15 @@ add_native_runner() {
log_success "Log rotation installed: $newsyslog_conf"
fi
- # Load the launchd service
- launchctl load "$plist_path"
- log_success "Native runner '${RUNNER_NAME}' loaded via launchd"
+ # Load the launchd service.
+ # LaunchDaemons require sudo to load/unload; LaunchAgents do not.
+ if [[ "$RUNNER_BOOT" == "true" ]]; then
+ sudo launchctl load "$plist_path"
+ log_success "Native runner '${RUNNER_NAME}' loaded via launchd (boot daemon — starts at boot)"
+ else
+ launchctl load "$plist_path"
+ log_success "Native runner '${RUNNER_NAME}' loaded via launchd (login agent — starts at login)"
+ fi
}
# ---------------------------------------------------------------------------
@@ -444,15 +480,34 @@ remove_native_runner() {
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
local plist_name="com.gitea.runner.${RUNNER_NAME}.plist"
- local plist_path="$HOME/Library/LaunchAgents/${plist_name}"
+
+ # The plist may live in either LaunchDaemons (boot=true) or LaunchAgents (boot=false).
+ # Check both directories so removal works regardless of how the runner was originally
+ # deployed — the runner.conf boot flag may have changed since deployment.
+ local plist_path=""
+ local needs_sudo=false
+ if [[ -f "/Library/LaunchDaemons/${plist_name}" ]]; then
+ plist_path="/Library/LaunchDaemons/${plist_name}"
+ needs_sudo=true
+ elif [[ -f "$HOME/Library/LaunchAgents/${plist_name}" ]]; then
+ plist_path="$HOME/Library/LaunchAgents/${plist_name}"
+ fi
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then
- launchctl unload "$plist_path" 2>/dev/null || true
+ if $needs_sudo; then
+ sudo launchctl unload "$plist_path" 2>/dev/null || true
+ else
+ launchctl unload "${plist_path:-$HOME/Library/LaunchAgents/${plist_name}}" 2>/dev/null || true
+ fi
log_success "Launchd service unloaded"
fi
- if [[ -f "$plist_path" ]]; then
- rm -f "$plist_path"
+ if [[ -n "$plist_path" ]] && [[ -f "$plist_path" ]]; then
+ if $needs_sudo; then
+ sudo rm -f "$plist_path"
+ else
+ rm -f "$plist_path"
+ fi
log_success "Plist removed: $plist_path"
fi