From f658ba4e4ddda5bcfd569632961710ac943b168a Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Wed, 6 May 2026 14:07:46 -0500 Subject: [PATCH 1/4] Stabilize Lume launchd controller runtime --- README.md | 7 + scripts/lume/install-launch-agent.sh | 20 +- scripts/lume/install-runtime.sh | 184 ++++++++++++++++++ scripts/lume/install-system-launch-daemons.sh | 41 +++- scripts/lume/reconcile-pool.sh | 24 +++ test/lume-scripts.test.ts | 25 ++- 6 files changed, 292 insertions(+), 9 deletions(-) create mode 100755 scripts/lume/install-runtime.sh diff --git a/README.md b/README.md index c0d8802..aadf38b 100644 --- a/README.md +++ b/README.md @@ -339,10 +339,17 @@ bash scripts/lume/create-base-vm.sh --config config/lume-runners.yaml --env .env bash scripts/lume/setup-base-vm.sh --config config/lume-runners.yaml --env .env bash scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env .env bash scripts/lume/status.sh --config config/lume-runners.yaml --env .env +bash scripts/lume/install-runtime.sh +bash scripts/lume/install-launch-agent.sh +sudo bash scripts/lume/install-system-launch-daemons.sh --disable-user-lume-agent ``` Keep the Lume runner env file outside git and locked down with `chmod 600`. The host controller reads that file and copies it into each guest VM just before starting the guest bootstrap. Do not bake GitHub credentials into the base VM image. If you want the macOS/base-image pipeline to stay pinned to a specific GitHub Actions runner build, set `pool.runnerVersion` in `config/lume-runners.yaml`; otherwise it falls back to `RUNNER_VERSION` from the env file. +The launchd installers publish a source-independent controller runtime under `~/Library/Application Support/github-runner-fleet/controller/current` and point the Lume pool job at that path. Runtime `.env` remains beside that controller at `~/Library/Application Support/github-runner-fleet/controller/.env`, is mode `0600`, and is preserved when the source checkout moves or the installer is rerun. Override the runtime root with `GITHUB_RUNNER_FLEET_RUNTIME_ROOT` if this Mac needs a different stable location. + +If launchd reports `Bootstrap failed: 5: Input/output error`, check the disabled override first with `launchctl print-disabled system | rg github-runner-fleet` or `launchctl print-disabled gui/$(id -u) | rg github-runner-fleet`. The installers clear their own disabled overrides before bootstrapping; the system installer only disables the per-user Lume jobs after the root services load successfully. + `create-base-vm.sh` now caches the macOS IPSW under `LUME_RUNNER_BASE_DIR/cache/` by default so rebuilding the base image does not re-download the restore image every time. Override that path with `LUME_RUNNER_IPSW_PATH` if you want the cache elsewhere. If unattended setup drifts or gets interrupted, rerun `scripts/lume/setup-base-vm.sh` against the existing base VM instead of deleting and recreating it. ## Security Notes diff --git a/scripts/lume/install-launch-agent.sh b/scripts/lume/install-launch-agent.sh index 34182cf..90f457f 100755 --- a/scripts/lume/install-launch-agent.sh +++ b/scripts/lume/install-launch-agent.sh @@ -3,6 +3,7 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +source "${SCRIPT_DIR}/install-runtime.sh" LAUNCH_AGENT_LABEL="com.omt.github-runner-fleet.lume-pool" LAUNCH_AGENTS_DIR="${HOME}/Library/LaunchAgents" @@ -12,6 +13,8 @@ STDOUT_PATH="${LOG_DIR}/lume-pool.stdout.log" STDERR_PATH="${LOG_DIR}/lume-pool.stderr.log" DOMAIN_TARGET="gui/$(id -u)" +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${HOME}/.local/bin:${PATH:-}" + usage() { cat <${rtk_path} bash -lc - cd '${REPO_ROOT}' && exec bash scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env .env + cd '${runtime_repo}' && exec bash scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env '${runtime_env}' WorkingDirectory - ${REPO_ROOT} + ${runtime_repo} RunAtLoad KeepAlive @@ -75,11 +80,14 @@ write_plist() { EOF plutil -lint "${temp_path}" >/dev/null - mv "${temp_path}" "${PLIST_PATH}" + install -m 0644 "${temp_path}" "${PLIST_PATH}" + rm -f "${temp_path}" } main() { local rtk_path + local runtime_repo + local runtime_env if [[ $# -gt 0 ]]; then case "$1" in @@ -100,11 +108,15 @@ main() { require_command rtk rtk_path="$(command -v rtk)" + install_lume_controller_runtime "${HOME}" + runtime_repo="$(lume_controller_runtime_repo "${HOME}")" + runtime_env="$(lume_controller_runtime_env "${HOME}")" mkdir -p "${LAUNCH_AGENTS_DIR}" "${LOG_DIR}" - write_plist "${rtk_path}" + write_plist "${rtk_path}" "${runtime_repo}" "${runtime_env}" launchctl bootout "${DOMAIN_TARGET}" "${PLIST_PATH}" >/dev/null 2>&1 || true + launchctl enable "${DOMAIN_TARGET}/${LAUNCH_AGENT_LABEL}" launchctl bootstrap "${DOMAIN_TARGET}" "${PLIST_PATH}" launchctl enable "${DOMAIN_TARGET}/${LAUNCH_AGENT_LABEL}" launchctl kickstart -k "${DOMAIN_TARGET}/${LAUNCH_AGENT_LABEL}" diff --git a/scripts/lume/install-runtime.sh b/scripts/lume/install-runtime.sh new file mode 100755 index 0000000..166d0aa --- /dev/null +++ b/scripts/lume/install-runtime.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +usage() { + cat </dev/null 2>&1; then + echo "missing required command: ${command_name}" >&2 + exit 1 + fi +} + +lume_controller_runtime_root() { + local target_home="$1" + + if [[ -n "${GITHUB_RUNNER_FLEET_RUNTIME_ROOT:-}" ]]; then + printf '%s\n' "${GITHUB_RUNNER_FLEET_RUNTIME_ROOT}" + return 0 + fi + + printf '%s/Library/Application Support/github-runner-fleet/controller\n' "${target_home}" +} + +lume_controller_runtime_repo() { + local target_home="$1" + printf '%s/current\n' "$(lume_controller_runtime_root "${target_home}")" +} + +lume_controller_runtime_env() { + local target_home="$1" + printf '%s/.env\n' "$(lume_controller_runtime_root "${target_home}")" +} + +write_default_runtime_env() { + local env_path="$1" + local target_home="$2" + local lume_base_dir="${target_home}/Library/Application Support/github-runner-fleet/lume" + local temp_path + + temp_path="$(mktemp)" + cat > "${temp_path}" <&2 + echo "unknown argument: $1" >&2 + return 1 + ;; + esac + done + + install_lume_controller_runtime "${target_home}" "${target_user}" "${target_group}" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/scripts/lume/install-system-launch-daemons.sh b/scripts/lume/install-system-launch-daemons.sh index a02e767..a2dfc3b 100755 --- a/scripts/lume/install-system-launch-daemons.sh +++ b/scripts/lume/install-system-launch-daemons.sh @@ -3,10 +3,12 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +source "${SCRIPT_DIR}/install-runtime.sh" TARGET_USER="${SUDO_USER:-$(stat -f '%Su' /dev/console)}" TARGET_HOME="$(dscl . -read "/Users/${TARGET_USER}" NFSHomeDirectory | awk '{print $2}')" TARGET_GROUP="$(id -gn "${TARGET_USER}")" +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${TARGET_HOME}/.local/bin:${PATH:-}" LUME_LABEL="com.omt.github-runner-fleet.lume-serve" POOL_LABEL="com.omt.github-runner-fleet.lume-pool.system" DAEMON_DIR="/Library/LaunchDaemons" @@ -14,7 +16,10 @@ LOG_DIR="${TARGET_HOME}/Library/Logs/github-runner-fleet" LUME_PLIST_PATH="${DAEMON_DIR}/${LUME_LABEL}.plist" POOL_PLIST_PATH="${DAEMON_DIR}/${POOL_LABEL}.plist" USER_LUME_AGENT_PATH="${TARGET_HOME}/Library/LaunchAgents/com.trycua.lume_daemon.plist" +USER_POOL_AGENT_LABEL="com.omt.github-runner-fleet.lume-pool" +USER_POOL_AGENT_PATH="${TARGET_HOME}/Library/LaunchAgents/${USER_POOL_AGENT_LABEL}.plist" DISABLE_USER_LUME_AGENT="false" +DISABLE_USER_POOL_AGENT="true" usage() { cat <&2 echo "unknown argument: $1" >&2 @@ -119,6 +129,8 @@ EOF write_pool_plist() { local rtk_path="$1" + local runtime_repo="$2" + local runtime_env="$3" local temp_path temp_path="$(mktemp)" @@ -138,10 +150,10 @@ write_pool_plist() { ${rtk_path} bash -lc - cd '${REPO_ROOT}' && exec bash scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env .env + cd '${runtime_repo}' && exec bash scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env '${runtime_env}' WorkingDirectory - ${REPO_ROOT} + ${runtime_repo} RunAtLoad KeepAlive @@ -177,11 +189,26 @@ disable_user_lume_agent() { launchctl disable "gui/${uid}/com.trycua.lume_daemon" >/dev/null 2>&1 || true } +disable_user_pool_agent() { + local uid + + uid="$(id -u "${TARGET_USER}")" + if [[ "${DISABLE_USER_POOL_AGENT}" != "true" ]]; then + return 0 + fi + + if [[ -f "${USER_POOL_AGENT_PATH}" ]]; then + launchctl bootout "gui/${uid}" "${USER_POOL_AGENT_PATH}" >/dev/null 2>&1 || true + fi + launchctl disable "gui/${uid}/${USER_POOL_AGENT_LABEL}" >/dev/null 2>&1 || true +} + bootstrap_daemon() { local label="$1" local plist_path="$2" launchctl bootout system "${plist_path}" >/dev/null 2>&1 || true + launchctl enable "system/${label}" launchctl bootstrap system "${plist_path}" launchctl enable "system/${label}" launchctl kickstart -k "system/${label}" @@ -190,6 +217,8 @@ bootstrap_daemon() { main() { local lume_path local rtk_path + local runtime_repo + local runtime_env require_command dscl require_command id @@ -209,12 +238,16 @@ main() { mkdir -p "${DAEMON_DIR}" "${LOG_DIR}" chown "${TARGET_USER}:${TARGET_GROUP}" "${LOG_DIR}" + install_lume_controller_runtime "${TARGET_HOME}" "${TARGET_USER}" "${TARGET_GROUP}" + runtime_repo="$(lume_controller_runtime_repo "${TARGET_HOME}")" + runtime_env="$(lume_controller_runtime_env "${TARGET_HOME}")" write_lume_plist "${lume_path}" - write_pool_plist "${rtk_path}" - disable_user_lume_agent + write_pool_plist "${rtk_path}" "${runtime_repo}" "${runtime_env}" bootstrap_daemon "${LUME_LABEL}" "${LUME_PLIST_PATH}" bootstrap_daemon "${POOL_LABEL}" "${POOL_PLIST_PATH}" + disable_user_lume_agent + disable_user_pool_agent printf 'installed %s at %s\n' "${LUME_LABEL}" "${LUME_PLIST_PATH}" printf 'installed %s at %s\n' "${POOL_LABEL}" "${POOL_PLIST_PATH}" diff --git a/scripts/lume/reconcile-pool.sh b/scripts/lume/reconcile-pool.sh index 21f9cf4..57fd047 100755 --- a/scripts/lume/reconcile-pool.sh +++ b/scripts/lume/reconcile-pool.sh @@ -42,6 +42,29 @@ while [[ $# -gt 0 ]]; do esac done +wait_for_registration_env() { + local env_file="$1" + + while true; do + if ( + set +u + if [[ -f "${env_file}" ]]; then + set -a + # shellcheck disable=SC1090 + source "${env_file}" + set +a + fi + + [[ -n "${GITHUB_PAT:-}" ]] + ); then + return 0 + fi + + log "missing GITHUB_PAT in ${env_file}; waiting before starting Lume slots" + sleep 60 + done +} + retire_removed_slots_from_state() { local state_file="$1" local current_pool_size="$2" @@ -181,6 +204,7 @@ fs.renameSync(tempFile, process.env.STATE_FILE); NODE } +wait_for_registration_env "${env_path}" pool_size="$(load_pool_size "${config_path}" "${env_path}")" load_slot_env "1" "${config_path}" "${env_path}" reconcile_state_file="${LUME_RECONCILE_STATE_FILE}" diff --git a/test/lume-scripts.test.ts b/test/lume-scripts.test.ts index d759700..dee67ac 100644 --- a/test/lume-scripts.test.ts +++ b/test/lume-scripts.test.ts @@ -11,6 +11,7 @@ describe("Lume pool scripts", () => { const createBase = read("scripts/lume/create-base-vm.sh"); const setupBase = read("scripts/lume/setup-base-vm.sh"); const provisionBase = read("scripts/lume/provision-base-vm.sh"); + const installRuntime = read("scripts/lume/install-runtime.sh"); const installLaunchAgent = read("scripts/lume/install-launch-agent.sh"); const installLaunchDaemons = read("scripts/lume/install-system-launch-daemons.sh"); @@ -24,6 +25,8 @@ describe("Lume pool scripts", () => { expect(runSlot).toContain('guest_env_file="$(render_guest_runner_env "${env_path}")"'); expect(runSlot).toContain('lume ssh "${LUME_VM_NAME}"'); expect(reconcile).toContain("retire_removed_slots_from_state"); + expect(reconcile).toContain("wait_for_registration_env"); + expect(reconcile).toContain("missing GITHUB_PAT"); expect(reconcile).toContain("write_reconcile_state"); expect(reconcile).toContain('reconcile_state_file="${LUME_RECONCILE_STATE_FILE}"'); expect(reconcile).toContain('spawn_detached'); @@ -37,14 +40,33 @@ describe("Lume pool scripts", () => { expect(provisionBase).toContain("tar -C"); expect(provisionBase).toContain("sudo -S -p '' tar -xf"); expect(provisionBase).toContain("sudo -S -p '' xcodebuild -runFirstLaunch"); + expect(installRuntime).toContain("GITHUB_RUNNER_FLEET_RUNTIME_ROOT"); + expect(installRuntime).toContain("Library/Application Support/github-runner-fleet/controller"); + expect(installRuntime).toContain("rsync -a --delete"); + expect(installRuntime).toContain("pnpm --dir"); + expect(installRuntime).toContain("install_lume_controller_runtime"); expect(installLaunchAgent).toContain('com.omt.github-runner-fleet.lume-pool'); - expect(installLaunchAgent).toContain('scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env .env'); + expect(installLaunchAgent).toContain('source "${SCRIPT_DIR}/install-runtime.sh"'); + expect(installLaunchAgent).toContain('install_lume_controller_runtime "${HOME}"'); + expect(installLaunchAgent).toContain('scripts/lume/reconcile-pool.sh --config config/lume-runners.yaml --env \'${runtime_env}\''); expect(installLaunchAgent).toContain('launchctl bootstrap "${DOMAIN_TARGET}" "${PLIST_PATH}"'); + expect(installLaunchAgent.indexOf('launchctl enable "${DOMAIN_TARGET}/${LAUNCH_AGENT_LABEL}"')).toBeLessThan( + installLaunchAgent.indexOf('launchctl bootstrap "${DOMAIN_TARGET}" "${PLIST_PATH}"'), + ); expect(installLaunchDaemons).toContain('run as root: sudo $0'); expect(installLaunchDaemons).toContain('/Library/LaunchDaemons'); expect(installLaunchDaemons).toContain('com.omt.github-runner-fleet.lume-serve'); expect(installLaunchDaemons).toContain('com.omt.github-runner-fleet.lume-pool.system'); + expect(installLaunchDaemons).toContain('disable_user_pool_agent'); + expect(installLaunchDaemons).toContain('install_lume_controller_runtime "${TARGET_HOME}" "${TARGET_USER}" "${TARGET_GROUP}"'); expect(installLaunchDaemons).toContain('launchctl bootstrap system "${plist_path}"'); + expect(installLaunchDaemons.indexOf('launchctl enable "system/${label}"')).toBeLessThan( + installLaunchDaemons.indexOf('launchctl bootstrap system "${plist_path}"'), + ); + const installLaunchDaemonsMain = installLaunchDaemons.slice(installLaunchDaemons.indexOf("main() {")); + expect(installLaunchDaemonsMain.indexOf('bootstrap_daemon "${POOL_LABEL}" "${POOL_PLIST_PATH}"')).toBeLessThan( + installLaunchDaemonsMain.indexOf("disable_user_pool_agent"), + ); }); test("documents operator-facing lume script usage", () => { @@ -52,6 +74,7 @@ describe("Lume pool scripts", () => { "scripts/lume/create-base-vm.sh", "scripts/lume/create-slot.sh", "scripts/lume/destroy-slot.sh", + "scripts/lume/install-runtime.sh", "scripts/lume/install-launch-agent.sh", "scripts/lume/install-system-launch-daemons.sh", "scripts/lume/provision-base-vm.sh", From b8c2c31cc095183d4e20a3cf2675b739941a0f0e Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Wed, 6 May 2026 14:17:37 -0500 Subject: [PATCH 2/4] Cover current runner version summary --- test/runner-version.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/runner-version.test.ts b/test/runner-version.test.ts index 28a03ec..e88684d 100644 --- a/test/runner-version.test.ts +++ b/test/runner-version.test.ts @@ -29,4 +29,12 @@ describe("runner version helpers", () => { outdated: true }); }); + + test("keeps current runner versions marked up to date", () => { + expect(summarizeRunnerVersion("v2.327.1", "2.327.1")).toEqual({ + current: "2.327.1", + latest: "2.327.1", + outdated: false + }); + }); }); From 7303f95949f813c7937858f7d075f9724df23b39 Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Wed, 6 May 2026 14:21:57 -0500 Subject: [PATCH 3/4] Cover env parser edge cases --- test/env.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/env.test.ts b/test/env.test.ts index 8369e6e..ef22247 100644 --- a/test/env.test.ts +++ b/test/env.test.ts @@ -158,4 +158,54 @@ describe("loadDeploymentEnv", () => { expect(env.synologySecure).toBe(false); expect(env.synologyPort).toBe("5000"); }); + + test("accepts alternate truthy and falsey boolean spellings", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "synology-env-")); + tempPaths.push(directory); + const envPath = path.join(directory, ".env"); + + fs.writeFileSync( + envPath, + "SYNOLOGY_SECURE=on\nSYNOLOGY_CERT_VERIFY=no\n", + "utf8" + ); + + const env = loadDeploymentEnv({ + envPath, + requirePat: false + }); + + expect(env.synologySecure).toBe(true); + expect(env.synologyCertVerify).toBe(false); + }); + + test("rejects malformed boolean settings", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "synology-env-")); + tempPaths.push(directory); + const envPath = path.join(directory, ".env"); + + fs.writeFileSync(envPath, "SYNOLOGY_SECURE=maybe\n", "utf8"); + + expect(() => + loadDeploymentEnv({ + envPath, + requirePat: false + }) + ).toThrow(/invalid boolean value "maybe"/); + }); + + test("rejects malformed integer settings", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "synology-env-")); + tempPaths.push(directory); + const envPath = path.join(directory, ".env"); + + fs.writeFileSync(envPath, "SYNOLOGY_DSM_VERSION=seven\n", "utf8"); + + expect(() => + loadDeploymentEnv({ + envPath, + requirePat: false + }) + ).toThrow(/invalid integer value "seven"/); + }); }); From 8ce287722878cd55d616f0e39e86a0133c307f29 Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Wed, 6 May 2026 18:16:44 -0500 Subject: [PATCH 4/4] Preserve Lume runtime env on reinstall --- scripts/lume/install-runtime.sh | 8 ++++---- test/lume-scripts.test.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/lume/install-runtime.sh b/scripts/lume/install-runtime.sh index 166d0aa..2440e1e 100755 --- a/scripts/lume/install-runtime.sh +++ b/scripts/lume/install-runtime.sh @@ -123,12 +123,12 @@ install_lume_controller_runtime() { --exclude '.env' \ "${REPO_ROOT}/" "${runtime_repo}/" - if [[ -f "${REPO_ROOT}/.env" ]]; then + if [[ -f "${runtime_env}" ]]; then + repair_default_runtime_env "${runtime_env}" "${target_home}" + elif [[ -f "${REPO_ROOT}/.env" ]]; then install -m 0600 "${REPO_ROOT}/.env" "${runtime_env}" - elif [[ ! -f "${runtime_env}" ]]; then - write_default_runtime_env "${runtime_env}" "${target_home}" else - repair_default_runtime_env "${runtime_env}" "${target_home}" + write_default_runtime_env "${runtime_env}" "${target_home}" fi if [[ -n "${target_user}" && -n "${target_group}" && "${EUID}" -eq 0 ]]; then diff --git a/test/lume-scripts.test.ts b/test/lume-scripts.test.ts index dee67ac..00aad97 100644 --- a/test/lume-scripts.test.ts +++ b/test/lume-scripts.test.ts @@ -45,6 +45,10 @@ describe("Lume pool scripts", () => { expect(installRuntime).toContain("rsync -a --delete"); expect(installRuntime).toContain("pnpm --dir"); expect(installRuntime).toContain("install_lume_controller_runtime"); + expect(installRuntime).toContain('if [[ -f "${runtime_env}" ]]'); + expect(installRuntime.indexOf('if [[ -f "${runtime_env}" ]]')).toBeLessThan( + installRuntime.indexOf('install -m 0600 "${REPO_ROOT}/.env" "${runtime_env}"'), + ); expect(installLaunchAgent).toContain('com.omt.github-runner-fleet.lume-pool'); expect(installLaunchAgent).toContain('source "${SCRIPT_DIR}/install-runtime.sh"'); expect(installLaunchAgent).toContain('install_lume_controller_runtime "${HOME}"');