diff --git a/README.md b/README.md index 3e4f68c..d78b133 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,8 @@ Although caching is generally recommended, you can disable it by passing `'false oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} use-cache: "false" ``` + +## Usage on persistent self-hosted runners + +When running on self-hosted runners that persist after CI jobs have finished, +the GitHub Action leaves tailscale binaries installed but stops the tailscale background processes. diff --git a/dist/index.js b/dist/index.js index eccc42d..d52b535 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41570,6 +41570,8 @@ async function startTailscaleDaemon(config) { fs.openSync(path.join(os.homedir(), "tailscaled.log"), "w"), ], }); + // Store PID for cleaning up daemon process in logout.ts. + fs.writeFileSync("tailscaled.pid", `${daemon.pid}`); daemon.unref(); // Ensure daemon doesn't keep Node.js process alive // Close stdin/stdout/stderr to fully detach if (daemon.stdin) diff --git a/dist/logout/index.js b/dist/logout/index.js index 16310d4..2e99e69 100644 --- a/dist/logout/index.js +++ b/dist/logout/index.js @@ -25686,10 +25686,13 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); const core = __importStar(__nccwpck_require__(7484)); const exec = __importStar(__nccwpck_require__(5236)); +const fs = __importStar(__nccwpck_require__(9896)); +const runnerWindows = "Windows"; +const runnerMacOS = "macOS"; async function logout() { try { const runnerOS = process.env.RUNNER_OS || ""; - if (runnerOS === "macOS") { + if (runnerOS === runnerMacOS) { // The below is required to allow GitHub's post job cleanup to complete. core.info("Resetting DNS settings on macOS"); await exec.exec("networksetup", ["-setdnsservers", "Ethernet", "Empty"]); @@ -25703,29 +25706,50 @@ async function logout() { // Check if tailscale is available first try { await exec.exec("tailscale", ["--version"], { silent: true }); + // Determine the correct command based on OS + let execArgs; + if (runnerOS === runnerWindows) { + execArgs = ["tailscale", "logout"]; + } + else { + // Linux and macOS - use system-installed binary with sudo + execArgs = ["sudo", "-E", "tailscale", "logout"]; + } + core.info(`Running: ${execArgs.join(" ")}`); + try { + await exec.exec(execArgs[0], execArgs.slice(1)); + core.info("✅ Successfully logged out of Tailscale"); + } + catch (error) { + // Don't fail the action if logout fails - it's just cleanup + core.warning(`Failed to logout from Tailscale: ${error}`); + core.info("Your ephemeral node will eventually be cleaned up by Tailscale"); + } } catch (error) { core.info("Tailscale not found or not accessible, skipping logout"); return; } - // Determine the correct command based on OS - let execArgs; - if (runnerOS === "Windows") { - execArgs = ["tailscale", "logout"]; - } - else { - // Linux and macOS - use system-installed binary with sudo - execArgs = ["sudo", "-E", "tailscale", "logout"]; - } - core.info(`Running: ${execArgs.join(" ")}`); + core.info("Stopping tailscale"); try { - await exec.exec(execArgs[0], execArgs.slice(1)); - core.info("✅ Successfully logged out of Tailscale"); + if (runnerOS === runnerWindows) { + await exec.exec("net", ["stop", "Tailscale"]); + await exec.exec("taskkill", ["/F", "/IM", "tailscale-ipn.exe"]); + } + else { + const pid = fs.readFileSync("tailscaled.pid").toString(); + if (pid === "") { + throw new Error("pid file empty"); + } + // The pid is actually the pid of the `sudo` parent of tailscaled, so use pkill -P to kill children of that parent + await exec.exec("sudo", ["pkill", "-P", pid]); + // Clean up DNS and routes. + await exec.exec("sudo", ["tailscaled", "--cleanup"]); + } + core.info("✅ Stopped tailscale"); } catch (error) { - // Don't fail the action if logout fails - it's just cleanup - core.warning(`Failed to logout from Tailscale: ${error}`); - core.info("Your ephemeral node will eventually be cleaned up by Tailscale"); + core.warning(`Failed to stop tailscale: ${error}`); } } catch (error) { diff --git a/src/logout/logout.ts b/src/logout/logout.ts index 8b16a37..27a7098 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -3,12 +3,16 @@ import * as core from "@actions/core"; import * as exec from "@actions/exec"; +import * as fs from "fs"; + +const runnerWindows = "Windows"; +const runnerMacOS = "macOS"; async function logout(): Promise { try { const runnerOS = process.env.RUNNER_OS || ""; - if (runnerOS === "macOS") { + if (runnerOS === runnerMacOS) { // The below is required to allow GitHub's post job cleanup to complete. core.info("Resetting DNS settings on macOS"); await exec.exec("networksetup", ["-setdnsservers", "Ethernet", "Empty"]); @@ -24,31 +28,51 @@ async function logout(): Promise { // Check if tailscale is available first try { await exec.exec("tailscale", ["--version"], { silent: true }); + + // Determine the correct command based on OS + let execArgs: string[]; + if (runnerOS === runnerWindows) { + execArgs = ["tailscale", "logout"]; + } else { + // Linux and macOS - use system-installed binary with sudo + execArgs = ["sudo", "-E", "tailscale", "logout"]; + } + + core.info(`Running: ${execArgs.join(" ")}`); + + try { + await exec.exec(execArgs[0], execArgs.slice(1)); + core.info("✅ Successfully logged out of Tailscale"); + } catch (error) { + // Don't fail the action if logout fails - it's just cleanup + core.warning(`Failed to logout from Tailscale: ${error}`); + core.info( + "Your ephemeral node will eventually be cleaned up by Tailscale" + ); + } } catch (error) { core.info("Tailscale not found or not accessible, skipping logout"); return; } - // Determine the correct command based on OS - let execArgs: string[]; - if (runnerOS === "Windows") { - execArgs = ["tailscale", "logout"]; - } else { - // Linux and macOS - use system-installed binary with sudo - execArgs = ["sudo", "-E", "tailscale", "logout"]; - } - - core.info(`Running: ${execArgs.join(" ")}`); - + core.info("Stopping tailscale"); try { - await exec.exec(execArgs[0], execArgs.slice(1)); - core.info("✅ Successfully logged out of Tailscale"); + if (runnerOS === runnerWindows) { + await exec.exec("net", ["stop", "Tailscale"]); + await exec.exec("taskkill", ["/F", "/IM", "tailscale-ipn.exe"]); + } else { + const pid = fs.readFileSync("tailscaled.pid").toString(); + if (pid === "") { + throw new Error("pid file empty"); + } + // The pid is actually the pid of the `sudo` parent of tailscaled, so use pkill -P to kill children of that parent + await exec.exec("sudo", ["pkill", "-P", pid]); + // Clean up DNS and routes. + await exec.exec("sudo", ["tailscaled", "--cleanup"]); + } + core.info("✅ Stopped tailscale"); } catch (error) { - // Don't fail the action if logout fails - it's just cleanup - core.warning(`Failed to logout from Tailscale: ${error}`); - core.info( - "Your ephemeral node will eventually be cleaned up by Tailscale" - ); + core.warning(`Failed to stop tailscale: ${error}`); } } catch (error) { // Don't fail the action for post-cleanup issues diff --git a/src/main.ts b/src/main.ts index dde99b9..2fd2745 100644 --- a/src/main.ts +++ b/src/main.ts @@ -593,6 +593,9 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise { ], }); + // Store PID for cleaning up daemon process in logout.ts. + fs.writeFileSync("tailscaled.pid", `${daemon.pid}`); + daemon.unref(); // Ensure daemon doesn't keep Node.js process alive // Close stdin/stdout/stderr to fully detach