From 73c8a00baedcb1b74c25bb2ba785cf3b02c8d86f Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Thu, 2 Jul 2026 16:40:48 -0700 Subject: [PATCH 1/5] feat: implement log grouping for better output Signed-off-by: Lee Briggs --- README.md | 23 +- action.yml | 6 +- dist/index.js | 318 +++++++++++++-------------- dist/logout/index.js | 159 +++++++++----- src/logout/logout.ts | 186 +++++++++++----- src/main.ts | 513 +++++++++++++++++++++++++++---------------- 6 files changed, 732 insertions(+), 473 deletions(-) diff --git a/README.md b/README.md index 08344dd..385dd93 100644 --- a/README.md +++ b/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: diff --git a/action.yml b/action.yml index 34f6512..19af21a 100644 --- a/action.yml +++ b/action.yml @@ -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' diff --git a/dist/index.js b/dist/index.js index 2e549e5..f3583af 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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.exit(2); + 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; } - // 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); } diff --git a/dist/logout/index.js b/dist/logout/index.js index f99401c..d70e9fc 100644 --- a/dist/logout/index.js +++ b/dist/logout/index.js @@ -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", + ]); + } + 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"]); } - // 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, "✅ 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 diff --git a/src/logout/logout.ts b/src/logout/logout.ts index ca07e62..d16b7ff 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -10,84 +10,152 @@ import * as path from "path"; const runnerWindows = "Windows"; const runnerMacOS = "macOS"; +type LogMode = "grouped" | "normal" | "quiet"; + async function logout(): Promise { 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"); + await execCommand(logMode, "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"]; + } + + 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", + ]); + } 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"]); } - // 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, "✅ Stopped tailscale"); + } catch (error) { + core.warning(`Failed to stop tailscale: ${error}`); } - core.info("✅ 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( + logMode: LogMode, + name: string, + fn: () => Promise, +): Promise { + 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 { + 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 diff --git a/src/main.ts b/src/main.ts index a77592c..6996125 100644 --- a/src/main.ts +++ b/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 { - const { stdout } = await execSilent("get tailscale status", cmdTailscale, [ - "status", - "--json", - ]); +async function getTailscaleStatus( + logMode: LogMode = "normal", +): Promise { + const { stdout } = await execSilent( + "get tailscale status", + cmdTailscale, + ["status", "--json"], + { logMode }, + ); return JSON.parse(stdout); } @@ -97,52 +104,80 @@ async function run(): Promise { // 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 { 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 { - core.info(`Pinging host ${host}`); +async function pingHost(host: string, logMode: LogMode): Promise { + 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 { 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 { 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( + logMode: LogMode, + name: string, + fn: () => Promise, +): Promise { + if (logMode !== "grouped") { + return fn(); + } + + core.startGroup(name); + try { + return await fn(); + } finally { + core.endGroup(); + } } async function getInputs(): Promise { 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 { 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 { 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 { - 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, - ]); - - 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: TailscaleConfig): Promise { @@ -651,7 +770,7 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise { ...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 { 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 { +async function waitForDaemonReady(logMode: LogMode): Promise { 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 { } 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((_, 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 { @@ -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 { +async function configureDNSOnMacOS( + status: tailscaleStatus, + logMode: LogMode, +): Promise { 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 { - 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); } From 404e7c5705f215424f1fd62fface898f7d89baca Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Thu, 2 Jul 2026 16:49:01 -0700 Subject: [PATCH 2/5] format Signed-off-by: Lee Briggs --- src/logout/logout.ts | 6 +----- src/main.ts | 25 ++++++------------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/logout/logout.ts b/src/logout/logout.ts index d16b7ff..8dd1314 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -109,11 +109,7 @@ async function logout(): Promise { function getLogMode(): LogMode { const logMode = core.getInput("log-mode") || "grouped"; - if ( - logMode !== "grouped" && - logMode !== "normal" && - logMode !== "quiet" - ) { + if (logMode !== "grouped" && logMode !== "normal" && logMode !== "quiet") { throw new Error( `Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`, ); diff --git a/src/main.ts b/src/main.ts index 6996125..55075ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -133,13 +133,9 @@ async function run(): Promise { }); } - await withLogGroup( - config.logMode, - "Connecting to Tailscale", - async () => { - await connectToTailscale(config, runnerOS); - }, - ); + await withLogGroup(config.logMode, "Connecting to Tailscale", async () => { + await connectToTailscale(config, runnerOS); + }); let shouldPingHosts = false; await withLogGroup( @@ -196,9 +192,7 @@ async function pingHostsIfNecessary(config: TailscaleConfig): Promise { ",", )} up to 3 minutes each (in parallel) in order to check connectivity`, ); - let pings = config.pingHosts.map((host) => - pingHost(host, config.logMode), - ); + let pings = config.pingHosts.map((host) => pingHost(host, config.logMode)); for (const ping of pings) { await ping; } @@ -221,10 +215,7 @@ async function pingHost(host: string, logMode: LogMode): Promise { await execSilent("ping host", cmdTailscale, ["ping", "-c", "1", host], { logMode, }); - logInfo( - logMode, - `✅ Ping host ${host} reachable via direct connection!`, - ); + logInfo(logMode, `✅ Ping host ${host} reachable via direct connection!`); return; } catch (err) { if ( @@ -243,11 +234,7 @@ async function pingHost(host: string, logMode: LogMode): Promise { function getLogMode(): LogMode { const logMode = core.getInput("log-mode") || "grouped"; - if ( - logMode !== "grouped" && - logMode !== "normal" && - logMode !== "quiet" - ) { + if (logMode !== "grouped" && logMode !== "normal" && logMode !== "quiet") { throw new Error( `Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`, ); From 191eb01caf1fad37292ad6ffc39136ca24700f9f Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Thu, 2 Jul 2026 17:04:08 -0700 Subject: [PATCH 3/5] build Signed-off-by: Lee Briggs --- dist/index.js | 4 +--- dist/logout/index.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dist/index.js b/dist/index.js index f3583af..696a6dd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -52220,9 +52220,7 @@ async function pingHost(host, logMode) { } function getLogMode() { const logMode = core.getInput("log-mode") || "grouped"; - if (logMode !== "grouped" && - logMode !== "normal" && - logMode !== "quiet") { + if (logMode !== "grouped" && logMode !== "normal" && logMode !== "quiet") { throw new Error(`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`); } return logMode; diff --git a/dist/logout/index.js b/dist/logout/index.js index d70e9fc..b50b2fc 100644 --- a/dist/logout/index.js +++ b/dist/logout/index.js @@ -26031,9 +26031,7 @@ async function logout() { } function getLogMode() { const logMode = core.getInput("log-mode") || "grouped"; - if (logMode !== "grouped" && - logMode !== "normal" && - logMode !== "quiet") { + if (logMode !== "grouped" && logMode !== "normal" && logMode !== "quiet") { throw new Error(`Invalid log-mode "${logMode}". Expected "grouped", "normal", or "quiet".`); } return logMode; From ba16990ca09b5f745b03ed05e83b66a722996df7 Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Thu, 2 Jul 2026 18:39:04 -0700 Subject: [PATCH 4/5] catch errors Signed-off-by: Lee Briggs --- dist/index.js | 22 ++++++++++++++++------ dist/logout/index.js | 13 +++++++++++-- src/logout/logout.ts | 13 +++++++++++-- src/main.ts | 26 ++++++++++++++++++-------- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/dist/index.js b/dist/index.js index 696a6dd..3a21bd4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -52696,12 +52696,22 @@ async function connectToTailscale(config, runnerOS) { execArgs = ["sudo", "-E", cmdTailscale, ...upArgs]; } const timeoutMs = parseTimeout(config.timeout); - await Promise.race([ - execSilent("tailscale up", execArgs[0], execArgs.slice(1), { - logMode: config.logMode, - }), - new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)), - ]); + let timeoutHandle; + try { + await Promise.race([ + execSilent("tailscale up", execArgs[0], execArgs.slice(1), { + logMode: config.logMode, + }), + new Promise((_, reject) => { + timeoutHandle = setTimeout(() => reject(new Error("Timeout")), timeoutMs); + }), + ]); + } + finally { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + } // Success logInfo(config.logMode, `✅ Tailscale up command completed successfully on attempt ${attempt}`); return; diff --git a/dist/logout/index.js b/dist/logout/index.js index b50b2fc..1ba8fc3 100644 --- a/dist/logout/index.js +++ b/dist/logout/index.js @@ -26054,10 +26054,19 @@ async function withLogGroup(logMode, name, fn) { } } async function execCommand(logMode, commandLine, args, options) { - return exec.exec(commandLine, args, { + const silent = options?.silent || logMode === "quiet"; + const out = await exec.getExecOutput(commandLine, args, { ...options, - silent: options?.silent || logMode === "quiet", + silent, + ignoreReturnCode: true, }); + if (out.exitCode !== 0) { + if (silent) { + process.stderr.write(out.stderr); + } + throw new Error(`${commandLine} failed with exit code ${out.exitCode}`); + } + return out.exitCode; } // Run the logout function logout().catch((error) => { diff --git a/src/logout/logout.ts b/src/logout/logout.ts index 8dd1314..4a3c198 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -146,10 +146,19 @@ async function execCommand( args?: string[], options?: exec.ExecOptions, ): Promise { - return exec.exec(commandLine, args, { + const silent = options?.silent || logMode === "quiet"; + const out = await exec.getExecOutput(commandLine, args, { ...options, - silent: options?.silent || logMode === "quiet", + silent, + ignoreReturnCode: true, }); + if (out.exitCode !== 0) { + if (silent) { + process.stderr.write(out.stderr); + } + throw new Error(`${commandLine} failed with exit code ${out.exitCode}`); + } + return out.exitCode; } // Run the logout function diff --git a/src/main.ts b/src/main.ts index 55075ea..23733ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -897,14 +897,24 @@ async function connectToTailscale( } const timeoutMs = parseTimeout(config.timeout); - await Promise.race([ - execSilent("tailscale up", execArgs[0], execArgs.slice(1), { - logMode: config.logMode, - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Timeout")), timeoutMs), - ), - ]); + let timeoutHandle: ReturnType | undefined; + try { + await Promise.race([ + execSilent("tailscale up", execArgs[0], execArgs.slice(1), { + logMode: config.logMode, + }), + new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error("Timeout")), + timeoutMs, + ); + }), + ]); + } finally { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + } // Success logInfo( From 5a0d794b306ae171006afad4c14e0ce74d4a8dfe Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Thu, 2 Jul 2026 18:47:15 -0700 Subject: [PATCH 5/5] extract logging function into its own class Signed-off-by: Lee Briggs --- dist/index.js | 259 +++++++++++++++++++++++++++---------------- dist/logout/index.js | 213 +++++++++++++++++++++++------------ src/logging.ts | 92 +++++++++++++++ src/logout/logout.ts | 97 ++++------------ src/main.ts | 89 +++------------ tsconfig.json | 2 +- 6 files changed, 434 insertions(+), 318 deletions(-) create mode 100644 src/logging.ts diff --git a/dist/index.js b/dist/index.js index 3a21bd4..5bddb73 100644 --- a/dist/index.js +++ b/dist/index.js @@ -52031,6 +52031,119 @@ module.exports = { } +/***/ }), + +/***/ 91338: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// Copyright (c) Lee Briggs, Tailscale Inc, & Contributors +// SPDX-License-Identifier: BSD-3-Clause +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ExecError = void 0; +exports.getLogMode = getLogMode; +exports.logInfo = logInfo; +exports.logDebug = logDebug; +exports.withLogGroup = withLogGroup; +exports.execCommand = execCommand; +const core = __importStar(__nccwpck_require__(37484)); +const exec = __importStar(__nccwpck_require__(95236)); +const process = __importStar(__nccwpck_require__(932)); +class ExecError { + constructor(msg, exitCode, stderr) { + this.msg = msg; + this.exitCode = exitCode; + this.stderr = stderr; + } + toString() { + return this.msg; + } +} +exports.ExecError = ExecError; +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 execCommand(commandLine, args, opts) { + const { label, logMode = "normal", ...execOpts } = opts || {}; + if (label) { + logInfo(logMode, `▶️ ${label}`); + } + const silent = execOpts.silent || logMode === "quiet" || !core.isDebug(); + const out = await exec.getExecOutput(commandLine, args, { + ...execOpts, + silent, + ignoreReturnCode: true, + }); + if (out.exitCode !== 0) { + if (silent) { + process.stderr.write(out.stderr); + } + throw new ExecError(`${commandLine} failed with exit code ${out.exitCode}`, out.exitCode, out.stderr); + } + return out; +} + + /***/ }), /***/ 41730: @@ -52076,7 +52189,6 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); const cache = __importStar(__nccwpck_require__(5116)); const core = __importStar(__nccwpck_require__(37484)); -const exec = __importStar(__nccwpck_require__(95236)); const tc = __importStar(__nccwpck_require__(33472)); const child_process_1 = __nccwpck_require__(35317); const crypto = __importStar(__nccwpck_require__(76982)); @@ -52085,6 +52197,7 @@ const os = __importStar(__nccwpck_require__(70857)); const path = __importStar(__nccwpck_require__(16928)); const semver = __importStar(__nccwpck_require__(62088)); const promises_1 = __nccwpck_require__(16460); +const logging_1 = __nccwpck_require__(91338); const cmdTailscale = "tailscale"; const cmdTailscaleFullPath = "/usr/local/bin/tailscale"; const cmdTailscaled = "tailscaled"; @@ -52122,29 +52235,29 @@ async function run() { } // Validate authentication validateAuth(config); - await withLogGroup(config.logMode, "Resolving Tailscale version", async () => { + await (0, logging_1.withLogGroup)(config.logMode, "Resolving Tailscale version", async () => { config.resolvedVersion = await resolveVersion(config.version, runnerOS, config.logMode); - logInfo(config.logMode, `Resolved Tailscale version: ${config.resolvedVersion}`); + (0, logging_1.logInfo)(config.logMode, `Resolved Tailscale version: ${config.resolvedVersion}`); }); // Set architecture config.arch = getTailscaleArch(runnerOS); - await withLogGroup(config.logMode, "Installing Tailscale", async () => { + await (0, logging_1.withLogGroup)(config.logMode, "Installing Tailscale", async () => { await installTailscale(config, runnerOS); }); if (runnerOS !== runnerWindows) { - await withLogGroup(config.logMode, "Starting tailscaled", async () => { + await (0, logging_1.withLogGroup)(config.logMode, "Starting tailscaled", async () => { await startTailscaleDaemon(config); }); } - await withLogGroup(config.logMode, "Connecting to Tailscale", async () => { + await (0, logging_1.withLogGroup)(config.logMode, "Connecting to Tailscale", async () => { await connectToTailscale(config, runnerOS); }); let shouldPingHosts = false; - await withLogGroup(config.logMode, "Checking Tailscale status", async () => { + await (0, logging_1.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!"); + (0, logging_1.logInfo)(config.logMode, "✅ Tailscale is running and connected!"); if (runnerOS === runnerMacOS) { await configureDNSOnMacOS(status, config.logMode); } @@ -52163,7 +52276,7 @@ async function run() { return; } // Still exit successfully since the main connection worked - logInfo(config.logMode, "✅ Tailscale daemon is connected!"); + (0, logging_1.logInfo)(config.logMode, "✅ Tailscale daemon is connected!"); shouldPingHosts = true; } }); @@ -52179,8 +52292,8 @@ async function pingHostsIfNecessary(config) { if (config.pingHosts.length == 0) { return; } - 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`); + await (0, logging_1.withLogGroup)(config.logMode, "Pinging Tailscale hosts", async () => { + (0, logging_1.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; @@ -52188,7 +52301,7 @@ async function pingHostsIfNecessary(config) { }); } async function pingHost(host, logMode) { - logInfo(logMode, `Pinging host ${host}`); + (0, logging_1.logInfo)(logMode, `Pinging host ${host}`); let start = new Date().getTime(); var i = 0; // Try for up to 180 seconds (3 minutes). @@ -52196,21 +52309,21 @@ async function pingHost(host, logMode) { if (i > 0) { // Exponential backoff on wait time, with maximum 5 second wait. let waitTime = Math.min(Math.pow(1.3, i), 5000); - logDebug(logMode, `Waiting ${waitTime} milliseconds before pinging`); + (0, logging_1.logDebug)(logMode, `Waiting ${waitTime} milliseconds before pinging`); await (0, promises_1.setTimeout)(waitTime); } try { await execSilent("ping host", cmdTailscale, ["ping", "-c", "1", host], { logMode, }); - logInfo(logMode, `✅ Ping host ${host} reachable via direct connection!`); + (0, logging_1.logInfo)(logMode, `✅ Ping host ${host} reachable via direct connection!`); return; } catch (err) { - if (err instanceof execError && + if (err instanceof logging_1.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. - logInfo(logMode, `✅ Ping host ${host} reachable via DERP!`); + (0, logging_1.logInfo)(logMode, `✅ Ping host ${host} reachable via DERP!`); return; } } @@ -52218,39 +52331,10 @@ async function pingHost(host, logMode) { } 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 logMode = (0, logging_1.getLogMode)(); const authKey = core.getInput("authkey") || ""; const oauthSecret = core.getInput("oauth-secret") || ""; // Mask sensitive values in logs unless debug mode is enabled @@ -52365,7 +52449,7 @@ async function installTailscale(config, runnerOS) { if (config.useCache && cacheKey) { const cacheHit = await cache.restoreCache([toolPath], cacheKey); if (cacheHit) { - logInfo(config.logMode, `Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`); + (0, logging_1.logInfo)(config.logMode, `Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`); // For Windows, install the cached MSI if (runnerOS === runnerWindows) { await installTailscaleWindows(config, toolPath, true); @@ -52391,7 +52475,7 @@ async function installTailscale(config, runnerOS) { if (config.useCache && cacheKey) { try { await cache.saveCache([toolPath], cacheKey); - logInfo(config.logMode, `Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`); + (0, logging_1.logInfo)(config.logMode, `Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`); } catch (error) { const typedError = error; @@ -52399,7 +52483,7 @@ async function installTailscale(config, runnerOS) { throw error; } else if (typedError.name === cache.ReserveCacheError.name) { - logInfo(config.logMode, typedError.message); + (0, logging_1.logInfo)(config.logMode, typedError.message); } else { core.warning(`Cache save failed: ${typedError.message}`); @@ -52431,15 +52515,15 @@ async function installTailscaleLinux(config, toolPath) { } // Download and extract const downloadUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz`; - logInfo(config.logMode, `Downloading ${downloadUrl}`); + (0, logging_1.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(); - logInfo(config.logMode, `Expected sha256: ${expectedSha}`); - logInfo(config.logMode, `Actual sha256: ${actualSha}`); + (0, logging_1.logInfo)(config.logMode, `Expected sha256: ${expectedSha}`); + (0, logging_1.logInfo)(config.logMode, `Actual sha256: ${actualSha}`); if (actualSha !== expectedSha) { throw new Error("SHA256 checksum mismatch"); } @@ -52470,7 +52554,7 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) { if (!fs.existsSync(msiPath)) { throw new Error(`Cached MSI not found at ${msiPath}`); } - logInfo(config.logMode, `Installing cached MSI from ${msiPath}`); + (0, logging_1.logInfo)(config.logMode, `Installing cached MSI from ${msiPath}`); } else { // Fresh download @@ -52494,21 +52578,21 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) { if (fs.existsSync(msiPath)) { const existingSha = await calculateFileSha256(msiPath); if (existingSha === expectedSha) { - logInfo(config.logMode, `Using existing MSI at ${msiPath} (checksum verified)`); + (0, logging_1.logInfo)(config.logMode, `Using existing MSI at ${msiPath} (checksum verified)`); needsDownload = false; } else { - logInfo(config.logMode, `Existing MSI checksum mismatch, re-downloading`); + (0, logging_1.logInfo)(config.logMode, `Existing MSI checksum mismatch, re-downloading`); fs.unlinkSync(msiPath); } } if (needsDownload) { - logInfo(config.logMode, `Downloading ${downloadUrl}`); + (0, logging_1.logInfo)(config.logMode, `Downloading ${downloadUrl}`); const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath); // Verify checksum const actualSha = await calculateFileSha256(downloadedMsiPath); - logInfo(config.logMode, `Expected sha256: ${expectedSha}`); - logInfo(config.logMode, `Actual sha256: ${actualSha}`); + (0, logging_1.logInfo)(config.logMode, `Expected sha256: ${expectedSha}`); + (0, logging_1.logInfo)(config.logMode, `Actual sha256: ${actualSha}`); if (actualSha !== expectedSha) { throw new Error("SHA256 checksum mismatch"); } @@ -52531,7 +52615,7 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) { core.addPath("C:\\Program Files\\Tailscale\\"); } async function installTailscaleMacOS(config, toolPath) { - logInfo(config.logMode, "Building tailscale from src on macOS..."); + (0, logging_1.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 @@ -52562,7 +52646,7 @@ async function installTailscaleMacOS(config, toolPath) { // Make sure they're executable 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"); + (0, logging_1.logInfo)(config.logMode, "✅ Tailscale installed successfully on macOS from source"); } async function startTailscaleDaemon(config) { const runnerOS = process.env.RUNNER_OS || ""; @@ -52577,7 +52661,7 @@ async function startTailscaleDaemon(config) { ...stateArgs, ...config.tailscaledArgs.split(" ").filter(Boolean), ]; - logInfo(config.logMode, "Starting tailscaled daemon..."); + (0, logging_1.logInfo)(config.logMode, "Starting tailscaled daemon..."); // Start daemon in background const daemon = (0, child_process_1.spawn)("sudo", ["-E", cmdTailscaled, ...args], { detached: true, @@ -52601,27 +52685,27 @@ async function startTailscaleDaemon(config) { daemon.stderr.destroy(); // Poll the local API until daemon is responsive await waitForDaemonReady(config.logMode); - logInfo(config.logMode, "✅ tailscaled daemon is up and running!"); + (0, logging_1.logInfo)(config.logMode, "✅ tailscaled daemon is up and running!"); } async function waitForDaemonReady(logMode) { const maxWaitMs = 15000; // 15 seconds const pollIntervalMs = 500; let waited = 0; - logInfo(logMode, "Waiting for tailscaled daemon to become ready..."); + (0, logging_1.logInfo)(logMode, "Waiting for tailscaled daemon to become ready..."); var lastErr; while (waited < maxWaitMs) { try { const status = await getTailscaleStatus(logMode); // If we get any valid response from the API, the daemon is ready if (status) { - logInfo(logMode, `Daemon ready! Initial state: ${status.BackendState || "Unknown"}`); + (0, logging_1.logInfo)(logMode, `Daemon ready! Initial state: ${status.BackendState || "Unknown"}`); return; } } catch (err) { // Daemon not ready yet, keep polling lastErr = err; - logDebug(logMode, `Waiting for daemon... (${waited}ms elapsed)`); + (0, logging_1.logDebug)(logMode, `Waiting for daemon... (${waited}ms elapsed)`); } await sleep(pollIntervalMs); waited += pollIntervalMs; @@ -52686,7 +52770,7 @@ async function connectToTailscale(config, runnerOS) { // Retry logic for (let attempt = 1; attempt <= config.retry; attempt++) { try { - logInfo(config.logMode, `Attempt ${attempt} to bring up Tailscale...`); + (0, logging_1.logInfo)(config.logMode, `Attempt ${attempt} to bring up Tailscale...`); let execArgs; if (runnerOS === runnerWindows) { execArgs = [cmdTailscale, ...upArgs]; @@ -52713,7 +52797,7 @@ async function connectToTailscale(config, runnerOS) { } } // Success - logInfo(config.logMode, `✅ Tailscale up command completed successfully on attempt ${attempt}`); + (0, logging_1.logInfo)(config.logMode, `✅ Tailscale up command completed successfully on attempt ${attempt}`); return; } catch (error) { @@ -52722,7 +52806,7 @@ async function connectToTailscale(config, runnerOS) { throw error; } const sleepTime = attempt * 2; // Reduced from 5 to 2 seconds - logInfo(config.logMode, `Retrying in ${sleepTime} seconds...`); + (0, logging_1.logInfo)(config.logMode, `Retrying in ${sleepTime} seconds...`); await sleep(sleepTime * 1000); } } @@ -52778,10 +52862,10 @@ async function installCachedBinaries(config, toolPath, runnerOS) { } async function configureDNSOnMacOS(status, logMode) { if (!status.CurrentTailnet.MagicDNSEnabled) { - logInfo(logMode, "MagicDNS is disabled, not configuring DNS"); + (0, logging_1.logInfo)(logMode, "MagicDNS is disabled, not configuring DNS"); return; } - logInfo(logMode, `Setting system DNS server to 100.100.100.100 and searchdomains to ${status.CurrentTailnet.MagicDNSSuffix}`); + (0, logging_1.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"], { logMode }); await execSilent("set search domains", "networksetup", ["-setsearchdomains", "Ethernet", status.CurrentTailnet.MagicDNSSuffix], { logMode }); @@ -52805,31 +52889,10 @@ run(); * @throws execError if exec returned a non-zero status code */ async function execSilent(label, cmd, args, opts) { - const { logMode = "normal", ...execOpts } = opts || {}; - logInfo(logMode, `▶️ ${label}`); - const out = await exec.getExecOutput(cmd, args, { - ...execOpts, - silent: logMode === "quiet" || !core.isDebug(), - ignoreReturnCode: true, + return (0, logging_1.execCommand)(cmd, args, { + ...opts, + label, }); - if (out.exitCode !== 0) { - 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); - } - throw new execError(`${cmd} failed with exit code ${out.exitCode}`, out.exitCode, out.stderr); - } - return out; -} -class execError { - constructor(msg, exitCode, stderr) { - this.msg = msg; - this.exitCode = exitCode; - this.stderr = stderr; - } - toString() { - return this.msg; - } } @@ -53059,6 +53122,14 @@ module.exports = require("perf_hooks"); /***/ }), +/***/ 932: +/***/ ((module) => { + +"use strict"; +module.exports = require("process"); + +/***/ }), + /***/ 83480: /***/ ((module) => { diff --git a/dist/logout/index.js b/dist/logout/index.js index 1ba8fc3..f9c141d 100644 --- a/dist/logout/index.js +++ b/dist/logout/index.js @@ -25895,7 +25895,7 @@ module.exports = { /***/ }), -/***/ 7254: +/***/ 1338: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -25936,36 +25936,142 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ExecError = void 0; +exports.getLogMode = getLogMode; +exports.logInfo = logInfo; +exports.logDebug = logDebug; +exports.withLogGroup = withLogGroup; +exports.execCommand = execCommand; const core = __importStar(__nccwpck_require__(7484)); const exec = __importStar(__nccwpck_require__(5236)); +const process = __importStar(__nccwpck_require__(932)); +class ExecError { + constructor(msg, exitCode, stderr) { + this.msg = msg; + this.exitCode = exitCode; + this.stderr = stderr; + } + toString() { + return this.msg; + } +} +exports.ExecError = ExecError; +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 execCommand(commandLine, args, opts) { + const { label, logMode = "normal", ...execOpts } = opts || {}; + if (label) { + logInfo(logMode, `▶️ ${label}`); + } + const silent = execOpts.silent || logMode === "quiet" || !core.isDebug(); + const out = await exec.getExecOutput(commandLine, args, { + ...execOpts, + silent, + ignoreReturnCode: true, + }); + if (out.exitCode !== 0) { + if (silent) { + process.stderr.write(out.stderr); + } + throw new ExecError(`${commandLine} failed with exit code ${out.exitCode}`, out.exitCode, out.stderr); + } + return out; +} + + +/***/ }), + +/***/ 7254: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// Copyright (c) Lee Briggs, Tailscale Inc, & Contributors +// SPDX-License-Identifier: BSD-3-Clause +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core = __importStar(__nccwpck_require__(7484)); const fs = __importStar(__nccwpck_require__(9896)); const os = __importStar(__nccwpck_require__(857)); const path = __importStar(__nccwpck_require__(6928)); +const logging_1 = __nccwpck_require__(1338); const runnerWindows = "Windows"; const runnerMacOS = "macOS"; async function logout() { try { const runnerOS = process.env.RUNNER_OS || ""; - const logMode = getLogMode(); - await withLogGroup(logMode, "Cleaning up Tailscale", async () => { + const logMode = (0, logging_1.getLogMode)(); + await (0, logging_1.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", - ]); + (0, logging_1.logInfo)(logMode, "Resetting DNS settings on macOS"); + await (0, logging_1.execCommand)("networksetup", ["-setdnsservers", "Ethernet", "Empty"], { logMode }); + await (0, logging_1.execCommand)("networksetup", ["-setsearchdomains", "Ethernet", "Empty"], { logMode }); } - logInfo(logMode, "🔄 Logging out of Tailscale..."); + (0, logging_1.logInfo)(logMode, "🔄 Logging out of Tailscale..."); // Check if tailscale is available first try { - await execCommand(logMode, "tailscale", ["--version"], { + await (0, logging_1.execCommand)("tailscale", ["--version"], { + logMode, silent: true, }); // Determine the correct command based on OS @@ -25977,30 +26083,28 @@ async function logout() { // Linux and macOS - use system-installed binary with sudo execArgs = ["sudo", "-E", "tailscale", "logout"]; } - logInfo(logMode, `Running: ${execArgs.join(" ")}`); + (0, logging_1.logInfo)(logMode, `Running: ${execArgs.join(" ")}`); try { - await execCommand(logMode, execArgs[0], execArgs.slice(1)); - logInfo(logMode, "✅ Successfully logged out of Tailscale"); + await (0, logging_1.execCommand)(execArgs[0], execArgs.slice(1), { logMode }); + (0, logging_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"); + (0, logging_1.logInfo)(logMode, "Your ephemeral node will eventually be cleaned up by Tailscale"); } } catch (error) { - logInfo(logMode, "Tailscale not found or not accessible, skipping logout"); + (0, logging_1.logInfo)(logMode, "Tailscale not found or not accessible, skipping logout"); return; } - logInfo(logMode, "Stopping tailscale"); + (0, logging_1.logInfo)(logMode, "Stopping tailscale"); try { if (runnerOS === runnerWindows) { - await execCommand(logMode, "net", ["stop", "Tailscale"]); - await execCommand(logMode, "taskkill", [ - "/F", - "/IM", - "tailscale-ipn.exe", - ]); + await (0, logging_1.execCommand)("net", ["stop", "Tailscale"], { logMode }); + await (0, logging_1.execCommand)("taskkill", ["/F", "/IM", "tailscale-ipn.exe"], { + logMode, + }); } else { const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR || @@ -26013,11 +26117,11 @@ async function logout() { 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]); + await (0, logging_1.execCommand)("sudo", ["pkill", "-P", pid], { logMode }); // Clean up DNS and routes. - await execCommand(logMode, "sudo", ["tailscaled", "--cleanup"]); + await (0, logging_1.execCommand)("sudo", ["tailscaled", "--cleanup"], { logMode }); } - logInfo(logMode, "✅ Stopped tailscale"); + (0, logging_1.logInfo)(logMode, "✅ Stopped tailscale"); } catch (error) { core.warning(`Failed to stop tailscale: ${error}`); @@ -26029,45 +26133,6 @@ async function logout() { 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) { - const silent = options?.silent || logMode === "quiet"; - const out = await exec.getExecOutput(commandLine, args, { - ...options, - silent, - ignoreReturnCode: true, - }); - if (out.exitCode !== 0) { - if (silent) { - process.stderr.write(out.stderr); - } - throw new Error(`${commandLine} failed with exit code ${out.exitCode}`); - } - return out.exitCode; -} // Run the logout function logout().catch((error) => { // Even if logout fails, don't fail the action @@ -26237,6 +26302,14 @@ module.exports = require("perf_hooks"); /***/ }), +/***/ 932: +/***/ ((module) => { + +"use strict"; +module.exports = require("process"); + +/***/ }), + /***/ 3480: /***/ ((module) => { diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..99219ae --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,92 @@ +// Copyright (c) Lee Briggs, Tailscale Inc, & Contributors +// SPDX-License-Identifier: BSD-3-Clause + +import * as core from "@actions/core"; +import * as exec from "@actions/exec"; +import * as process from "process"; + +export type LogMode = "grouped" | "normal" | "quiet"; + +export class ExecError { + msg: string; + exitCode: number; + stderr: string; + + public constructor(msg: string, exitCode: number, stderr: string) { + this.msg = msg; + this.exitCode = exitCode; + this.stderr = stderr; + } + + public toString(): string { + return this.msg; + } +} + +export 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; +} + +export function logInfo(logMode: LogMode, message: string): void { + if (logMode !== "quiet") { + core.info(message); + } +} + +export function logDebug(logMode: LogMode, message: string): void { + if (logMode !== "quiet") { + core.debug(message); + } +} + +export async function withLogGroup( + logMode: LogMode, + name: string, + fn: () => Promise, +): Promise { + if (logMode !== "grouped") { + return fn(); + } + + core.startGroup(name); + try { + return await fn(); + } finally { + core.endGroup(); + } +} + +export async function execCommand( + commandLine: string, + args?: string[], + opts?: exec.ExecOptions & { label?: string; logMode?: LogMode }, +): Promise { + const { label, logMode = "normal", ...execOpts } = opts || {}; + if (label) { + logInfo(logMode, `▶️ ${label}`); + } + + const silent = execOpts.silent || logMode === "quiet" || !core.isDebug(); + const out = await exec.getExecOutput(commandLine, args, { + ...execOpts, + silent, + ignoreReturnCode: true, + }); + if (out.exitCode !== 0) { + if (silent) { + process.stderr.write(out.stderr); + } + throw new ExecError( + `${commandLine} failed with exit code ${out.exitCode}`, + out.exitCode, + out.stderr, + ); + } + return out; +} diff --git a/src/logout/logout.ts b/src/logout/logout.ts index 4a3c198..083b86c 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -2,16 +2,14 @@ // SPDX-License-Identifier: BSD-3-Clause import * as core from "@actions/core"; -import * as exec from "@actions/exec"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import { execCommand, getLogMode, logInfo, withLogGroup } from "../logging"; const runnerWindows = "Windows"; const runnerMacOS = "macOS"; -type LogMode = "grouped" | "normal" | "quiet"; - async function logout(): Promise { try { const runnerOS = process.env.RUNNER_OS || ""; @@ -21,23 +19,24 @@ async function logout(): Promise { 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", - ]); + await execCommand( + "networksetup", + ["-setdnsservers", "Ethernet", "Empty"], + { logMode }, + ); + await execCommand( + "networksetup", + ["-setsearchdomains", "Ethernet", "Empty"], + { logMode }, + ); } logInfo(logMode, "🔄 Logging out of Tailscale..."); // Check if tailscale is available first try { - await execCommand(logMode, "tailscale", ["--version"], { + await execCommand("tailscale", ["--version"], { + logMode, silent: true, }); @@ -53,7 +52,7 @@ async function logout(): Promise { logInfo(logMode, `Running: ${execArgs.join(" ")}`); try { - await execCommand(logMode, execArgs[0], execArgs.slice(1)); + await execCommand(execArgs[0], execArgs.slice(1), { logMode }); logInfo(logMode, "✅ Successfully logged out of Tailscale"); } catch (error) { // Don't fail the action if logout fails - it's just cleanup @@ -74,12 +73,10 @@ async function logout(): Promise { logInfo(logMode, "Stopping tailscale"); try { if (runnerOS === runnerWindows) { - await execCommand(logMode, "net", ["stop", "Tailscale"]); - await execCommand(logMode, "taskkill", [ - "/F", - "/IM", - "tailscale-ipn.exe", - ]); + await execCommand("net", ["stop", "Tailscale"], { logMode }); + await execCommand("taskkill", ["/F", "/IM", "tailscale-ipn.exe"], { + logMode, + }); } else { const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR || @@ -92,9 +89,9 @@ async function logout(): Promise { 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]); + await execCommand("sudo", ["pkill", "-P", pid], { logMode }); // Clean up DNS and routes. - await execCommand(logMode, "sudo", ["tailscaled", "--cleanup"]); + await execCommand("sudo", ["tailscaled", "--cleanup"], { logMode }); } logInfo(logMode, "✅ Stopped tailscale"); } catch (error) { @@ -107,60 +104,6 @@ async function logout(): Promise { } } -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( - logMode: LogMode, - name: string, - fn: () => Promise, -): Promise { - 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 { - const silent = options?.silent || logMode === "quiet"; - const out = await exec.getExecOutput(commandLine, args, { - ...options, - silent, - ignoreReturnCode: true, - }); - if (out.exitCode !== 0) { - if (silent) { - process.stderr.write(out.stderr); - } - throw new Error(`${commandLine} failed with exit code ${out.exitCode}`); - } - return out.exitCode; -} - // Run the logout function logout().catch((error) => { // Even if logout fails, don't fail the action diff --git a/src/main.ts b/src/main.ts index 23733ca..dfb92bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,15 @@ import * as os from "os"; import * as path from "path"; import * as semver from "semver"; import { setTimeout as wait } from "timers/promises"; +import { + ExecError, + execCommand, + getLogMode, + logDebug, + logInfo, + withLogGroup, +} from "./logging"; +import type { LogMode } from "./logging"; const cmdTailscale = "tailscale"; const cmdTailscaleFullPath = "/usr/local/bin/tailscale"; @@ -34,8 +43,6 @@ function xdgRuntimeDir(): string { const versionLatest = "latest"; const versionUnstable = "unstable"; -type LogMode = "grouped" | "normal" | "quiet"; - interface TailscaleConfig { version: string; resolvedVersion: string; @@ -219,7 +226,7 @@ async function pingHost(host: string, logMode: LogMode): Promise { return; } catch (err) { if ( - err instanceof execError && + 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. @@ -232,45 +239,6 @@ async function pingHost(host: string, logMode: LogMode): Promise { 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( - logMode: LogMode, - name: string, - fn: () => Promise, -): Promise { - if (logMode !== "grouped") { - return fn(); - } - - core.startGroup(name); - try { - return await fn(); - } finally { - core.endGroup(); - } -} - async function getInputs(): Promise { let ping = core.getInput("ping"); let pingHosts = ping?.length > 0 ? ping.split(",") : []; @@ -1076,39 +1044,8 @@ async function execSilent( args?: string[], opts?: exec.ExecOptions & { logMode?: LogMode }, ): Promise { - const { logMode = "normal", ...execOpts } = opts || {}; - logInfo(logMode, `▶️ ${label}`); - const out = await exec.getExecOutput(cmd, args, { - ...execOpts, - silent: logMode === "quiet" || !core.isDebug(), - ignoreReturnCode: true, + return execCommand(cmd, args, { + ...opts, + label, }); - if (out.exitCode !== 0) { - 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); - } - throw new execError( - `${cmd} failed with exit code ${out.exitCode}`, - out.exitCode, - out.stderr, - ); - } - return out; -} - -class execError { - msg: string; - exitCode: number; - stderr: string; - - public constructor(msg: string, exitCode: number, stderr: string) { - this.msg = msg; - this.exitCode = exitCode; - this.stderr = stderr; - } - - public toString(): string { - return this.msg; - } } diff --git a/tsconfig.json b/tsconfig.json index ca4ac69..8f52031 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -43,7 +43,7 @@ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ + "types": ["node"], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */