mirror of
https://github.com/tailscale/github-action.git
synced 2026-08-30 05:59:22 +00:00
feat: implement log grouping for better output
Signed-off-by: Lee Briggs <lee@leebriggs.co.uk>
This commit is contained in:
parent
780049a30b
commit
73c8a00bae
6 changed files with 733 additions and 474 deletions
23
README.md
23
README.md
|
|
@ -29,8 +29,8 @@ and log out immediately after finishing their CI run, at which point they are au
|
|||
by the coordination server. The nodes are also [marked Preapproved][kb-auth-keys]
|
||||
on tailnets which use [Device Approval][kb-device-approval]
|
||||
|
||||
|
||||
### Workload identity federation
|
||||
|
||||
[Workload identity federation][kb-workload-identity-federation] can also be used for authenticating nodes with your tailnet:
|
||||
|
||||
```yaml
|
||||
|
|
@ -92,6 +92,27 @@ tailscale ping my-target.my-tailnet.ts.net
|
|||
|
||||
The `ping` option will wait up to 3 minutes for a connection (direct or relayed).
|
||||
|
||||
## Log mode
|
||||
|
||||
By default, this action folds its major setup and cleanup phases into GitHub Actions log groups.
|
||||
You can change this with the `log-mode` input:
|
||||
|
||||
```yaml
|
||||
- name: Tailscale
|
||||
uses: tailscale/github-action@v4
|
||||
with:
|
||||
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||
tags: tag:ci
|
||||
log-mode: normal
|
||||
```
|
||||
|
||||
Supported values are:
|
||||
|
||||
- `grouped`: fold major action phases in the log. This is the default.
|
||||
- `normal`: print routine action logs without grouping.
|
||||
- `quiet`: suppress routine informational output while preserving warnings and errors.
|
||||
|
||||
## Tailnet Lock
|
||||
|
||||
If you are using this Action in a [Tailnet Lock][kb-tailnet-lock] enabled network, you need to:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ inputs:
|
|||
description: 'Comma separated list of hosts (Tailscale IP addresses or machine names if MagicDNS is enabled on the tailnet) to `tailscale ping` for connectivity verification after `tailscale up` completes.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
log-mode:
|
||||
description: 'Controls action log output mode. Use `grouped` to fold major setup and cleanup phases, `normal` for ungrouped logs, or `quiet` to suppress routine informational output.'
|
||||
required: false
|
||||
default: 'grouped'
|
||||
|
||||
runs:
|
||||
using: 'node24'
|
||||
main: 'dist/index.js'
|
||||
|
|
|
|||
318
dist/index.js
generated
vendored
318
dist/index.js
generated
vendored
|
|
@ -52102,11 +52102,8 @@ function xdgRuntimeDir() {
|
|||
const versionLatest = "latest";
|
||||
const versionUnstable = "unstable";
|
||||
// Cross-platform Tailscale local API status check
|
||||
async function getTailscaleStatus() {
|
||||
const { stdout } = await execSilent("get tailscale status", cmdTailscale, [
|
||||
"status",
|
||||
"--json",
|
||||
]);
|
||||
async function getTailscaleStatus(logMode = "normal") {
|
||||
const { stdout } = await execSilent("get tailscale status", cmdTailscale, ["status", "--json"], { logMode });
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
async function run() {
|
||||
|
|
@ -52125,47 +52122,53 @@ async function run() {
|
|||
}
|
||||
// Validate authentication
|
||||
validateAuth(config);
|
||||
// Resolve version
|
||||
config.resolvedVersion = await resolveVersion(config.version, runnerOS);
|
||||
core.info(`Resolved Tailscale version: ${config.resolvedVersion}`);
|
||||
await withLogGroup(config.logMode, "Resolving Tailscale version", async () => {
|
||||
config.resolvedVersion = await resolveVersion(config.version, runnerOS, config.logMode);
|
||||
logInfo(config.logMode, `Resolved Tailscale version: ${config.resolvedVersion}`);
|
||||
});
|
||||
// Set architecture
|
||||
config.arch = getTailscaleArch(runnerOS);
|
||||
// Install Tailscale
|
||||
await installTailscale(config, runnerOS);
|
||||
// Start daemon (non-Windows only)
|
||||
await withLogGroup(config.logMode, "Installing Tailscale", async () => {
|
||||
await installTailscale(config, runnerOS);
|
||||
});
|
||||
if (runnerOS !== runnerWindows) {
|
||||
await startTailscaleDaemon(config);
|
||||
await withLogGroup(config.logMode, "Starting tailscaled", async () => {
|
||||
await startTailscaleDaemon(config);
|
||||
});
|
||||
}
|
||||
// Connect to Tailscale
|
||||
await connectToTailscale(config, runnerOS);
|
||||
// Check Tailscale status (cross-platform)
|
||||
try {
|
||||
const status = await getTailscaleStatus();
|
||||
if (status.BackendState === "Running") {
|
||||
core.info("✅ Tailscale is running and connected!");
|
||||
if (runnerOS === runnerMacOS) {
|
||||
await configureDNSOnMacOS(status);
|
||||
await withLogGroup(config.logMode, "Connecting to Tailscale", async () => {
|
||||
await connectToTailscale(config, runnerOS);
|
||||
});
|
||||
let shouldPingHosts = false;
|
||||
await withLogGroup(config.logMode, "Checking Tailscale status", async () => {
|
||||
try {
|
||||
const status = await getTailscaleStatus(config.logMode);
|
||||
if (status.BackendState === "Running") {
|
||||
logInfo(config.logMode, "✅ Tailscale is running and connected!");
|
||||
if (runnerOS === runnerMacOS) {
|
||||
await configureDNSOnMacOS(status, config.logMode);
|
||||
}
|
||||
shouldPingHosts = true;
|
||||
}
|
||||
else {
|
||||
core.setFailed(`❌ Tailscale backend state: ${status.BackendState}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
await pingHostsIfNecessary(config);
|
||||
// Explicitly exit to prevent hanging
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
core.setFailed(`❌ Tailscale backend state: ${status.BackendState}`);
|
||||
process.exit(1);
|
||||
catch (err) {
|
||||
core.warning(`Failed to get Tailscale status: ${err}`);
|
||||
if (runnerOS === runnerMacOS) {
|
||||
core.setFailed(`❌ Tailscale status is required in order to configure macOS`);
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
// Still exit successfully since the main connection worked
|
||||
logInfo(config.logMode, "✅ Tailscale daemon is connected!");
|
||||
shouldPingHosts = true;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
core.warning(`Failed to get Tailscale status: ${err}`);
|
||||
if (runnerOS === runnerMacOS) {
|
||||
core.setFailed(`❌ Tailscale status is required in order to configure macOS`);
|
||||
process.exit(2);
|
||||
}
|
||||
// Still exit successfully since the main connection worked
|
||||
core.info("✅ Tailscale daemon is connected!");
|
||||
});
|
||||
if (shouldPingHosts) {
|
||||
await pingHostsIfNecessary(config);
|
||||
// Explicitly exit to prevent hanging
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -52176,14 +52179,16 @@ async function pingHostsIfNecessary(config) {
|
|||
if (config.pingHosts.length == 0) {
|
||||
return;
|
||||
}
|
||||
core.info(`Will ping hosts ${config.pingHosts.join(",")} up to 3 minutes each (in parallel) in order to check connectivity`);
|
||||
let pings = config.pingHosts.map((host) => pingHost(host));
|
||||
for (const ping of pings) {
|
||||
await ping;
|
||||
}
|
||||
await withLogGroup(config.logMode, "Pinging Tailscale hosts", async () => {
|
||||
logInfo(config.logMode, `Will ping hosts ${config.pingHosts.join(",")} up to 3 minutes each (in parallel) in order to check connectivity`);
|
||||
let pings = config.pingHosts.map((host) => pingHost(host, config.logMode));
|
||||
for (const ping of pings) {
|
||||
await ping;
|
||||
}
|
||||
});
|
||||
}
|
||||
async function pingHost(host) {
|
||||
core.info(`Pinging host ${host}`);
|
||||
async function pingHost(host, logMode) {
|
||||
logInfo(logMode, `Pinging host ${host}`);
|
||||
let start = new Date().getTime();
|
||||
var i = 0;
|
||||
// Try for up to 180 seconds (3 minutes).
|
||||
|
|
@ -52191,35 +52196,63 @@ async function pingHost(host) {
|
|||
if (i > 0) {
|
||||
// Exponential backoff on wait time, with maximum 5 second wait.
|
||||
let waitTime = Math.min(Math.pow(1.3, i), 5000);
|
||||
core.debug(`Waiting ${waitTime} milliseconds before pinging`);
|
||||
logDebug(logMode, `Waiting ${waitTime} milliseconds before pinging`);
|
||||
await (0, promises_1.setTimeout)(waitTime);
|
||||
}
|
||||
try {
|
||||
let result = await execSilent("ping host", cmdTailscale, [
|
||||
"ping",
|
||||
"-c",
|
||||
"1",
|
||||
host,
|
||||
]);
|
||||
core.info(`✅ Ping host ${host} reachable via direct connection!`);
|
||||
await execSilent("ping host", cmdTailscale, ["ping", "-c", "1", host], {
|
||||
logMode,
|
||||
});
|
||||
logInfo(logMode, `✅ Ping host ${host} reachable via direct connection!`);
|
||||
return;
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof execError &&
|
||||
err.stderr.includes("direct connection not established")) {
|
||||
// Relayed connectivity is good enough, we don't want to tie up a CI job waiting for a direct connection.
|
||||
core.info(`✅ Ping host ${host} reachable via DERP!`);
|
||||
logInfo(logMode, `✅ Ping host ${host} reachable via DERP!`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
core.setFailed(`❌ Ping host ${host} did not respond`);
|
||||
process.exit(1);
|
||||
throw new Error(`❌ Ping host ${host} did not respond`);
|
||||
}
|
||||
function getLogMode() {
|
||||
const logMode = core.getInput("log-mode") || "grouped";
|
||||
if (logMode !== "grouped" &&
|
||||
logMode !== "normal" &&
|
||||
logMode !== "quiet") {
|
||||
throw new Error(`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`);
|
||||
}
|
||||
return logMode;
|
||||
}
|
||||
function logInfo(logMode, message) {
|
||||
if (logMode !== "quiet") {
|
||||
core.info(message);
|
||||
}
|
||||
}
|
||||
function logDebug(logMode, message) {
|
||||
if (logMode !== "quiet") {
|
||||
core.debug(message);
|
||||
}
|
||||
}
|
||||
async function withLogGroup(logMode, name, fn) {
|
||||
if (logMode !== "grouped") {
|
||||
return fn();
|
||||
}
|
||||
core.startGroup(name);
|
||||
try {
|
||||
return await fn();
|
||||
}
|
||||
finally {
|
||||
core.endGroup();
|
||||
}
|
||||
}
|
||||
async function getInputs() {
|
||||
let ping = core.getInput("ping");
|
||||
let pingHosts = ping?.length > 0 ? ping.split(",") : [];
|
||||
const logMode = getLogMode();
|
||||
const authKey = core.getInput("authkey") || "";
|
||||
const oauthSecret = core.getInput("oauth-secret") || "";
|
||||
// Mask sensitive values in logs unless debug mode is enabled
|
||||
|
|
@ -52249,6 +52282,7 @@ async function getInputs() {
|
|||
useCache: core.getBooleanInput("use-cache"),
|
||||
sha256Sum: core.getInput("sha256sum") || "",
|
||||
pingHosts: pingHosts,
|
||||
logMode: logMode,
|
||||
};
|
||||
if (config.oauthSecret && !config.tags) {
|
||||
throw new Error("the tags parameter is required when using an OAuth client");
|
||||
|
|
@ -52267,19 +52301,14 @@ function validateAuth(config) {
|
|||
throw new Error("Workload identity federation requires using tailscale version 1.90.0 or later.");
|
||||
}
|
||||
}
|
||||
async function resolveVersion(version, runnerOS) {
|
||||
async function resolveVersion(version, runnerOS, logMode) {
|
||||
if (runnerOS === runnerMacOS && version === versionUnstable) {
|
||||
return "main";
|
||||
}
|
||||
if (version === versionLatest || version === versionUnstable) {
|
||||
let path = version === versionUnstable ? versionUnstable : "stable";
|
||||
let pkg = `https://pkgs.tailscale.com/${path}/?mode=json`;
|
||||
const { stdout } = await execSilent(`curl ${pkg}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-s",
|
||||
pkg,
|
||||
]);
|
||||
const { stdout } = await execSilent(`curl ${pkg}`, "curl", ["-H", "user-agent:action-setup-tailscale", "-s", pkg], { logMode });
|
||||
const response = JSON.parse(stdout);
|
||||
switch (runnerOS) {
|
||||
case runnerLinux:
|
||||
|
|
@ -52338,14 +52367,14 @@ async function installTailscale(config, runnerOS) {
|
|||
if (config.useCache && cacheKey) {
|
||||
const cacheHit = await cache.restoreCache([toolPath], cacheKey);
|
||||
if (cacheHit) {
|
||||
core.info(`Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`);
|
||||
logInfo(config.logMode, `Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`);
|
||||
// For Windows, install the cached MSI
|
||||
if (runnerOS === runnerWindows) {
|
||||
await installTailscaleWindows(config, toolPath, true);
|
||||
}
|
||||
else {
|
||||
// For Linux/macOS, copy binaries to /usr/local/bin
|
||||
await installCachedBinaries(toolPath, runnerOS);
|
||||
await installCachedBinaries(config, toolPath, runnerOS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -52364,7 +52393,7 @@ async function installTailscale(config, runnerOS) {
|
|||
if (config.useCache && cacheKey) {
|
||||
try {
|
||||
await cache.saveCache([toolPath], cacheKey);
|
||||
core.info(`Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`);
|
||||
logInfo(config.logMode, `Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`);
|
||||
}
|
||||
catch (error) {
|
||||
const typedError = error;
|
||||
|
|
@ -52372,7 +52401,7 @@ async function installTailscale(config, runnerOS) {
|
|||
throw error;
|
||||
}
|
||||
else if (typedError.name === cache.ReserveCacheError.name) {
|
||||
core.info(typedError.message);
|
||||
logInfo(config.logMode, typedError.message);
|
||||
}
|
||||
else {
|
||||
core.warning(`Cache save failed: ${typedError.message}`);
|
||||
|
|
@ -52399,26 +52428,20 @@ async function installTailscaleLinux(config, toolPath) {
|
|||
// Get SHA256 if not provided
|
||||
if (!config.sha256Sum) {
|
||||
const shaUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz.sha256`;
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-L",
|
||||
shaUrl,
|
||||
"--fail",
|
||||
]);
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", ["-H", "user-agent:action-setup-tailscale", "-L", shaUrl, "--fail"], { logMode: config.logMode });
|
||||
config.sha256Sum = stdout.trim();
|
||||
}
|
||||
// Download and extract
|
||||
const downloadUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz`;
|
||||
core.info(`Downloading ${downloadUrl}`);
|
||||
logInfo(config.logMode, `Downloading ${downloadUrl}`);
|
||||
const tarDest = path.join(xdgCacheDir(), "tailscale.tgz");
|
||||
fs.mkdirSync(path.dirname(tarDest), { recursive: true });
|
||||
const tarPath = await tc.downloadTool(downloadUrl, tarDest);
|
||||
// Verify checksum
|
||||
const actualSha = await calculateFileSha256(tarPath);
|
||||
const expectedSha = config.sha256Sum.trim().toLowerCase();
|
||||
core.info(`Expected sha256: ${expectedSha}`);
|
||||
core.info(`Actual sha256: ${actualSha}`);
|
||||
logInfo(config.logMode, `Expected sha256: ${expectedSha}`);
|
||||
logInfo(config.logMode, `Actual sha256: ${actualSha}`);
|
||||
if (actualSha !== expectedSha) {
|
||||
throw new Error("SHA256 checksum mismatch");
|
||||
}
|
||||
|
|
@ -52435,18 +52458,10 @@ async function installTailscaleLinux(config, toolPath) {
|
|||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
]);
|
||||
], { logMode: config.logMode });
|
||||
// Make sure they're executable
|
||||
await execSilent("chmod tailscale binary", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled binary", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscale binary", "sudo", ["chmod", "+x", cmdTailscaleFullPath], { logMode: config.logMode });
|
||||
await execSilent("chmod tailscaled binary", "sudo", ["chmod", "+x", cmdTailscaledFullPath], { logMode: config.logMode });
|
||||
}
|
||||
async function installTailscaleWindows(config, toolPath, fromCache = false) {
|
||||
// Create tool directory
|
||||
|
|
@ -52457,7 +52472,7 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
|
|||
if (!fs.existsSync(msiPath)) {
|
||||
throw new Error(`Cached MSI not found at ${msiPath}`);
|
||||
}
|
||||
core.info(`Installing cached MSI from ${msiPath}`);
|
||||
logInfo(config.logMode, `Installing cached MSI from ${msiPath}`);
|
||||
}
|
||||
else {
|
||||
// Fresh download
|
||||
|
|
@ -52470,13 +52485,7 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
|
|||
// Get SHA256 if not provided
|
||||
if (!config.sha256Sum) {
|
||||
const shaUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi.sha256`;
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-L",
|
||||
shaUrl,
|
||||
"--fail",
|
||||
]);
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", ["-H", "user-agent:action-setup-tailscale", "-L", shaUrl, "--fail"], { logMode: config.logMode });
|
||||
config.sha256Sum = stdout.trim();
|
||||
}
|
||||
// Download MSI
|
||||
|
|
@ -52487,21 +52496,21 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
|
|||
if (fs.existsSync(msiPath)) {
|
||||
const existingSha = await calculateFileSha256(msiPath);
|
||||
if (existingSha === expectedSha) {
|
||||
core.info(`Using existing MSI at ${msiPath} (checksum verified)`);
|
||||
logInfo(config.logMode, `Using existing MSI at ${msiPath} (checksum verified)`);
|
||||
needsDownload = false;
|
||||
}
|
||||
else {
|
||||
core.info(`Existing MSI checksum mismatch, re-downloading`);
|
||||
logInfo(config.logMode, `Existing MSI checksum mismatch, re-downloading`);
|
||||
fs.unlinkSync(msiPath);
|
||||
}
|
||||
}
|
||||
if (needsDownload) {
|
||||
core.info(`Downloading ${downloadUrl}`);
|
||||
logInfo(config.logMode, `Downloading ${downloadUrl}`);
|
||||
const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath);
|
||||
// Verify checksum
|
||||
const actualSha = await calculateFileSha256(downloadedMsiPath);
|
||||
core.info(`Expected sha256: ${expectedSha}`);
|
||||
core.info(`Actual sha256: ${actualSha}`);
|
||||
logInfo(config.logMode, `Expected sha256: ${expectedSha}`);
|
||||
logInfo(config.logMode, `Actual sha256: ${actualSha}`);
|
||||
if (actualSha !== expectedSha) {
|
||||
throw new Error("SHA256 checksum mismatch");
|
||||
}
|
||||
|
|
@ -52519,17 +52528,18 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
|
|||
path.join(process.env.RUNNER_TEMP || "", "tailscale.log"),
|
||||
"/i",
|
||||
msiPath,
|
||||
]);
|
||||
], { logMode: config.logMode });
|
||||
// Add to PATH
|
||||
core.addPath("C:\\Program Files\\Tailscale\\");
|
||||
}
|
||||
async function installTailscaleMacOS(config, toolPath) {
|
||||
core.info("Building tailscale from src on macOS...");
|
||||
logInfo(config.logMode, "Building tailscale from src on macOS...");
|
||||
// Clone the repo
|
||||
await execSilent("clone tailscale repo", "git clone https://github.com/tailscale/tailscale.git tailscale");
|
||||
await execSilent("clone tailscale repo", "git clone https://github.com/tailscale/tailscale.git tailscale", [], { logMode: config.logMode });
|
||||
// Checkout the resolved version
|
||||
await execSilent("checkout resolved version", `git checkout v${config.resolvedVersion}`, [], {
|
||||
cwd: cmdTailscale,
|
||||
logMode: config.logMode,
|
||||
});
|
||||
// Create tool directory and copy binaries there for caching
|
||||
fs.mkdirSync(toolPath, { recursive: true });
|
||||
|
|
@ -52541,6 +52551,7 @@ async function installTailscaleMacOS(config, toolPath) {
|
|||
...process.env,
|
||||
TS_USE_TOOLCHAIN: "1",
|
||||
},
|
||||
logMode: config.logMode,
|
||||
});
|
||||
}
|
||||
// Install binaries to /usr/local/bin
|
||||
|
|
@ -52549,19 +52560,11 @@ async function installTailscaleMacOS(config, toolPath) {
|
|||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
]);
|
||||
], { logMode: config.logMode });
|
||||
// Make sure they're executable
|
||||
await execSilent("chmod tailscale", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
core.info("✅ Tailscale installed successfully on macOS from source");
|
||||
await execSilent("chmod tailscale", "sudo", ["chmod", "+x", cmdTailscaleFullPath], { logMode: config.logMode });
|
||||
await execSilent("chmod tailscaled", "sudo", ["chmod", "+x", cmdTailscaledFullPath], { logMode: config.logMode });
|
||||
logInfo(config.logMode, "✅ Tailscale installed successfully on macOS from source");
|
||||
}
|
||||
async function startTailscaleDaemon(config) {
|
||||
const runnerOS = process.env.RUNNER_OS || "";
|
||||
|
|
@ -52576,7 +52579,7 @@ async function startTailscaleDaemon(config) {
|
|||
...stateArgs,
|
||||
...config.tailscaledArgs.split(" ").filter(Boolean),
|
||||
];
|
||||
core.info("Starting tailscaled daemon...");
|
||||
logInfo(config.logMode, "Starting tailscaled daemon...");
|
||||
// Start daemon in background
|
||||
const daemon = (0, child_process_1.spawn)("sudo", ["-E", cmdTailscaled, ...args], {
|
||||
detached: true,
|
||||
|
|
@ -52599,28 +52602,28 @@ async function startTailscaleDaemon(config) {
|
|||
if (daemon.stderr)
|
||||
daemon.stderr.destroy();
|
||||
// Poll the local API until daemon is responsive
|
||||
await waitForDaemonReady();
|
||||
core.info("✅ tailscaled daemon is up and running!");
|
||||
await waitForDaemonReady(config.logMode);
|
||||
logInfo(config.logMode, "✅ tailscaled daemon is up and running!");
|
||||
}
|
||||
async function waitForDaemonReady() {
|
||||
async function waitForDaemonReady(logMode) {
|
||||
const maxWaitMs = 15000; // 15 seconds
|
||||
const pollIntervalMs = 500;
|
||||
let waited = 0;
|
||||
core.info("Waiting for tailscaled daemon to become ready...");
|
||||
logInfo(logMode, "Waiting for tailscaled daemon to become ready...");
|
||||
var lastErr;
|
||||
while (waited < maxWaitMs) {
|
||||
try {
|
||||
const status = await getTailscaleStatus();
|
||||
const status = await getTailscaleStatus(logMode);
|
||||
// If we get any valid response from the API, the daemon is ready
|
||||
if (status) {
|
||||
core.info(`Daemon ready! Initial state: ${status.BackendState || "Unknown"}`);
|
||||
logInfo(logMode, `Daemon ready! Initial state: ${status.BackendState || "Unknown"}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
// Daemon not ready yet, keep polling
|
||||
lastErr = err;
|
||||
core.debug(`Waiting for daemon... (${waited}ms elapsed)`);
|
||||
logDebug(logMode, `Waiting for daemon... (${waited}ms elapsed)`);
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
waited += pollIntervalMs;
|
||||
|
|
@ -52635,7 +52638,9 @@ async function connectToTailscale(config, runnerOS) {
|
|||
hostname = `github-${process.env.COMPUTERNAME}`;
|
||||
}
|
||||
else {
|
||||
const { stdout } = await execSilent("hostname", "hostname");
|
||||
const { stdout } = await execSilent("hostname", "hostname", [], {
|
||||
logMode: config.logMode,
|
||||
});
|
||||
hostname = `github-${stdout.trim()}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -52683,7 +52688,7 @@ async function connectToTailscale(config, runnerOS) {
|
|||
// Retry logic
|
||||
for (let attempt = 1; attempt <= config.retry; attempt++) {
|
||||
try {
|
||||
core.info(`Attempt ${attempt} to bring up Tailscale...`);
|
||||
logInfo(config.logMode, `Attempt ${attempt} to bring up Tailscale...`);
|
||||
let execArgs;
|
||||
if (runnerOS === runnerWindows) {
|
||||
execArgs = [cmdTailscale, ...upArgs];
|
||||
|
|
@ -52694,11 +52699,13 @@ async function connectToTailscale(config, runnerOS) {
|
|||
}
|
||||
const timeoutMs = parseTimeout(config.timeout);
|
||||
await Promise.race([
|
||||
execSilent("tailscale up", execArgs[0], execArgs.slice(1)),
|
||||
execSilent("tailscale up", execArgs[0], execArgs.slice(1), {
|
||||
logMode: config.logMode,
|
||||
}),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)),
|
||||
]);
|
||||
// Success
|
||||
core.info(`✅ Tailscale up command completed successfully on attempt ${attempt}`);
|
||||
logInfo(config.logMode, `✅ Tailscale up command completed successfully on attempt ${attempt}`);
|
||||
return;
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -52707,7 +52714,7 @@ async function connectToTailscale(config, runnerOS) {
|
|||
throw error;
|
||||
}
|
||||
const sleepTime = attempt * 2; // Reduced from 5 to 2 seconds
|
||||
core.info(`Retrying in ${sleepTime} seconds...`);
|
||||
logInfo(config.logMode, `Retrying in ${sleepTime} seconds...`);
|
||||
await sleep(sleepTime * 1000);
|
||||
}
|
||||
}
|
||||
|
|
@ -52745,55 +52752,31 @@ function getToolPath(config, runnerOS) {
|
|||
}
|
||||
return path.join(cacheDirectory, cmdTailscale, config.resolvedVersion, `${runnerOS}-${config.arch}`);
|
||||
}
|
||||
async function installCachedBinaries(toolPath, runnerOS) {
|
||||
async function installCachedBinaries(config, toolPath, runnerOS) {
|
||||
if (runnerOS === runnerLinux || runnerOS === runnerMacOS) {
|
||||
// Copy cached binaries to /usr/local/bin
|
||||
const tailscaleBin = path.join(toolPath, cmdTailscale);
|
||||
const tailscaledBin = path.join(toolPath, cmdTailscaled);
|
||||
if (fs.existsSync(tailscaleBin) && fs.existsSync(tailscaledBin)) {
|
||||
await execSilent("copy tailscale from cache", "sudo", [
|
||||
"cp",
|
||||
tailscaleBin,
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("copy tailscaled from cache", "sudo", [
|
||||
"cp",
|
||||
tailscaledBin,
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscale", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent("copy tailscale from cache", "sudo", ["cp", tailscaleBin, cmdTailscaleFullPath], { logMode: config.logMode });
|
||||
await execSilent("copy tailscaled from cache", "sudo", ["cp", tailscaledBin, cmdTailscaledFullPath], { logMode: config.logMode });
|
||||
await execSilent("chmod tailscale", "sudo", ["chmod", "+x", cmdTailscaleFullPath], { logMode: config.logMode });
|
||||
await execSilent("chmod tailscaled", "sudo", ["chmod", "+x", cmdTailscaledFullPath], { logMode: config.logMode });
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cached binaries not found in ${toolPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function configureDNSOnMacOS(status) {
|
||||
async function configureDNSOnMacOS(status, logMode) {
|
||||
if (!status.CurrentTailnet.MagicDNSEnabled) {
|
||||
core.info("MagicDNS is disabled, not configuring DNS");
|
||||
logInfo(logMode, "MagicDNS is disabled, not configuring DNS");
|
||||
return;
|
||||
}
|
||||
core.info(`Setting system DNS server to 100.100.100.100 and searchdomains to ${status.CurrentTailnet.MagicDNSSuffix}`);
|
||||
logInfo(logMode, `Setting system DNS server to 100.100.100.100 and searchdomains to ${status.CurrentTailnet.MagicDNSSuffix}`);
|
||||
try {
|
||||
await execSilent("set dns servers", "networksetup", [
|
||||
"-setdnsservers",
|
||||
"Ethernet",
|
||||
"100.100.100.100",
|
||||
]);
|
||||
await execSilent("set search domains", "networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
status.CurrentTailnet.MagicDNSSuffix,
|
||||
]);
|
||||
await execSilent("set dns servers", "networksetup", ["-setdnsservers", "Ethernet", "100.100.100.100"], { logMode });
|
||||
await execSilent("set search domains", "networksetup", ["-setsearchdomains", "Ethernet", status.CurrentTailnet.MagicDNSSuffix], { logMode });
|
||||
}
|
||||
catch (e) {
|
||||
throw Error(`Failed to configure DNS on macOS: ${e}`);
|
||||
|
|
@ -52814,14 +52797,15 @@ run();
|
|||
* @throws execError if exec returned a non-zero status code
|
||||
*/
|
||||
async function execSilent(label, cmd, args, opts) {
|
||||
core.info(`▶️ ${label}`);
|
||||
const { logMode = "normal", ...execOpts } = opts || {};
|
||||
logInfo(logMode, `▶️ ${label}`);
|
||||
const out = await exec.getExecOutput(cmd, args, {
|
||||
...opts,
|
||||
silent: !core.isDebug(),
|
||||
...execOpts,
|
||||
silent: logMode === "quiet" || !core.isDebug(),
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (out.exitCode !== 0) {
|
||||
if (!core.isDebug()) {
|
||||
if (logMode === "quiet" || !core.isDebug()) {
|
||||
// When debug logging is off, stderr won't have been written to console, write it now.
|
||||
process.stderr.write(out.stderr);
|
||||
}
|
||||
|
|
|
|||
159
dist/logout/index.js
generated
vendored
159
dist/logout/index.js
generated
vendored
|
|
@ -25946,76 +25946,121 @@ const runnerMacOS = "macOS";
|
|||
async function logout() {
|
||||
try {
|
||||
const runnerOS = process.env.RUNNER_OS || "";
|
||||
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"]);
|
||||
await exec.exec("networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
}
|
||||
core.info("🔄 Logging out of Tailscale...");
|
||||
// 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"];
|
||||
const logMode = getLogMode();
|
||||
await withLogGroup(logMode, "Cleaning up Tailscale", async () => {
|
||||
if (runnerOS === runnerMacOS) {
|
||||
// The below is required to allow GitHub's post job cleanup to complete.
|
||||
logInfo(logMode, "Resetting DNS settings on macOS");
|
||||
await execCommand(logMode, "networksetup", [
|
||||
"-setdnsservers",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
await execCommand(logMode, "networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// Linux and macOS - use system-installed binary with sudo
|
||||
execArgs = ["sudo", "-E", "tailscale", "logout"];
|
||||
}
|
||||
core.info(`Running: ${execArgs.join(" ")}`);
|
||||
logInfo(logMode, "🔄 Logging out of Tailscale...");
|
||||
// Check if tailscale is available first
|
||||
try {
|
||||
await exec.exec(execArgs[0], execArgs.slice(1));
|
||||
core.info("✅ Successfully logged out of Tailscale");
|
||||
await execCommand(logMode, "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"];
|
||||
}
|
||||
logInfo(logMode, `Running: ${execArgs.join(" ")}`);
|
||||
try {
|
||||
await execCommand(logMode, execArgs[0], execArgs.slice(1));
|
||||
logInfo(logMode, "✅ 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}`);
|
||||
logInfo(logMode, "Your ephemeral node will eventually be cleaned up by 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");
|
||||
logInfo(logMode, "Tailscale not found or not accessible, skipping logout");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
core.info("Tailscale not found or not accessible, skipping logout");
|
||||
return;
|
||||
}
|
||||
core.info("Stopping tailscale");
|
||||
try {
|
||||
if (runnerOS === runnerWindows) {
|
||||
await exec.exec("net", ["stop", "Tailscale"]);
|
||||
await exec.exec("taskkill", ["/F", "/IM", "tailscale-ipn.exe"]);
|
||||
}
|
||||
else {
|
||||
const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR ||
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
path.join(os.homedir(), ".cache");
|
||||
const pid = fs
|
||||
.readFileSync(path.join(xdgRuntimeDir, "tailscaled.pid"))
|
||||
.toString();
|
||||
if (pid === "") {
|
||||
throw new Error("pid file empty");
|
||||
logInfo(logMode, "Stopping tailscale");
|
||||
try {
|
||||
if (runnerOS === runnerWindows) {
|
||||
await execCommand(logMode, "net", ["stop", "Tailscale"]);
|
||||
await execCommand(logMode, "taskkill", [
|
||||
"/F",
|
||||
"/IM",
|
||||
"tailscale-ipn.exe",
|
||||
]);
|
||||
}
|
||||
// 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"]);
|
||||
else {
|
||||
const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR ||
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
path.join(os.homedir(), ".cache");
|
||||
const pid = fs
|
||||
.readFileSync(path.join(xdgRuntimeDir, "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 execCommand(logMode, "sudo", ["pkill", "-P", pid]);
|
||||
// Clean up DNS and routes.
|
||||
await execCommand(logMode, "sudo", ["tailscaled", "--cleanup"]);
|
||||
}
|
||||
logInfo(logMode, "✅ Stopped tailscale");
|
||||
}
|
||||
core.info("✅ Stopped tailscale");
|
||||
}
|
||||
catch (error) {
|
||||
core.warning(`Failed to stop tailscale: ${error}`);
|
||||
}
|
||||
catch (error) {
|
||||
core.warning(`Failed to stop tailscale: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// Don't fail the action for post-cleanup issues
|
||||
core.warning(`Post-action cleanup error: ${error}`);
|
||||
}
|
||||
}
|
||||
function getLogMode() {
|
||||
const logMode = core.getInput("log-mode") || "grouped";
|
||||
if (logMode !== "grouped" &&
|
||||
logMode !== "normal" &&
|
||||
logMode !== "quiet") {
|
||||
throw new Error(`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`);
|
||||
}
|
||||
return logMode;
|
||||
}
|
||||
function logInfo(logMode, message) {
|
||||
if (logMode !== "quiet") {
|
||||
core.info(message);
|
||||
}
|
||||
}
|
||||
async function withLogGroup(logMode, name, fn) {
|
||||
if (logMode !== "grouped") {
|
||||
return fn();
|
||||
}
|
||||
core.startGroup(name);
|
||||
try {
|
||||
return await fn();
|
||||
}
|
||||
finally {
|
||||
core.endGroup();
|
||||
}
|
||||
}
|
||||
async function execCommand(logMode, commandLine, args, options) {
|
||||
return exec.exec(commandLine, args, {
|
||||
...options,
|
||||
silent: options?.silent || logMode === "quiet",
|
||||
});
|
||||
}
|
||||
// Run the logout function
|
||||
logout().catch((error) => {
|
||||
// Even if logout fails, don't fail the action
|
||||
|
|
|
|||
|
|
@ -10,84 +10,152 @@ import * as path from "path";
|
|||
const runnerWindows = "Windows";
|
||||
const runnerMacOS = "macOS";
|
||||
|
||||
type LogMode = "grouped" | "normal" | "quiet";
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
const runnerOS = process.env.RUNNER_OS || "";
|
||||
const logMode = getLogMode();
|
||||
|
||||
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"]);
|
||||
await exec.exec("networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
}
|
||||
|
||||
core.info("🔄 Logging out of Tailscale...");
|
||||
|
||||
// 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"];
|
||||
await withLogGroup(logMode, "Cleaning up Tailscale", async () => {
|
||||
if (runnerOS === runnerMacOS) {
|
||||
// The below is required to allow GitHub's post job cleanup to complete.
|
||||
logInfo(logMode, "Resetting DNS settings on macOS");
|
||||
await execCommand(logMode, "networksetup", [
|
||||
"-setdnsservers",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
await execCommand(logMode, "networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
"Empty",
|
||||
]);
|
||||
}
|
||||
|
||||
core.info(`Running: ${execArgs.join(" ")}`);
|
||||
logInfo(logMode, "🔄 Logging out of Tailscale...");
|
||||
|
||||
// Check if tailscale is available first
|
||||
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;
|
||||
}
|
||||
await execCommand(logMode, "tailscale", ["--version"], {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
core.info("Stopping tailscale");
|
||||
try {
|
||||
if (runnerOS === runnerWindows) {
|
||||
await exec.exec("net", ["stop", "Tailscale"]);
|
||||
await exec.exec("taskkill", ["/F", "/IM", "tailscale-ipn.exe"]);
|
||||
} else {
|
||||
const xdgRuntimeDir =
|
||||
process.env.XDG_RUNTIME_DIR ||
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
path.join(os.homedir(), ".cache");
|
||||
const pid = fs
|
||||
.readFileSync(path.join(xdgRuntimeDir, "tailscaled.pid"))
|
||||
.toString();
|
||||
if (pid === "") {
|
||||
throw new Error("pid file empty");
|
||||
// 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"];
|
||||
}
|
||||
// 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"]);
|
||||
|
||||
logInfo(logMode, `Running: ${execArgs.join(" ")}`);
|
||||
|
||||
try {
|
||||
await execCommand(logMode, execArgs[0], execArgs.slice(1));
|
||||
logInfo(logMode, "✅ 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}`);
|
||||
logInfo(
|
||||
logMode,
|
||||
"Your ephemeral node will eventually be cleaned up by Tailscale",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logInfo(
|
||||
logMode,
|
||||
"Tailscale not found or not accessible, skipping logout",
|
||||
);
|
||||
return;
|
||||
}
|
||||
core.info("✅ Stopped tailscale");
|
||||
} catch (error) {
|
||||
core.warning(`Failed to stop tailscale: ${error}`);
|
||||
}
|
||||
|
||||
logInfo(logMode, "Stopping tailscale");
|
||||
try {
|
||||
if (runnerOS === runnerWindows) {
|
||||
await execCommand(logMode, "net", ["stop", "Tailscale"]);
|
||||
await execCommand(logMode, "taskkill", [
|
||||
"/F",
|
||||
"/IM",
|
||||
"tailscale-ipn.exe",
|
||||
]);
|
||||
} else {
|
||||
const xdgRuntimeDir =
|
||||
process.env.XDG_RUNTIME_DIR ||
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
path.join(os.homedir(), ".cache");
|
||||
const pid = fs
|
||||
.readFileSync(path.join(xdgRuntimeDir, "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 execCommand(logMode, "sudo", ["pkill", "-P", pid]);
|
||||
// Clean up DNS and routes.
|
||||
await execCommand(logMode, "sudo", ["tailscaled", "--cleanup"]);
|
||||
}
|
||||
logInfo(logMode, "✅ Stopped tailscale");
|
||||
} catch (error) {
|
||||
core.warning(`Failed to stop tailscale: ${error}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Don't fail the action for post-cleanup issues
|
||||
core.warning(`Post-action cleanup error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getLogMode(): LogMode {
|
||||
const logMode = core.getInput("log-mode") || "grouped";
|
||||
if (
|
||||
logMode !== "grouped" &&
|
||||
logMode !== "normal" &&
|
||||
logMode !== "quiet"
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`,
|
||||
);
|
||||
}
|
||||
return logMode;
|
||||
}
|
||||
|
||||
function logInfo(logMode: LogMode, message: string): void {
|
||||
if (logMode !== "quiet") {
|
||||
core.info(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function withLogGroup<T>(
|
||||
logMode: LogMode,
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
if (logMode !== "grouped") {
|
||||
return fn();
|
||||
}
|
||||
|
||||
core.startGroup(name);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
core.endGroup();
|
||||
}
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
logMode: LogMode,
|
||||
commandLine: string,
|
||||
args?: string[],
|
||||
options?: exec.ExecOptions,
|
||||
): Promise<number> {
|
||||
return exec.exec(commandLine, args, {
|
||||
...options,
|
||||
silent: options?.silent || logMode === "quiet",
|
||||
});
|
||||
}
|
||||
|
||||
// Run the logout function
|
||||
logout().catch((error) => {
|
||||
// Even if logout fails, don't fail the action
|
||||
|
|
|
|||
511
src/main.ts
511
src/main.ts
|
|
@ -34,6 +34,8 @@ function xdgRuntimeDir(): string {
|
|||
const versionLatest = "latest";
|
||||
const versionUnstable = "unstable";
|
||||
|
||||
type LogMode = "grouped" | "normal" | "quiet";
|
||||
|
||||
interface TailscaleConfig {
|
||||
version: string;
|
||||
resolvedVersion: string;
|
||||
|
|
@ -52,6 +54,7 @@ interface TailscaleConfig {
|
|||
useCache: boolean;
|
||||
sha256Sum: string;
|
||||
pingHosts: string[];
|
||||
logMode: LogMode;
|
||||
}
|
||||
|
||||
type tailnetInfo = {
|
||||
|
|
@ -65,11 +68,15 @@ type tailscaleStatus = {
|
|||
};
|
||||
|
||||
// Cross-platform Tailscale local API status check
|
||||
async function getTailscaleStatus(): Promise<tailscaleStatus> {
|
||||
const { stdout } = await execSilent("get tailscale status", cmdTailscale, [
|
||||
"status",
|
||||
"--json",
|
||||
]);
|
||||
async function getTailscaleStatus(
|
||||
logMode: LogMode = "normal",
|
||||
): Promise<tailscaleStatus> {
|
||||
const { stdout } = await execSilent(
|
||||
"get tailscale status",
|
||||
cmdTailscale,
|
||||
["status", "--json"],
|
||||
{ logMode },
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
|
|
@ -97,52 +104,80 @@ async function run(): Promise<void> {
|
|||
// Validate authentication
|
||||
validateAuth(config);
|
||||
|
||||
// Resolve version
|
||||
config.resolvedVersion = await resolveVersion(config.version, runnerOS);
|
||||
core.info(`Resolved Tailscale version: ${config.resolvedVersion}`);
|
||||
await withLogGroup(
|
||||
config.logMode,
|
||||
"Resolving Tailscale version",
|
||||
async () => {
|
||||
config.resolvedVersion = await resolveVersion(
|
||||
config.version,
|
||||
runnerOS,
|
||||
config.logMode,
|
||||
);
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Resolved Tailscale version: ${config.resolvedVersion}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Set architecture
|
||||
config.arch = getTailscaleArch(runnerOS);
|
||||
|
||||
// Install Tailscale
|
||||
await installTailscale(config, runnerOS);
|
||||
await withLogGroup(config.logMode, "Installing Tailscale", async () => {
|
||||
await installTailscale(config, runnerOS);
|
||||
});
|
||||
|
||||
// Start daemon (non-Windows only)
|
||||
if (runnerOS !== runnerWindows) {
|
||||
await startTailscaleDaemon(config);
|
||||
await withLogGroup(config.logMode, "Starting tailscaled", async () => {
|
||||
await startTailscaleDaemon(config);
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to Tailscale
|
||||
await connectToTailscale(config, runnerOS);
|
||||
await withLogGroup(
|
||||
config.logMode,
|
||||
"Connecting to Tailscale",
|
||||
async () => {
|
||||
await connectToTailscale(config, runnerOS);
|
||||
},
|
||||
);
|
||||
|
||||
// Check Tailscale status (cross-platform)
|
||||
try {
|
||||
const status = await getTailscaleStatus();
|
||||
if (status.BackendState === "Running") {
|
||||
core.info("✅ Tailscale is running and connected!");
|
||||
if (runnerOS === runnerMacOS) {
|
||||
await configureDNSOnMacOS(status);
|
||||
let shouldPingHosts = false;
|
||||
await withLogGroup(
|
||||
config.logMode,
|
||||
"Checking Tailscale status",
|
||||
async () => {
|
||||
try {
|
||||
const status = await getTailscaleStatus(config.logMode);
|
||||
if (status.BackendState === "Running") {
|
||||
logInfo(config.logMode, "✅ Tailscale is running and connected!");
|
||||
if (runnerOS === runnerMacOS) {
|
||||
await configureDNSOnMacOS(status, config.logMode);
|
||||
}
|
||||
shouldPingHosts = true;
|
||||
} else {
|
||||
core.setFailed(
|
||||
`❌ Tailscale backend state: ${status.BackendState}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Failed to get Tailscale status: ${err}`);
|
||||
if (runnerOS === runnerMacOS) {
|
||||
core.setFailed(
|
||||
`❌ Tailscale status is required in order to configure macOS`,
|
||||
);
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
// Still exit successfully since the main connection worked
|
||||
logInfo(config.logMode, "✅ Tailscale daemon is connected!");
|
||||
shouldPingHosts = true;
|
||||
}
|
||||
await pingHostsIfNecessary(config);
|
||||
// Explicitly exit to prevent hanging
|
||||
process.exit(0);
|
||||
} else {
|
||||
core.setFailed(`❌ Tailscale backend state: ${status.BackendState}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Failed to get Tailscale status: ${err}`);
|
||||
if (runnerOS === runnerMacOS) {
|
||||
core.setFailed(
|
||||
`❌ Tailscale status is required in order to configure macOS`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
// Still exit successfully since the main connection worked
|
||||
core.info("✅ Tailscale daemon is connected!");
|
||||
},
|
||||
);
|
||||
|
||||
if (shouldPingHosts) {
|
||||
await pingHostsIfNecessary(config);
|
||||
// Explicitly exit to prevent hanging
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error instanceof Error ? error.message : String(error));
|
||||
|
|
@ -154,19 +189,24 @@ async function pingHostsIfNecessary(config: TailscaleConfig): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Will ping hosts ${config.pingHosts.join(
|
||||
",",
|
||||
)} up to 3 minutes each (in parallel) in order to check connectivity`,
|
||||
);
|
||||
let pings = config.pingHosts.map((host) => pingHost(host));
|
||||
for (const ping of pings) {
|
||||
await ping;
|
||||
}
|
||||
await withLogGroup(config.logMode, "Pinging Tailscale hosts", async () => {
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Will ping hosts ${config.pingHosts.join(
|
||||
",",
|
||||
)} up to 3 minutes each (in parallel) in order to check connectivity`,
|
||||
);
|
||||
let pings = config.pingHosts.map((host) =>
|
||||
pingHost(host, config.logMode),
|
||||
);
|
||||
for (const ping of pings) {
|
||||
await ping;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function pingHost(host: string): Promise<void> {
|
||||
core.info(`Pinging host ${host}`);
|
||||
async function pingHost(host: string, logMode: LogMode): Promise<void> {
|
||||
logInfo(logMode, `Pinging host ${host}`);
|
||||
let start = new Date().getTime();
|
||||
var i = 0;
|
||||
// Try for up to 180 seconds (3 minutes).
|
||||
|
|
@ -174,17 +214,17 @@ async function pingHost(host: string): Promise<void> {
|
|||
if (i > 0) {
|
||||
// Exponential backoff on wait time, with maximum 5 second wait.
|
||||
let waitTime = Math.min(Math.pow(1.3, i), 5000);
|
||||
core.debug(`Waiting ${waitTime} milliseconds before pinging`);
|
||||
logDebug(logMode, `Waiting ${waitTime} milliseconds before pinging`);
|
||||
await wait(waitTime);
|
||||
}
|
||||
try {
|
||||
let result = await execSilent("ping host", cmdTailscale, [
|
||||
"ping",
|
||||
"-c",
|
||||
"1",
|
||||
host,
|
||||
]);
|
||||
core.info(`✅ Ping host ${host} reachable via direct connection!`);
|
||||
await execSilent("ping host", cmdTailscale, ["ping", "-c", "1", host], {
|
||||
logMode,
|
||||
});
|
||||
logInfo(
|
||||
logMode,
|
||||
`✅ Ping host ${host} reachable via direct connection!`,
|
||||
);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (
|
||||
|
|
@ -192,19 +232,62 @@ async function pingHost(host: string): Promise<void> {
|
|||
err.stderr.includes("direct connection not established")
|
||||
) {
|
||||
// Relayed connectivity is good enough, we don't want to tie up a CI job waiting for a direct connection.
|
||||
core.info(`✅ Ping host ${host} reachable via DERP!`);
|
||||
logInfo(logMode, `✅ Ping host ${host} reachable via DERP!`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
core.setFailed(`❌ Ping host ${host} did not respond`);
|
||||
process.exit(1);
|
||||
throw new Error(`❌ Ping host ${host} did not respond`);
|
||||
}
|
||||
|
||||
function getLogMode(): LogMode {
|
||||
const logMode = core.getInput("log-mode") || "grouped";
|
||||
if (
|
||||
logMode !== "grouped" &&
|
||||
logMode !== "normal" &&
|
||||
logMode !== "quiet"
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`,
|
||||
);
|
||||
}
|
||||
return logMode;
|
||||
}
|
||||
|
||||
function logInfo(logMode: LogMode, message: string): void {
|
||||
if (logMode !== "quiet") {
|
||||
core.info(message);
|
||||
}
|
||||
}
|
||||
|
||||
function logDebug(logMode: LogMode, message: string): void {
|
||||
if (logMode !== "quiet") {
|
||||
core.debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function withLogGroup<T>(
|
||||
logMode: LogMode,
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
if (logMode !== "grouped") {
|
||||
return fn();
|
||||
}
|
||||
|
||||
core.startGroup(name);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
core.endGroup();
|
||||
}
|
||||
}
|
||||
|
||||
async function getInputs(): Promise<TailscaleConfig> {
|
||||
let ping = core.getInput("ping");
|
||||
let pingHosts = ping?.length > 0 ? ping.split(",") : [];
|
||||
const logMode = getLogMode();
|
||||
|
||||
const authKey = core.getInput("authkey") || "";
|
||||
const oauthSecret = core.getInput("oauth-secret") || "";
|
||||
|
|
@ -237,6 +320,7 @@ async function getInputs(): Promise<TailscaleConfig> {
|
|||
useCache: core.getBooleanInput("use-cache"),
|
||||
sha256Sum: core.getInput("sha256sum") || "",
|
||||
pingHosts: pingHosts,
|
||||
logMode: logMode,
|
||||
};
|
||||
|
||||
if (config.oauthSecret && !config.tags) {
|
||||
|
|
@ -273,6 +357,7 @@ function validateAuth(config: TailscaleConfig): void {
|
|||
async function resolveVersion(
|
||||
version: string,
|
||||
runnerOS: string,
|
||||
logMode: LogMode,
|
||||
): Promise<string> {
|
||||
if (runnerOS === runnerMacOS && version === versionUnstable) {
|
||||
return "main";
|
||||
|
|
@ -281,12 +366,12 @@ async function resolveVersion(
|
|||
if (version === versionLatest || version === versionUnstable) {
|
||||
let path = version === versionUnstable ? versionUnstable : "stable";
|
||||
let pkg = `https://pkgs.tailscale.com/${path}/?mode=json`;
|
||||
const { stdout } = await execSilent(`curl ${pkg}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-s",
|
||||
pkg,
|
||||
]);
|
||||
const { stdout } = await execSilent(
|
||||
`curl ${pkg}`,
|
||||
"curl",
|
||||
["-H", "user-agent:action-setup-tailscale", "-s", pkg],
|
||||
{ logMode },
|
||||
);
|
||||
const response = JSON.parse(stdout);
|
||||
switch (runnerOS) {
|
||||
case runnerLinux:
|
||||
|
|
@ -351,7 +436,8 @@ async function installTailscale(
|
|||
if (config.useCache && cacheKey) {
|
||||
const cacheHit = await cache.restoreCache([toolPath], cacheKey);
|
||||
if (cacheHit) {
|
||||
core.info(
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`,
|
||||
);
|
||||
|
||||
|
|
@ -360,7 +446,7 @@ async function installTailscale(
|
|||
await installTailscaleWindows(config, toolPath, true);
|
||||
} else {
|
||||
// For Linux/macOS, copy binaries to /usr/local/bin
|
||||
await installCachedBinaries(toolPath, runnerOS);
|
||||
await installCachedBinaries(config, toolPath, runnerOS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -379,13 +465,16 @@ async function installTailscale(
|
|||
if (config.useCache && cacheKey) {
|
||||
try {
|
||||
await cache.saveCache([toolPath], cacheKey);
|
||||
core.info(`Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`);
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
const typedError = error as Error;
|
||||
if (typedError.name === cache.ValidationError.name) {
|
||||
throw error;
|
||||
} else if (typedError.name === cache.ReserveCacheError.name) {
|
||||
core.info(typedError.message);
|
||||
logInfo(config.logMode, typedError.message);
|
||||
} else {
|
||||
core.warning(`Cache save failed: ${typedError.message}`);
|
||||
}
|
||||
|
|
@ -417,19 +506,18 @@ async function installTailscaleLinux(
|
|||
// Get SHA256 if not provided
|
||||
if (!config.sha256Sum) {
|
||||
const shaUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz.sha256`;
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-L",
|
||||
shaUrl,
|
||||
"--fail",
|
||||
]);
|
||||
const { stdout } = await execSilent(
|
||||
`curl ${shaUrl}`,
|
||||
"curl",
|
||||
["-H", "user-agent:action-setup-tailscale", "-L", shaUrl, "--fail"],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
config.sha256Sum = stdout.trim();
|
||||
}
|
||||
|
||||
// Download and extract
|
||||
const downloadUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz`;
|
||||
core.info(`Downloading ${downloadUrl}`);
|
||||
logInfo(config.logMode, `Downloading ${downloadUrl}`);
|
||||
|
||||
const tarDest = path.join(xdgCacheDir(), "tailscale.tgz");
|
||||
fs.mkdirSync(path.dirname(tarDest), { recursive: true });
|
||||
|
|
@ -438,8 +526,8 @@ async function installTailscaleLinux(
|
|||
// Verify checksum
|
||||
const actualSha = await calculateFileSha256(tarPath);
|
||||
const expectedSha = config.sha256Sum.trim().toLowerCase();
|
||||
core.info(`Expected sha256: ${expectedSha}`);
|
||||
core.info(`Actual sha256: ${actualSha}`);
|
||||
logInfo(config.logMode, `Expected sha256: ${expectedSha}`);
|
||||
logInfo(config.logMode, `Actual sha256: ${actualSha}`);
|
||||
if (actualSha !== expectedSha) {
|
||||
throw new Error("SHA256 checksum mismatch");
|
||||
}
|
||||
|
|
@ -463,24 +551,31 @@ async function installTailscaleLinux(
|
|||
);
|
||||
|
||||
// Install binaries to /usr/local/bin
|
||||
await execSilent("copy tailscale binaries to /usr/local/bin", "sudo", [
|
||||
"cp",
|
||||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
]);
|
||||
await execSilent(
|
||||
"copy tailscale binaries to /usr/local/bin",
|
||||
"sudo",
|
||||
[
|
||||
"cp",
|
||||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
|
||||
// Make sure they're executable
|
||||
await execSilent("chmod tailscale binary", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled binary", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent(
|
||||
"chmod tailscale binary",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaleFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"chmod tailscaled binary",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaledFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
}
|
||||
|
||||
async function installTailscaleWindows(
|
||||
|
|
@ -497,7 +592,7 @@ async function installTailscaleWindows(
|
|||
if (!fs.existsSync(msiPath)) {
|
||||
throw new Error(`Cached MSI not found at ${msiPath}`);
|
||||
}
|
||||
core.info(`Installing cached MSI from ${msiPath}`);
|
||||
logInfo(config.logMode, `Installing cached MSI from ${msiPath}`);
|
||||
} else {
|
||||
// Fresh download
|
||||
// Determine if stable or unstable
|
||||
|
|
@ -510,13 +605,12 @@ async function installTailscaleWindows(
|
|||
// Get SHA256 if not provided
|
||||
if (!config.sha256Sum) {
|
||||
const shaUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi.sha256`;
|
||||
const { stdout } = await execSilent(`curl ${shaUrl}`, "curl", [
|
||||
"-H",
|
||||
"user-agent:action-setup-tailscale",
|
||||
"-L",
|
||||
shaUrl,
|
||||
"--fail",
|
||||
]);
|
||||
const { stdout } = await execSilent(
|
||||
`curl ${shaUrl}`,
|
||||
"curl",
|
||||
["-H", "user-agent:action-setup-tailscale", "-L", shaUrl, "--fail"],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
config.sha256Sum = stdout.trim();
|
||||
}
|
||||
|
||||
|
|
@ -529,22 +623,28 @@ async function installTailscaleWindows(
|
|||
if (fs.existsSync(msiPath)) {
|
||||
const existingSha = await calculateFileSha256(msiPath);
|
||||
if (existingSha === expectedSha) {
|
||||
core.info(`Using existing MSI at ${msiPath} (checksum verified)`);
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Using existing MSI at ${msiPath} (checksum verified)`,
|
||||
);
|
||||
needsDownload = false;
|
||||
} else {
|
||||
core.info(`Existing MSI checksum mismatch, re-downloading`);
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`Existing MSI checksum mismatch, re-downloading`,
|
||||
);
|
||||
fs.unlinkSync(msiPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (needsDownload) {
|
||||
core.info(`Downloading ${downloadUrl}`);
|
||||
logInfo(config.logMode, `Downloading ${downloadUrl}`);
|
||||
const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath);
|
||||
|
||||
// Verify checksum
|
||||
const actualSha = await calculateFileSha256(downloadedMsiPath);
|
||||
core.info(`Expected sha256: ${expectedSha}`);
|
||||
core.info(`Actual sha256: ${actualSha}`);
|
||||
logInfo(config.logMode, `Expected sha256: ${expectedSha}`);
|
||||
logInfo(config.logMode, `Actual sha256: ${actualSha}`);
|
||||
if (actualSha !== expectedSha) {
|
||||
throw new Error("SHA256 checksum mismatch");
|
||||
}
|
||||
|
|
@ -558,13 +658,18 @@ async function installTailscaleWindows(
|
|||
}
|
||||
|
||||
// Install MSI (same for both fresh and cached)
|
||||
await execSilent("install msi", "msiexec.exe", [
|
||||
"/quiet",
|
||||
`/l*v`,
|
||||
path.join(process.env.RUNNER_TEMP || "", "tailscale.log"),
|
||||
"/i",
|
||||
msiPath,
|
||||
]);
|
||||
await execSilent(
|
||||
"install msi",
|
||||
"msiexec.exe",
|
||||
[
|
||||
"/quiet",
|
||||
`/l*v`,
|
||||
path.join(process.env.RUNNER_TEMP || "", "tailscale.log"),
|
||||
"/i",
|
||||
msiPath,
|
||||
],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
|
||||
// Add to PATH
|
||||
core.addPath("C:\\Program Files\\Tailscale\\");
|
||||
|
|
@ -574,12 +679,14 @@ async function installTailscaleMacOS(
|
|||
config: TailscaleConfig,
|
||||
toolPath: string,
|
||||
): Promise<void> {
|
||||
core.info("Building tailscale from src on macOS...");
|
||||
logInfo(config.logMode, "Building tailscale from src on macOS...");
|
||||
|
||||
// Clone the repo
|
||||
await execSilent(
|
||||
"clone tailscale repo",
|
||||
"git clone https://github.com/tailscale/tailscale.git tailscale",
|
||||
[],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
|
||||
// Checkout the resolved version
|
||||
|
|
@ -589,6 +696,7 @@ async function installTailscaleMacOS(
|
|||
[],
|
||||
{
|
||||
cwd: cmdTailscale,
|
||||
logMode: config.logMode,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -607,31 +715,42 @@ async function installTailscaleMacOS(
|
|||
...process.env,
|
||||
TS_USE_TOOLCHAIN: "1",
|
||||
},
|
||||
logMode: config.logMode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Install binaries to /usr/local/bin
|
||||
await execSilent("copy binaries to /usr/local/bin", "sudo", [
|
||||
"cp",
|
||||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
]);
|
||||
await execSilent(
|
||||
"copy binaries to /usr/local/bin",
|
||||
"sudo",
|
||||
[
|
||||
"cp",
|
||||
path.join(toolPath, cmdTailscale),
|
||||
path.join(toolPath, cmdTailscaled),
|
||||
"/usr/local/bin",
|
||||
],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
|
||||
// Make sure they're executable
|
||||
await execSilent("chmod tailscale", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent(
|
||||
"chmod tailscale",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaleFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"chmod tailscaled",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaledFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
|
||||
core.info("✅ Tailscale installed successfully on macOS from source");
|
||||
logInfo(
|
||||
config.logMode,
|
||||
"✅ Tailscale installed successfully on macOS from source",
|
||||
);
|
||||
}
|
||||
|
||||
async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
|
||||
|
|
@ -651,7 +770,7 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
|
|||
...config.tailscaledArgs.split(" ").filter(Boolean),
|
||||
];
|
||||
|
||||
core.info("Starting tailscaled daemon...");
|
||||
logInfo(config.logMode, "Starting tailscaled daemon...");
|
||||
|
||||
// Start daemon in background
|
||||
const daemon = spawn("sudo", ["-E", cmdTailscaled, ...args], {
|
||||
|
|
@ -676,25 +795,26 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
|
|||
if (daemon.stderr) daemon.stderr.destroy();
|
||||
|
||||
// Poll the local API until daemon is responsive
|
||||
await waitForDaemonReady();
|
||||
await waitForDaemonReady(config.logMode);
|
||||
|
||||
core.info("✅ tailscaled daemon is up and running!");
|
||||
logInfo(config.logMode, "✅ tailscaled daemon is up and running!");
|
||||
}
|
||||
|
||||
async function waitForDaemonReady(): Promise<void> {
|
||||
async function waitForDaemonReady(logMode: LogMode): Promise<void> {
|
||||
const maxWaitMs = 15000; // 15 seconds
|
||||
const pollIntervalMs = 500;
|
||||
let waited = 0;
|
||||
|
||||
core.info("Waiting for tailscaled daemon to become ready...");
|
||||
logInfo(logMode, "Waiting for tailscaled daemon to become ready...");
|
||||
|
||||
var lastErr: any;
|
||||
while (waited < maxWaitMs) {
|
||||
try {
|
||||
const status = await getTailscaleStatus();
|
||||
const status = await getTailscaleStatus(logMode);
|
||||
// If we get any valid response from the API, the daemon is ready
|
||||
if (status) {
|
||||
core.info(
|
||||
logInfo(
|
||||
logMode,
|
||||
`Daemon ready! Initial state: ${status.BackendState || "Unknown"}`,
|
||||
);
|
||||
return;
|
||||
|
|
@ -702,7 +822,7 @@ async function waitForDaemonReady(): Promise<void> {
|
|||
} catch (err) {
|
||||
// Daemon not ready yet, keep polling
|
||||
lastErr = err;
|
||||
core.debug(`Waiting for daemon... (${waited}ms elapsed)`);
|
||||
logDebug(logMode, `Waiting for daemon... (${waited}ms elapsed)`);
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
waited += pollIntervalMs;
|
||||
|
|
@ -723,7 +843,9 @@ async function connectToTailscale(
|
|||
if (runnerOS === runnerWindows) {
|
||||
hostname = `github-${process.env.COMPUTERNAME}`;
|
||||
} else {
|
||||
const { stdout } = await execSilent("hostname", "hostname");
|
||||
const { stdout } = await execSilent("hostname", "hostname", [], {
|
||||
logMode: config.logMode,
|
||||
});
|
||||
hostname = `github-${stdout.trim()}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -777,7 +899,7 @@ async function connectToTailscale(
|
|||
// Retry logic
|
||||
for (let attempt = 1; attempt <= config.retry; attempt++) {
|
||||
try {
|
||||
core.info(`Attempt ${attempt} to bring up Tailscale...`);
|
||||
logInfo(config.logMode, `Attempt ${attempt} to bring up Tailscale...`);
|
||||
|
||||
let execArgs: string[];
|
||||
if (runnerOS === runnerWindows) {
|
||||
|
|
@ -789,14 +911,17 @@ async function connectToTailscale(
|
|||
|
||||
const timeoutMs = parseTimeout(config.timeout);
|
||||
await Promise.race([
|
||||
execSilent("tailscale up", execArgs[0], execArgs.slice(1)),
|
||||
execSilent("tailscale up", execArgs[0], execArgs.slice(1), {
|
||||
logMode: config.logMode,
|
||||
}),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), timeoutMs),
|
||||
),
|
||||
]);
|
||||
|
||||
// Success
|
||||
core.info(
|
||||
logInfo(
|
||||
config.logMode,
|
||||
`✅ Tailscale up command completed successfully on attempt ${attempt}`,
|
||||
);
|
||||
return;
|
||||
|
|
@ -807,7 +932,7 @@ async function connectToTailscale(
|
|||
}
|
||||
|
||||
const sleepTime = attempt * 2; // Reduced from 5 to 2 seconds
|
||||
core.info(`Retrying in ${sleepTime} seconds...`);
|
||||
logInfo(config.logMode, `Retrying in ${sleepTime} seconds...`);
|
||||
await sleep(sleepTime * 1000);
|
||||
}
|
||||
}
|
||||
|
|
@ -862,6 +987,7 @@ function getToolPath(config: TailscaleConfig, runnerOS: string): string {
|
|||
}
|
||||
|
||||
async function installCachedBinaries(
|
||||
config: TailscaleConfig,
|
||||
toolPath: string,
|
||||
runnerOS: string,
|
||||
): Promise<void> {
|
||||
|
|
@ -871,52 +997,62 @@ async function installCachedBinaries(
|
|||
const tailscaledBin = path.join(toolPath, cmdTailscaled);
|
||||
|
||||
if (fs.existsSync(tailscaleBin) && fs.existsSync(tailscaledBin)) {
|
||||
await execSilent("copy tailscale from cache", "sudo", [
|
||||
"cp",
|
||||
tailscaleBin,
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("copy tailscaled from cache", "sudo", [
|
||||
"cp",
|
||||
tailscaledBin,
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscale", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaleFullPath,
|
||||
]);
|
||||
await execSilent("chmod tailscaled", "sudo", [
|
||||
"chmod",
|
||||
"+x",
|
||||
cmdTailscaledFullPath,
|
||||
]);
|
||||
await execSilent(
|
||||
"copy tailscale from cache",
|
||||
"sudo",
|
||||
["cp", tailscaleBin, cmdTailscaleFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"copy tailscaled from cache",
|
||||
"sudo",
|
||||
["cp", tailscaledBin, cmdTailscaledFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"chmod tailscale",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaleFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"chmod tailscaled",
|
||||
"sudo",
|
||||
["chmod", "+x", cmdTailscaledFullPath],
|
||||
{ logMode: config.logMode },
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Cached binaries not found in ${toolPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function configureDNSOnMacOS(status: tailscaleStatus): Promise<void> {
|
||||
async function configureDNSOnMacOS(
|
||||
status: tailscaleStatus,
|
||||
logMode: LogMode,
|
||||
): Promise<void> {
|
||||
if (!status.CurrentTailnet.MagicDNSEnabled) {
|
||||
core.info("MagicDNS is disabled, not configuring DNS");
|
||||
logInfo(logMode, "MagicDNS is disabled, not configuring DNS");
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(
|
||||
logInfo(
|
||||
logMode,
|
||||
`Setting system DNS server to 100.100.100.100 and searchdomains to ${status.CurrentTailnet.MagicDNSSuffix}`,
|
||||
);
|
||||
try {
|
||||
await execSilent("set dns servers", "networksetup", [
|
||||
"-setdnsservers",
|
||||
"Ethernet",
|
||||
"100.100.100.100",
|
||||
]);
|
||||
await execSilent("set search domains", "networksetup", [
|
||||
"-setsearchdomains",
|
||||
"Ethernet",
|
||||
status.CurrentTailnet.MagicDNSSuffix,
|
||||
]);
|
||||
await execSilent(
|
||||
"set dns servers",
|
||||
"networksetup",
|
||||
["-setdnsservers", "Ethernet", "100.100.100.100"],
|
||||
{ logMode },
|
||||
);
|
||||
await execSilent(
|
||||
"set search domains",
|
||||
"networksetup",
|
||||
["-setsearchdomains", "Ethernet", status.CurrentTailnet.MagicDNSSuffix],
|
||||
{ logMode },
|
||||
);
|
||||
} catch (e) {
|
||||
throw Error(`Failed to configure DNS on macOS: ${e}`);
|
||||
}
|
||||
|
|
@ -941,16 +1077,17 @@ async function execSilent(
|
|||
label: string,
|
||||
cmd: string,
|
||||
args?: string[],
|
||||
opts?: {},
|
||||
opts?: exec.ExecOptions & { logMode?: LogMode },
|
||||
): Promise<exec.ExecOutput> {
|
||||
core.info(`▶️ ${label}`);
|
||||
const { logMode = "normal", ...execOpts } = opts || {};
|
||||
logInfo(logMode, `▶️ ${label}`);
|
||||
const out = await exec.getExecOutput(cmd, args, {
|
||||
...opts,
|
||||
silent: !core.isDebug(),
|
||||
...execOpts,
|
||||
silent: logMode === "quiet" || !core.isDebug(),
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (out.exitCode !== 0) {
|
||||
if (!core.isDebug()) {
|
||||
if (logMode === "quiet" || !core.isDebug()) {
|
||||
// When debug logging is off, stderr won't have been written to console, write it now.
|
||||
process.stderr.write(out.stderr);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue