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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 40 additions & 16 deletions dist/logout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -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) {
Expand Down
62 changes: 43 additions & 19 deletions src/logout/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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"]);
Expand All @@ -24,31 +28,51 @@ async function logout(): Promise<void> {
// 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
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
],
});

// 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
Expand Down
Loading