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..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"; @@ -52102,11 +52215,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 +52235,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 (0, logging_1.withLogGroup)(config.logMode, "Resolving Tailscale version", async () => { + config.resolvedVersion = await resolveVersion(config.version, runnerOS, config.logMode); + (0, logging_1.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 (0, logging_1.withLogGroup)(config.logMode, "Installing Tailscale", async () => { + await installTailscale(config, runnerOS); + }); if (runnerOS !== runnerWindows) { - await startTailscaleDaemon(config); + await (0, logging_1.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 (0, logging_1.withLogGroup)(config.logMode, "Connecting to Tailscale", async () => { + await connectToTailscale(config, runnerOS); + }); + let shouldPingHosts = false; + await (0, logging_1.withLogGroup)(config.logMode, "Checking Tailscale status", async () => { + try { + const status = await getTailscaleStatus(config.logMode); + if (status.BackendState === "Running") { + (0, logging_1.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 + (0, logging_1.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 +52292,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 (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; + } + }); } -async function pingHost(host) { - core.info(`Pinging host ${host}`); +async function pingHost(host, logMode) { + (0, logging_1.logInfo)(logMode, `Pinging host ${host}`); let start = new Date().getTime(); var i = 0; // Try for up to 180 seconds (3 minutes). @@ -52191,35 +52309,32 @@ 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`); + (0, logging_1.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, + }); + (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. - core.info(`✅ Ping host ${host} reachable via DERP!`); + (0, logging_1.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`); } async function getInputs() { let ping = core.getInput("ping"); let pingHosts = ping?.length > 0 ? ping.split(",") : []; + 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 @@ -52249,6 +52364,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 +52383,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 +52449,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}`); + (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); } else { // For Linux/macOS, copy binaries to /usr/local/bin - await installCachedBinaries(toolPath, runnerOS); + await installCachedBinaries(config, toolPath, runnerOS); } return; } @@ -52364,7 +52475,7 @@ async function installTailscale(config, runnerOS) { if (config.useCache && cacheKey) { try { await cache.saveCache([toolPath], cacheKey); - core.info(`Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`); + (0, logging_1.logInfo)(config.logMode, `Cached Tailscale ${config.resolvedVersion} at: ${toolPath}`); } catch (error) { const typedError = error; @@ -52372,7 +52483,7 @@ async function installTailscale(config, runnerOS) { throw error; } else if (typedError.name === cache.ReserveCacheError.name) { - core.info(typedError.message); + (0, logging_1.logInfo)(config.logMode, typedError.message); } else { core.warning(`Cache save failed: ${typedError.message}`); @@ -52399,26 +52510,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}`); + (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(); - core.info(`Expected sha256: ${expectedSha}`); - core.info(`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"); } @@ -52435,18 +52540,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 +52554,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}`); + (0, logging_1.logInfo)(config.logMode, `Installing cached MSI from ${msiPath}`); } else { // Fresh download @@ -52470,13 +52567,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 +52578,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)`); + (0, logging_1.logInfo)(config.logMode, `Using existing MSI at ${msiPath} (checksum verified)`); needsDownload = false; } else { - core.info(`Existing MSI checksum mismatch, re-downloading`); + (0, logging_1.logInfo)(config.logMode, `Existing MSI checksum mismatch, re-downloading`); fs.unlinkSync(msiPath); } } if (needsDownload) { - core.info(`Downloading ${downloadUrl}`); + (0, logging_1.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}`); + (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"); } @@ -52519,17 +52610,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..."); + (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"); + 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 +52633,7 @@ async function installTailscaleMacOS(config, toolPath) { ...process.env, TS_USE_TOOLCHAIN: "1", }, + logMode: config.logMode, }); } // Install binaries to /usr/local/bin @@ -52549,19 +52642,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 }); + (0, logging_1.logInfo)(config.logMode, "✅ Tailscale installed successfully on macOS from source"); } async function startTailscaleDaemon(config) { const runnerOS = process.env.RUNNER_OS || ""; @@ -52576,7 +52661,7 @@ async function startTailscaleDaemon(config) { ...stateArgs, ...config.tailscaledArgs.split(" ").filter(Boolean), ]; - core.info("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, @@ -52599,28 +52684,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); + (0, logging_1.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..."); + (0, logging_1.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"}`); + (0, logging_1.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)`); + (0, logging_1.logDebug)(logMode, `Waiting for daemon... (${waited}ms elapsed)`); } await sleep(pollIntervalMs); waited += pollIntervalMs; @@ -52635,7 +52720,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 +52770,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...`); + (0, logging_1.logInfo)(config.logMode, `Attempt ${attempt} to bring up Tailscale...`); let execArgs; if (runnerOS === runnerWindows) { execArgs = [cmdTailscale, ...upArgs]; @@ -52693,12 +52780,24 @@ 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)), - 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 - core.info(`✅ 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) { @@ -52707,7 +52806,7 @@ async function connectToTailscale(config, runnerOS) { throw error; } const sleepTime = attempt * 2; // Reduced from 5 to 2 seconds - core.info(`Retrying in ${sleepTime} seconds...`); + (0, logging_1.logInfo)(config.logMode, `Retrying in ${sleepTime} seconds...`); await sleep(sleepTime * 1000); } } @@ -52745,55 +52844,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"); + (0, logging_1.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}`); + (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", - ]); - 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,30 +52889,10 @@ run(); * @throws execError if exec returned a non-zero status code */ async function execSilent(label, cmd, args, opts) { - core.info(`▶️ ${label}`); - const out = await exec.getExecOutput(cmd, args, { + return (0, logging_1.execCommand)(cmd, args, { ...opts, - silent: !core.isDebug(), - ignoreReturnCode: true, + label, }); - if (out.exitCode !== 0) { - if (!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; - } } @@ -53067,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 f99401c..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,80 +25936,197 @@ 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 || ""; - 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 = (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. + (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 }); } - else { - // Linux and macOS - use system-installed binary with sudo - execArgs = ["sudo", "-E", "tailscale", "logout"]; - } - core.info(`Running: ${execArgs.join(" ")}`); + (0, logging_1.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 (0, logging_1.execCommand)("tailscale", ["--version"], { + logMode, + 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"]; + } + (0, logging_1.logInfo)(logMode, `Running: ${execArgs.join(" ")}`); + try { + 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}`); + (0, logging_1.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"); + (0, logging_1.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"); + (0, logging_1.logInfo)(logMode, "Stopping tailscale"); + try { + if (runnerOS === runnerWindows) { + await (0, logging_1.execCommand)("net", ["stop", "Tailscale"], { logMode }); + await (0, logging_1.execCommand)("taskkill", ["/F", "/IM", "tailscale-ipn.exe"], { + logMode, + }); } - // The pid is actually the pid of the `sudo` parent of tailscaled, so use pkill -P to kill children of that parent - await exec.exec("sudo", ["pkill", "-P", pid]); - // Clean up DNS and routes. - await exec.exec("sudo", ["tailscaled", "--cleanup"]); + else { + const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR || + process.env.XDG_CACHE_HOME || + path.join(os.homedir(), ".cache"); + const pid = fs + .readFileSync(path.join(xdgRuntimeDir, "tailscaled.pid")) + .toString(); + if (pid === "") { + throw new Error("pid file empty"); + } + // The pid is actually the pid of the `sudo` parent of tailscaled, so use pkill -P to kill children of that parent + await (0, logging_1.execCommand)("sudo", ["pkill", "-P", pid], { logMode }); + // Clean up DNS and routes. + await (0, logging_1.execCommand)("sudo", ["tailscaled", "--cleanup"], { logMode }); + } + (0, logging_1.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 @@ -26185,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 ca07e62..083b86c 100644 --- a/src/logout/logout.ts +++ b/src/logout/logout.ts @@ -2,10 +2,10 @@ // 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"; @@ -13,75 +13,91 @@ const runnerMacOS = "macOS"; 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", - ]); - } + 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( + "networksetup", + ["-setdnsservers", "Ethernet", "Empty"], + { logMode }, + ); + await execCommand( + "networksetup", + ["-setsearchdomains", "Ethernet", "Empty"], + { logMode }, + ); + } - core.info("🔄 Logging out of Tailscale..."); + logInfo(logMode, "🔄 Logging out of Tailscale..."); - // Check if tailscale is available first - try { - await exec.exec("tailscale", ["--version"], { silent: true }); + // Check if tailscale is available first + try { + await execCommand("tailscale", ["--version"], { + logMode, + 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"]; - } + // Determine the correct command based on OS + let execArgs: string[]; + if (runnerOS === runnerWindows) { + execArgs = ["tailscale", "logout"]; + } else { + // Linux and macOS - use system-installed binary with sudo + execArgs = ["sudo", "-E", "tailscale", "logout"]; + } - core.info(`Running: ${execArgs.join(" ")}`); + logInfo(logMode, `Running: ${execArgs.join(" ")}`); - try { - await exec.exec(execArgs[0], execArgs.slice(1)); - core.info("✅ Successfully logged out of Tailscale"); + try { + 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 + 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("net", ["stop", "Tailscale"], { logMode }); + await execCommand("taskkill", ["/F", "/IM", "tailscale-ipn.exe"], { + logMode, + }); + } 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("sudo", ["pkill", "-P", pid], { logMode }); + // Clean up DNS and routes. + await execCommand("sudo", ["tailscaled", "--cleanup"], { logMode }); } - // 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}`); diff --git a/src/main.ts b/src/main.ts index a77592c..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"; @@ -52,6 +61,7 @@ interface TailscaleConfig { useCache: boolean; sha256Sum: string; pingHosts: string[]; + logMode: LogMode; } type tailnetInfo = { @@ -65,11 +75,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 +111,76 @@ 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); - - // 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; + } + } 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 +192,22 @@ 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,37 +215,34 @@ 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 ( - 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. - 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`); } 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 +275,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 +312,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 +321,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 +391,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 +401,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 +420,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 +461,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 +481,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 +506,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 +547,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 +560,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 +578,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 +613,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 +634,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 +651,7 @@ async function installTailscaleMacOS( [], { cwd: cmdTailscale, + logMode: config.logMode, }, ); @@ -607,31 +670,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 +725,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 +750,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 +777,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 +798,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 +854,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) { @@ -788,15 +865,28 @@ async function connectToTailscale( } const timeoutMs = parseTimeout(config.timeout); - await Promise.race([ - execSilent("tailscale up", execArgs[0], execArgs.slice(1)), - 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 - core.info( + logInfo( + config.logMode, `✅ Tailscale up command completed successfully on attempt ${attempt}`, ); return; @@ -807,7 +897,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 +952,7 @@ function getToolPath(config: TailscaleConfig, runnerOS: string): string { } async function installCachedBinaries( + config: TailscaleConfig, toolPath: string, runnerOS: string, ): Promise { @@ -871,52 +962,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,40 +1042,10 @@ async function execSilent( label: string, cmd: string, args?: string[], - opts?: {}, + opts?: exec.ExecOptions & { logMode?: LogMode }, ): Promise { - core.info(`▶️ ${label}`); - const out = await exec.getExecOutput(cmd, args, { + return execCommand(cmd, args, { ...opts, - silent: !core.isDebug(), - ignoreReturnCode: true, + label, }); - if (out.exitCode !== 0) { - if (!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. */