add dependency on prettier and apply autoformatting

Updates tailscale/corp#30483

Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
Percy Wegmann 2025-09-27 12:43:19 -05:00 committed by Percy Wegmann
parent 391d5e0792
commit 39ac0a5882
6 changed files with 546 additions and 385 deletions

349
dist/index.js vendored
View file

@ -41113,37 +41113,40 @@ 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 cache = __importStar(__nccwpck_require__(5116));
const fs = __importStar(__nccwpck_require__(79896));
const path = __importStar(__nccwpck_require__(16928));
const os = __importStar(__nccwpck_require__(70857));
const child_process_1 = __nccwpck_require__(35317);
const crypto = __importStar(__nccwpck_require__(76982));
const fs = __importStar(__nccwpck_require__(79896));
const http = __importStar(__nccwpck_require__(58611));
const os = __importStar(__nccwpck_require__(70857));
const path = __importStar(__nccwpck_require__(16928));
// Cross-platform Tailscale local API status check
async function getTailscaleStatus() {
const platform = os.platform();
if (platform === 'win32') {
if (platform === "win32") {
// Windows: use tailscale status command
const { stdout } = await exec.getExecOutput('tailscale', ['status', '--json']);
const { stdout } = await exec.getExecOutput("tailscale", [
"status",
"--json",
]);
return JSON.parse(stdout);
}
else if (platform === 'darwin') {
else if (platform === "darwin") {
// macOS: use /var/run/tailscaled.socket
return new Promise((resolve, reject) => {
const options = {
socketPath: '/var/run/tailscaled.socket',
path: '/localapi/v0/status',
method: 'GET',
headers: { Host: 'local-tailscaled.sock' },
socketPath: "/var/run/tailscaled.socket",
path: "/localapi/v0/status",
method: "GET",
headers: { Host: "local-tailscaled.sock" },
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
}
@ -41155,9 +41158,9 @@ async function getTailscaleStatus() {
// Set timeout to prevent hanging
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timeout'));
reject(new Error("Request timeout"));
});
req.on('error', reject);
req.on("error", reject);
req.end();
});
}
@ -41165,15 +41168,15 @@ async function getTailscaleStatus() {
// Linux: use Unix socket
return new Promise((resolve, reject) => {
const options = {
socketPath: '/run/tailscale/tailscaled.sock',
path: '/localapi/v0/status',
method: 'GET',
headers: { Host: 'local-tailscaled.sock' },
socketPath: "/run/tailscale/tailscaled.sock",
path: "/localapi/v0/status",
method: "GET",
headers: { Host: "local-tailscaled.sock" },
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
}
@ -41185,9 +41188,9 @@ async function getTailscaleStatus() {
// Set timeout to prevent hanging
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timeout'));
reject(new Error("Request timeout"));
});
req.on('error', reject);
req.on("error", reject);
req.end();
});
}
@ -41195,9 +41198,9 @@ async function getTailscaleStatus() {
async function run() {
try {
// Validate runner OS
const runnerOS = process.env.RUNNER_OS || '';
if (!['Linux', 'Windows', 'macOS'].includes(runnerOS)) {
throw new Error('Support Linux, Windows, and macOS Only');
const runnerOS = process.env.RUNNER_OS || "";
if (!["Linux", "Windows", "macOS"].includes(runnerOS)) {
throw new Error("Support Linux, Windows, and macOS Only");
}
// Get and validate inputs
const config = await getInputs();
@ -41211,7 +41214,7 @@ async function run() {
// Install Tailscale
await installTailscale(config, runnerOS);
// Start daemon (non-Windows only)
if (runnerOS !== 'Windows') {
if (runnerOS !== "Windows") {
await startTailscaleDaemon(config);
}
// Connect to Tailscale
@ -41220,8 +41223,8 @@ async function run() {
try {
const status = await getTailscaleStatus();
core.debug(`Tailscale status: ${JSON.stringify(status)}`);
if (status.BackendState === 'Running') {
core.info('✅ Tailscale is running and connected!');
if (status.BackendState === "Running") {
core.info("✅ Tailscale is running and connected!");
// Explicitly exit to prevent hanging
process.exit(0);
}
@ -41233,7 +41236,7 @@ async function run() {
catch (err) {
core.warning(`Failed to get Tailscale status: ${err}`);
// Still exit successfully since the main connection worked
core.info('✅ Tailscale connection completed successfully!');
core.info("✅ Tailscale connection completed successfully!");
process.exit(0);
}
}
@ -41243,34 +41246,35 @@ async function run() {
}
async function getInputs() {
return {
version: core.getInput('version') || '1.82.0',
resolvedVersion: '',
arch: '',
authKey: core.getInput('authkey') || '',
oauthClientId: core.getInput('oauth-client-id') || '',
oauthSecret: core.getInput('oauth-client-secret') || '',
tags: core.getInput('tags') || '',
hostname: core.getInput('hostname') || '',
args: core.getInput('args') || '',
tailscaledArgs: core.getInput('tailscaled-args') || '',
stateDir: core.getInput('statedir') || '',
timeout: core.getInput('timeout') || '60s', // Reduced from 2m to 60s
retry: parseInt(core.getInput('retry') || '5'),
useCache: core.getBooleanInput('use-cache'),
sha256Sum: core.getInput('sha256sum') || ''
version: core.getInput("version") || "1.82.0",
resolvedVersion: "",
arch: "",
authKey: core.getInput("authkey") || "",
oauthClientId: core.getInput("oauth-client-id") || "",
oauthSecret: core.getInput("oauth-client-secret") || "",
tags: core.getInput("tags") || "",
hostname: core.getInput("hostname") || "",
args: core.getInput("args") || "",
tailscaledArgs: core.getInput("tailscaled-args") || "",
stateDir: core.getInput("statedir") || "",
timeout: core.getInput("timeout") || "60s", // Reduced from 2m to 60s
retry: parseInt(core.getInput("retry") || "5"),
useCache: core.getBooleanInput("use-cache"),
sha256Sum: core.getInput("sha256sum") || "",
};
}
function validateAuth(config) {
if (!config.authKey && (!config.oauthSecret || !config.tags)) {
throw new Error('OAuth identity empty, please provide either an auth key or OAuth secret and tags.');
throw new Error("OAuth identity empty, please provide either an auth key or OAuth secret and tags.");
}
}
async function resolveVersion(version) {
if (version === 'latest') {
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-s',
'https://pkgs.tailscale.com/stable/?mode=json'
if (version === "latest") {
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-s",
"https://pkgs.tailscale.com/stable/?mode=json",
]);
const response = JSON.parse(stdout);
return response.Version;
@ -41278,23 +41282,30 @@ async function resolveVersion(version) {
return version;
}
function getTailscaleArch(runnerOS) {
const runnerArch = process.env.RUNNER_ARCH || '';
if (runnerOS === 'Linux') {
const runnerArch = process.env.RUNNER_ARCH || "";
if (runnerOS === "Linux") {
switch (runnerArch) {
case 'ARM64': return 'arm64';
case 'ARM': return 'arm';
case 'X86': return '386';
default: return 'amd64';
case "ARM64":
return "arm64";
case "ARM":
return "arm";
case "X86":
return "386";
default:
return "amd64";
}
}
else if (runnerOS === 'Windows') {
else if (runnerOS === "Windows") {
switch (runnerArch) {
case 'ARM64': return 'arm64';
case 'X86': return 'x86';
default: return 'amd64';
case "ARM64":
return "arm64";
case "X86":
return "x86";
default:
return "amd64";
}
}
return 'amd64';
return "amd64";
}
async function installTailscale(config, runnerOS) {
const cacheKey = generateCacheKey(config, runnerOS);
@ -41305,7 +41316,7 @@ async function installTailscale(config, runnerOS) {
if (cacheHit) {
core.info(`Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`);
// For Windows, install the cached MSI
if (runnerOS === 'Windows') {
if (runnerOS === "Windows") {
await installTailscaleWindows(config, toolPath, true);
}
else {
@ -41316,13 +41327,13 @@ async function installTailscale(config, runnerOS) {
}
}
// Install fresh if not cached
if (runnerOS === 'Linux') {
if (runnerOS === "Linux") {
await installTailscaleLinux(config, toolPath);
}
else if (runnerOS === 'Windows') {
else if (runnerOS === "Windows") {
await installTailscaleWindows(config, toolPath);
}
else if (runnerOS === 'macOS') {
else if (runnerOS === "macOS") {
await installTailscaleMacOS(config, toolPath);
}
// Save to cache after installation
@ -41347,60 +41358,66 @@ async function installTailscale(config, runnerOS) {
}
async function calculateFileSha256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const hash = crypto.createHash("sha256");
const stream = fs.createReadStream(filePath);
stream.on('error', err => reject(err));
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
stream.on("error", (err) => reject(err));
stream.on("data", (chunk) => hash.update(chunk));
stream.on("end", () => resolve(hash.digest("hex").toLowerCase()));
});
}
async function installTailscaleLinux(config, toolPath) {
// Determine if stable or unstable
const minor = parseInt(config.resolvedVersion.split('.')[1]);
const minor = parseInt(config.resolvedVersion.split(".")[1]);
const isStable = minor % 2 === 0;
const baseUrl = isStable ? 'https://pkgs.tailscale.com/stable' : 'https://pkgs.tailscale.com/unstable';
const baseUrl = isStable
? "https://pkgs.tailscale.com/stable"
: "https://pkgs.tailscale.com/unstable";
// Get SHA256 if not provided
if (!config.sha256Sum) {
const shaUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz.sha256`;
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-L', shaUrl, '--fail'
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-L",
shaUrl,
"--fail",
]);
config.sha256Sum = stdout.trim();
}
// Download and extract
const downloadUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz`;
core.info(`Downloading ${downloadUrl}`);
const tarPath = await tc.downloadTool(downloadUrl, 'tailscale.tgz');
const tarPath = await tc.downloadTool(downloadUrl, "tailscale.tgz");
// Verify checksum
const actualSha = await calculateFileSha256(tarPath);
const expectedSha = config.sha256Sum.trim().toLowerCase();
core.info(`Expected sha256: ${expectedSha}`);
core.info(`Actual sha256: ${actualSha}`);
if (actualSha !== expectedSha) {
throw new Error('SHA256 checksum mismatch');
throw new Error("SHA256 checksum mismatch");
}
// Extract to tool path
const extractedPath = await tc.extractTar(tarPath, undefined, 'xz');
const extractedPath = await tc.extractTar(tarPath, undefined, "xz");
const extractedDir = path.join(extractedPath, `tailscale_${config.resolvedVersion}_${config.arch}`);
// Create tool directory and copy binaries there for caching
fs.mkdirSync(toolPath, { recursive: true });
fs.copyFileSync(path.join(extractedDir, 'tailscale'), path.join(toolPath, 'tailscale'));
fs.copyFileSync(path.join(extractedDir, 'tailscaled'), path.join(toolPath, 'tailscaled'));
fs.copyFileSync(path.join(extractedDir, "tailscale"), path.join(toolPath, "tailscale"));
fs.copyFileSync(path.join(extractedDir, "tailscaled"), path.join(toolPath, "tailscaled"));
// Install binaries to /usr/local/bin
await exec.exec('sudo', ['cp',
path.join(toolPath, 'tailscale'),
path.join(toolPath, 'tailscaled'),
'/usr/local/bin'
await exec.exec("sudo", [
"cp",
path.join(toolPath, "tailscale"),
path.join(toolPath, "tailscaled"),
"/usr/local/bin",
]);
// Make sure they're executable
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
}
async function installTailscaleWindows(config, toolPath, fromCache = false) {
// Create tool directory
fs.mkdirSync(toolPath, { recursive: true });
const msiPath = path.join(toolPath, 'tailscale.msi');
const msiPath = path.join(toolPath, "tailscale.msi");
if (fromCache) {
// Installing from cached MSI
if (!fs.existsSync(msiPath)) {
@ -41411,15 +41428,20 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
else {
// Fresh download
// Determine if stable or unstable
const minor = parseInt(config.resolvedVersion.split('.')[1]);
const minor = parseInt(config.resolvedVersion.split(".")[1]);
const isStable = minor % 2 === 0;
const baseUrl = isStable ? 'https://pkgs.tailscale.com/stable' : 'https://pkgs.tailscale.com/unstable';
const baseUrl = isStable
? "https://pkgs.tailscale.com/stable"
: "https://pkgs.tailscale.com/unstable";
// Get SHA256 if not provided
if (!config.sha256Sum) {
const shaUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi.sha256`;
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-L', shaUrl, '--fail'
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-L",
shaUrl,
"--fail",
]);
config.sha256Sum = stdout.trim();
}
@ -41433,7 +41455,7 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
core.info(`Expected sha256: ${expectedSha}`);
core.info(`Actual sha256: ${actualSha}`);
if (actualSha !== expectedSha) {
throw new Error('SHA256 checksum mismatch');
throw new Error("SHA256 checksum mismatch");
}
// Keep the MSI file in toolPath for caching (don't delete it)
// The downloadedMsiPath is in temp, but we want to keep it in toolPath
@ -41442,63 +41464,70 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) {
}
}
// Install MSI (same for both fresh and cached)
await exec.exec('msiexec.exe', [
'/quiet',
`/l*v`, path.join(process.env.RUNNER_TEMP || '', 'tailscale.log'),
'/i', msiPath
await exec.exec("msiexec.exe", [
"/quiet",
`/l*v`,
path.join(process.env.RUNNER_TEMP || "", "tailscale.log"),
"/i",
msiPath,
]);
// Add to PATH
core.addPath('C:\\Program Files\\Tailscale\\');
core.addPath("C:\\Program Files\\Tailscale\\");
}
async function installTailscaleMacOS(config, toolPath) {
core.info('Building tailscale from src on macOS...');
core.info("Building tailscale from src on macOS...");
// Clone the repo
await exec.exec('git clone https://github.com/tailscale/tailscale.git tailscale');
await exec.exec("git clone https://github.com/tailscale/tailscale.git tailscale");
// Checkout the resolved version
await exec.exec(`git checkout v${config.resolvedVersion}`, [], {
cwd: 'tailscale',
cwd: "tailscale",
});
// Create tool directory and copy binaries there for caching
fs.mkdirSync(toolPath, { recursive: true });
// Build tailscale and tailscaled into tool directory
for (const binary of ['tailscale', 'tailscaled']) {
for (const binary of ["tailscale", "tailscaled"]) {
await exec.exec(`./build_dist.sh -o ${path.join(toolPath, binary)} ./cmd/${binary}`, [], {
cwd: 'tailscale',
cwd: "tailscale",
env: {
...process.env,
'TS_USE_TOOLCHAIN': '1',
}
TS_USE_TOOLCHAIN: "1",
},
});
}
// Install binaries to /usr/local/bin
await exec.exec('sudo', ['cp',
path.join(toolPath, 'tailscale'),
path.join(toolPath, 'tailscaled'),
'/usr/local/bin'
await exec.exec("sudo", [
"cp",
path.join(toolPath, "tailscale"),
path.join(toolPath, "tailscaled"),
"/usr/local/bin",
]);
// Make sure they're executable
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
core.info('✅ Tailscale installed successfully on macOS from source');
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
core.info("✅ Tailscale installed successfully on macOS from source");
}
async function startTailscaleDaemon(config) {
const runnerOS = process.env.RUNNER_OS || '';
const runnerOS = process.env.RUNNER_OS || "";
// Manual daemon start
const stateArgs = config.stateDir ?
[`--statedir=${config.stateDir}`] :
['--state=mem:'];
const stateArgs = config.stateDir
? [`--statedir=${config.stateDir}`]
: ["--state=mem:"];
if (config.stateDir) {
fs.mkdirSync(config.stateDir, { recursive: true });
}
const args = [
...stateArgs,
...config.tailscaledArgs.split(' ').filter(Boolean)
...config.tailscaledArgs.split(" ").filter(Boolean),
];
core.info('Starting tailscaled daemon...');
core.info("Starting tailscaled daemon...");
// Start daemon in background
const daemon = (0, child_process_1.spawn)('sudo', ['-E', 'tailscaled', ...args], {
const daemon = (0, child_process_1.spawn)("sudo", ["-E", "tailscaled", ...args], {
detached: true,
stdio: ['ignore', 'ignore', fs.openSync(path.join(os.homedir(), 'tailscaled.log'), 'w')]
stdio: [
"ignore",
"ignore",
fs.openSync(path.join(os.homedir(), "tailscaled.log"), "w"),
],
});
daemon.unref(); // Ensure daemon doesn't keep Node.js process alive
// Close stdin/stdout/stderr to fully detach
@ -41510,19 +41539,19 @@ async function startTailscaleDaemon(config) {
daemon.stderr.destroy();
// Poll the local API until daemon is responsive
await waitForDaemonReady();
core.info('✅ tailscaled daemon is up and running!');
core.info("✅ tailscaled daemon is up and running!");
}
async function waitForDaemonReady() {
const maxWaitMs = 15000; // 15 seconds
const pollIntervalMs = 500;
let waited = 0;
core.info('Waiting for tailscaled daemon to become ready...');
core.info("Waiting for tailscaled daemon to become ready...");
while (waited < maxWaitMs) {
try {
const status = await getTailscaleStatus();
// If we get any valid response from the API, the daemon is ready
if (status) {
core.info(`Daemon ready! Initial state: ${status.BackendState || 'Unknown'}`);
core.info(`Daemon ready! Initial state: ${status.BackendState || "Unknown"}`);
return;
}
}
@ -41533,17 +41562,17 @@ async function waitForDaemonReady() {
await sleep(pollIntervalMs);
waited += pollIntervalMs;
}
throw new Error('tailscaled daemon did not become ready within timeout');
throw new Error("tailscaled daemon did not become ready within timeout");
}
async function connectToTailscale(config, runnerOS) {
// Determine hostname
let hostname = config.hostname;
if (!hostname) {
if (runnerOS === 'Windows') {
if (runnerOS === "Windows") {
hostname = `github-${process.env.COMPUTERNAME}`;
}
else {
const { stdout } = await exec.getExecOutput('hostname');
const { stdout } = await exec.getExecOutput("hostname");
hostname = `github-${stdout.trim()}`;
}
}
@ -41560,36 +41589,36 @@ async function connectToTailscale(config, runnerOS) {
}
// Platform-specific args
const platformArgs = [];
if (runnerOS === 'Windows') {
platformArgs.push('--unattended');
if (runnerOS === "Windows") {
platformArgs.push("--unattended");
}
// Build command
const upArgs = [
'up',
"up",
...tagsArg,
`--authkey=${finalAuthKey}`,
`--hostname=${hostname}`,
'--accept-routes',
"--accept-routes",
...platformArgs,
...config.args.split(' ').filter(Boolean)
...config.args.split(" ").filter(Boolean),
];
// Retry logic
for (let attempt = 1; attempt <= config.retry; attempt++) {
try {
core.info(`Attempt ${attempt} to bring up Tailscale...`);
let execArgs;
if (runnerOS === 'Windows') {
execArgs = ['tailscale', ...upArgs];
if (runnerOS === "Windows") {
execArgs = ["tailscale", ...upArgs];
}
else {
// Linux and macOS - use system-installed binary with sudo
execArgs = ['sudo', '-E', 'tailscale', ...upArgs];
execArgs = ["sudo", "-E", "tailscale", ...upArgs];
}
const timeoutMs = parseTimeout(config.timeout);
core.info(`Running: ${execArgs.join(' ')} (timeout: ${timeoutMs}ms)`);
core.info(`Running: ${execArgs.join(" ")} (timeout: ${timeoutMs}ms)`);
await Promise.race([
exec.exec(execArgs[0], execArgs.slice(1)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs))
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)),
]);
// Success
core.info(`✅ Tailscale up command completed successfully on attempt ${attempt}`);
@ -41611,41 +41640,49 @@ function parseTimeout(timeout) {
if (!match)
return 120000; // default 2 minutes
const value = parseInt(match[1]);
const unit = match[2] || 's';
const unit = match[2] || "s";
switch (unit) {
case 's': return value * 1000;
case 'm': return value * 60 * 1000;
case 'h': return value * 60 * 60 * 1000;
default: return value * 1000;
case "s":
return value * 1000;
case "m":
return value * 60 * 1000;
case "h":
return value * 60 * 60 * 1000;
default:
return value * 1000;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
function generateCacheKey(config, runnerOS) {
// Don't cache if version is latest or if caching is disabled
if (config.resolvedVersion === 'latest' || !config.useCache) {
if (config.resolvedVersion === "latest" || !config.useCache) {
return undefined;
}
return `action-setup-tailscale/${config.resolvedVersion}/${runnerOS}-${config.arch}`;
}
function getToolPath(config, runnerOS) {
const cacheDirectory = process.env.RUNNER_TOOL_CACHE || '';
if (cacheDirectory === '') {
core.warning('Expected RUNNER_TOOL_CACHE to be defined');
const cacheDirectory = process.env.RUNNER_TOOL_CACHE || "";
if (cacheDirectory === "") {
core.warning("Expected RUNNER_TOOL_CACHE to be defined");
}
return path.join(cacheDirectory, 'tailscale', config.resolvedVersion, `${runnerOS}-${config.arch}`);
return path.join(cacheDirectory, "tailscale", config.resolvedVersion, `${runnerOS}-${config.arch}`);
}
async function installCachedBinaries(toolPath, runnerOS) {
if (runnerOS === 'Linux' || runnerOS === 'macOS') {
if (runnerOS === "Linux" || runnerOS === "macOS") {
// Copy cached binaries to /usr/local/bin
const tailscaleBin = path.join(toolPath, 'tailscale');
const tailscaledBin = path.join(toolPath, 'tailscaled');
const tailscaleBin = path.join(toolPath, "tailscale");
const tailscaledBin = path.join(toolPath, "tailscaled");
if (fs.existsSync(tailscaleBin) && fs.existsSync(tailscaledBin)) {
await exec.exec('sudo', ['cp', tailscaleBin, '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['cp', tailscaledBin, '/usr/local/bin/tailscaled']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
await exec.exec("sudo", ["cp", tailscaleBin, "/usr/local/bin/tailscale"]);
await exec.exec("sudo", [
"cp",
tailscaledBin,
"/usr/local/bin/tailscaled",
]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
}
else {
throw new Error(`Cached binaries not found in ${toolPath}`);

22
dist/logout/index.js vendored
View file

@ -25686,34 +25686,34 @@ const core = __importStar(__nccwpck_require__(7484));
const exec = __importStar(__nccwpck_require__(5236));
async function logout() {
try {
const runnerOS = process.env.RUNNER_OS || '';
core.info('🔄 Logging out of Tailscale...');
const runnerOS = process.env.RUNNER_OS || "";
core.info("🔄 Logging out of Tailscale...");
// Check if tailscale is available first
try {
await exec.exec('tailscale', ['--version'], { silent: true });
await exec.exec("tailscale", ["--version"], { silent: true });
}
catch (error) {
core.info('Tailscale not found or not accessible, skipping logout');
core.info("Tailscale not found or not accessible, skipping logout");
return;
}
// Determine the correct command based on OS
let execArgs;
if (runnerOS === 'Windows') {
execArgs = ['tailscale', 'logout'];
if (runnerOS === "Windows") {
execArgs = ["tailscale", "logout"];
}
else {
// Linux and macOS - use system-installed binary with sudo
execArgs = ['sudo', '-E', 'tailscale', 'logout'];
execArgs = ["sudo", "-E", "tailscale", "logout"];
}
core.info(`Running: ${execArgs.join(' ')}`);
core.info(`Running: ${execArgs.join(" ")}`);
try {
await exec.exec(execArgs[0], execArgs.slice(1));
core.info('✅ Successfully logged out of Tailscale');
core.info("✅ Successfully logged out of Tailscale");
}
catch (error) {
// Don't fail the action if logout fails - it's just cleanup
core.warning(`Failed to logout from Tailscale: ${error}`);
core.info('Your ephemeral node will eventually be cleaned up by Tailscale');
core.info("Your ephemeral node will eventually be cleaned up by Tailscale");
}
}
catch (error) {
@ -25722,7 +25722,7 @@ async function logout() {
}
}
// Run the logout function
logout().catch(error => {
logout().catch((error) => {
// Even if logout fails, don't fail the action
core.warning(`Logout process failed: ${error}`);
});

39
package-lock.json generated
View file

@ -18,6 +18,8 @@
"devDependencies": {
"@types/node": "^20.17.6",
"@vercel/ncc": "^0.38.3",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2",
"typescript": "^5.8.3"
}
},
@ -1070,6 +1072,43 @@
"wrappy": "1"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"prettier": ">=2.0",
"typescript": ">=2.9"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"optional": true
}
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",

View file

@ -22,7 +22,9 @@
"devDependencies": {
"@types/node": "^20.17.6",
"@vercel/ncc": "^0.38.3",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2"
},
"overrides": {
"glob": "^10.3.10",

View file

@ -1,39 +1,40 @@
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as os from 'os';
import * as core from "@actions/core";
import * as exec from "@actions/exec";
async function logout(): Promise<void> {
try {
const runnerOS = process.env.RUNNER_OS || '';
core.info('🔄 Logging out of Tailscale...');
const runnerOS = process.env.RUNNER_OS || "";
core.info("🔄 Logging out of Tailscale...");
// Check if tailscale is available first
try {
await exec.exec('tailscale', ['--version'], { silent: true });
await exec.exec("tailscale", ["--version"], { silent: true });
} catch (error) {
core.info('Tailscale not found or not accessible, skipping logout');
core.info("Tailscale not found or not accessible, skipping logout");
return;
}
// Determine the correct command based on OS
let execArgs: string[];
if (runnerOS === 'Windows') {
execArgs = ['tailscale', 'logout'];
if (runnerOS === "Windows") {
execArgs = ["tailscale", "logout"];
} else {
// Linux and macOS - use system-installed binary with sudo
execArgs = ['sudo', '-E', 'tailscale', 'logout'];
execArgs = ["sudo", "-E", "tailscale", "logout"];
}
core.info(`Running: ${execArgs.join(' ')}`);
core.info(`Running: ${execArgs.join(" ")}`);
try {
await exec.exec(execArgs[0], execArgs.slice(1));
core.info('✅ Successfully logged out of Tailscale');
core.info("✅ Successfully logged out of Tailscale");
} catch (error) {
// Don't fail the action if logout fails - it's just cleanup
core.warning(`Failed to logout from Tailscale: ${error}`);
core.info('Your ephemeral node will eventually be cleaned up by Tailscale');
core.info(
"Your ephemeral node will eventually be cleaned up by Tailscale"
);
}
} catch (error) {
// Don't fail the action for post-cleanup issues
@ -42,7 +43,7 @@ async function logout(): Promise<void> {
}
// Run the logout function
logout().catch(error => {
logout().catch((error) => {
// Even if logout fails, don't fail the action
core.warning(`Logout process failed: ${error}`);
});

View file

@ -1,13 +1,13 @@
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache';
import * as cache from '@actions/cache';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawn } from 'child_process';
import * as crypto from 'crypto';
import * as http from 'http';
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import * as tc from "@actions/tool-cache";
import { spawn } from "child_process";
import * as crypto from "crypto";
import * as fs from "fs";
import * as http from "http";
import * as os from "os";
import * as path from "path";
interface TailscaleConfig {
version: string;
@ -30,25 +30,28 @@ interface TailscaleConfig {
// Cross-platform Tailscale local API status check
async function getTailscaleStatus(): Promise<any> {
const platform = os.platform();
if (platform === 'win32') {
if (platform === "win32") {
// Windows: use tailscale status command
const { stdout } = await exec.getExecOutput('tailscale', ['status', '--json']);
const { stdout } = await exec.getExecOutput("tailscale", [
"status",
"--json",
]);
return JSON.parse(stdout);
} else if (platform === 'darwin') {
} else if (platform === "darwin") {
// macOS: use /var/run/tailscaled.socket
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {
socketPath: '/var/run/tailscaled.socket',
path: '/localapi/v0/status',
method: 'GET',
headers: { Host: 'local-tailscaled.sock' },
socketPath: "/var/run/tailscaled.socket",
path: "/localapi/v0/status",
method: "GET",
headers: { Host: "local-tailscaled.sock" },
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (e) {
@ -56,30 +59,30 @@ async function getTailscaleStatus(): Promise<any> {
}
});
});
// Set timeout to prevent hanging
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timeout'));
reject(new Error("Request timeout"));
});
req.on('error', reject);
req.on("error", reject);
req.end();
});
} else {
// Linux: use Unix socket
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {
socketPath: '/run/tailscale/tailscaled.sock',
path: '/localapi/v0/status',
method: 'GET',
headers: { Host: 'local-tailscaled.sock' },
socketPath: "/run/tailscale/tailscaled.sock",
path: "/localapi/v0/status",
method: "GET",
headers: { Host: "local-tailscaled.sock" },
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (e) {
@ -87,14 +90,14 @@ async function getTailscaleStatus(): Promise<any> {
}
});
});
// Set timeout to prevent hanging
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timeout'));
reject(new Error("Request timeout"));
});
req.on('error', reject);
req.on("error", reject);
req.end();
});
}
@ -103,9 +106,9 @@ async function getTailscaleStatus(): Promise<any> {
async function run(): Promise<void> {
try {
// Validate runner OS
const runnerOS = process.env.RUNNER_OS || '';
if (!['Linux', 'Windows', 'macOS'].includes(runnerOS)) {
throw new Error('Support Linux, Windows, and macOS Only');
const runnerOS = process.env.RUNNER_OS || "";
if (!["Linux", "Windows", "macOS"].includes(runnerOS)) {
throw new Error("Support Linux, Windows, and macOS Only");
}
// Get and validate inputs
@ -125,7 +128,7 @@ async function run(): Promise<void> {
await installTailscale(config, runnerOS);
// Start daemon (non-Windows only)
if (runnerOS !== 'Windows') {
if (runnerOS !== "Windows") {
await startTailscaleDaemon(config);
}
@ -136,8 +139,8 @@ async function run(): Promise<void> {
try {
const status = await getTailscaleStatus();
core.debug(`Tailscale status: ${JSON.stringify(status)}`);
if (status.BackendState === 'Running') {
core.info('✅ Tailscale is running and connected!');
if (status.BackendState === "Running") {
core.info("✅ Tailscale is running and connected!");
// Explicitly exit to prevent hanging
process.exit(0);
} else {
@ -147,7 +150,7 @@ async function run(): Promise<void> {
} catch (err) {
core.warning(`Failed to get Tailscale status: ${err}`);
// Still exit successfully since the main connection worked
core.info('✅ Tailscale connection completed successfully!');
core.info("✅ Tailscale connection completed successfully!");
process.exit(0);
}
} catch (error) {
@ -157,36 +160,39 @@ async function run(): Promise<void> {
async function getInputs(): Promise<TailscaleConfig> {
return {
version: core.getInput('version') || '1.82.0',
resolvedVersion: '',
arch: '',
authKey: core.getInput('authkey') || '',
oauthClientId: core.getInput('oauth-client-id') || '',
oauthSecret: core.getInput('oauth-client-secret') || '',
tags: core.getInput('tags') || '',
hostname: core.getInput('hostname') || '',
args: core.getInput('args') || '',
tailscaledArgs: core.getInput('tailscaled-args') || '',
stateDir: core.getInput('statedir') || '',
timeout: core.getInput('timeout') || '60s', // Reduced from 2m to 60s
retry: parseInt(core.getInput('retry') || '5'),
useCache: core.getBooleanInput('use-cache'),
sha256Sum: core.getInput('sha256sum') || ''
version: core.getInput("version") || "1.82.0",
resolvedVersion: "",
arch: "",
authKey: core.getInput("authkey") || "",
oauthClientId: core.getInput("oauth-client-id") || "",
oauthSecret: core.getInput("oauth-client-secret") || "",
tags: core.getInput("tags") || "",
hostname: core.getInput("hostname") || "",
args: core.getInput("args") || "",
tailscaledArgs: core.getInput("tailscaled-args") || "",
stateDir: core.getInput("statedir") || "",
timeout: core.getInput("timeout") || "60s", // Reduced from 2m to 60s
retry: parseInt(core.getInput("retry") || "5"),
useCache: core.getBooleanInput("use-cache"),
sha256Sum: core.getInput("sha256sum") || "",
};
}
function validateAuth(config: TailscaleConfig): void {
if (!config.authKey && (!config.oauthSecret || !config.tags)) {
throw new Error('OAuth identity empty, please provide either an auth key or OAuth secret and tags.');
throw new Error(
"OAuth identity empty, please provide either an auth key or OAuth secret and tags."
);
}
}
async function resolveVersion(version: string): Promise<string> {
if (version === 'latest') {
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-s',
'https://pkgs.tailscale.com/stable/?mode=json'
if (version === "latest") {
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-s",
"https://pkgs.tailscale.com/stable/?mode=json",
]);
const response = JSON.parse(stdout);
return response.Version;
@ -195,26 +201,36 @@ async function resolveVersion(version: string): Promise<string> {
}
function getTailscaleArch(runnerOS: string): string {
const runnerArch = process.env.RUNNER_ARCH || '';
const runnerArch = process.env.RUNNER_ARCH || "";
if (runnerOS === 'Linux') {
if (runnerOS === "Linux") {
switch (runnerArch) {
case 'ARM64': return 'arm64';
case 'ARM': return 'arm';
case 'X86': return '386';
default: return 'amd64';
case "ARM64":
return "arm64";
case "ARM":
return "arm";
case "X86":
return "386";
default:
return "amd64";
}
} else if (runnerOS === 'Windows') {
} else if (runnerOS === "Windows") {
switch (runnerArch) {
case 'ARM64': return 'arm64';
case 'X86': return 'x86';
default: return 'amd64';
case "ARM64":
return "arm64";
case "X86":
return "x86";
default:
return "amd64";
}
}
return 'amd64';
return "amd64";
}
async function installTailscale(config: TailscaleConfig, runnerOS: string): Promise<void> {
async function installTailscale(
config: TailscaleConfig,
runnerOS: string
): Promise<void> {
const cacheKey = generateCacheKey(config, runnerOS);
const toolPath = getToolPath(config, runnerOS);
@ -222,10 +238,12 @@ async function installTailscale(config: TailscaleConfig, runnerOS: string): Prom
if (config.useCache && cacheKey) {
const cacheHit = await cache.restoreCache([toolPath], cacheKey);
if (cacheHit) {
core.info(`Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`);
core.info(
`Found Tailscale ${config.resolvedVersion} in cache: ${toolPath}`
);
// For Windows, install the cached MSI
if (runnerOS === 'Windows') {
if (runnerOS === "Windows") {
await installTailscaleWindows(config, toolPath, true);
} else {
// For Linux/macOS, copy binaries to /usr/local/bin
@ -236,11 +254,11 @@ async function installTailscale(config: TailscaleConfig, runnerOS: string): Prom
}
// Install fresh if not cached
if (runnerOS === 'Linux') {
if (runnerOS === "Linux") {
await installTailscaleLinux(config, toolPath);
} else if (runnerOS === 'Windows') {
} else if (runnerOS === "Windows") {
await installTailscaleWindows(config, toolPath);
} else if (runnerOS === 'macOS') {
} else if (runnerOS === "macOS") {
await installTailscaleMacOS(config, toolPath);
}
@ -264,26 +282,34 @@ async function installTailscale(config: TailscaleConfig, runnerOS: string): Prom
async function calculateFileSha256(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const hash = crypto.createHash("sha256");
const stream = fs.createReadStream(filePath);
stream.on('error', err => reject(err));
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
stream.on("error", (err) => reject(err));
stream.on("data", (chunk) => hash.update(chunk));
stream.on("end", () => resolve(hash.digest("hex").toLowerCase()));
});
}
async function installTailscaleLinux(config: TailscaleConfig, toolPath: string): Promise<void> {
async function installTailscaleLinux(
config: TailscaleConfig,
toolPath: string
): Promise<void> {
// Determine if stable or unstable
const minor = parseInt(config.resolvedVersion.split('.')[1]);
const minor = parseInt(config.resolvedVersion.split(".")[1]);
const isStable = minor % 2 === 0;
const baseUrl = isStable ? 'https://pkgs.tailscale.com/stable' : 'https://pkgs.tailscale.com/unstable';
const baseUrl = isStable
? "https://pkgs.tailscale.com/stable"
: "https://pkgs.tailscale.com/unstable";
// Get SHA256 if not provided
if (!config.sha256Sum) {
const shaUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz.sha256`;
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-L', shaUrl, '--fail'
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-L",
shaUrl,
"--fail",
]);
config.sha256Sum = stdout.trim();
}
@ -292,7 +318,7 @@ async function installTailscaleLinux(config: TailscaleConfig, toolPath: string):
const downloadUrl = `${baseUrl}/tailscale_${config.resolvedVersion}_${config.arch}.tgz`;
core.info(`Downloading ${downloadUrl}`);
const tarPath = await tc.downloadTool(downloadUrl, 'tailscale.tgz');
const tarPath = await tc.downloadTool(downloadUrl, "tailscale.tgz");
// Verify checksum
const actualSha = await calculateFileSha256(tarPath);
@ -300,34 +326,48 @@ async function installTailscaleLinux(config: TailscaleConfig, toolPath: string):
core.info(`Expected sha256: ${expectedSha}`);
core.info(`Actual sha256: ${actualSha}`);
if (actualSha !== expectedSha) {
throw new Error('SHA256 checksum mismatch');
throw new Error("SHA256 checksum mismatch");
}
// Extract to tool path
const extractedPath = await tc.extractTar(tarPath, undefined, 'xz');
const extractedDir = path.join(extractedPath, `tailscale_${config.resolvedVersion}_${config.arch}`);
const extractedPath = await tc.extractTar(tarPath, undefined, "xz");
const extractedDir = path.join(
extractedPath,
`tailscale_${config.resolvedVersion}_${config.arch}`
);
// Create tool directory and copy binaries there for caching
fs.mkdirSync(toolPath, { recursive: true });
fs.copyFileSync(path.join(extractedDir, 'tailscale'), path.join(toolPath, 'tailscale'));
fs.copyFileSync(path.join(extractedDir, 'tailscaled'), path.join(toolPath, 'tailscaled'));
fs.copyFileSync(
path.join(extractedDir, "tailscale"),
path.join(toolPath, "tailscale")
);
fs.copyFileSync(
path.join(extractedDir, "tailscaled"),
path.join(toolPath, "tailscaled")
);
// Install binaries to /usr/local/bin
await exec.exec('sudo', ['cp',
path.join(toolPath, 'tailscale'),
path.join(toolPath, 'tailscaled'),
'/usr/local/bin'
await exec.exec("sudo", [
"cp",
path.join(toolPath, "tailscale"),
path.join(toolPath, "tailscaled"),
"/usr/local/bin",
]);
// Make sure they're executable
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
}
async function installTailscaleWindows(config: TailscaleConfig, toolPath: string, fromCache: boolean = false): Promise<void> {
async function installTailscaleWindows(
config: TailscaleConfig,
toolPath: string,
fromCache: boolean = false
): Promise<void> {
// Create tool directory
fs.mkdirSync(toolPath, { recursive: true });
const msiPath = path.join(toolPath, 'tailscale.msi');
const msiPath = path.join(toolPath, "tailscale.msi");
if (fromCache) {
// Installing from cached MSI
@ -338,16 +378,21 @@ async function installTailscaleWindows(config: TailscaleConfig, toolPath: string
} else {
// Fresh download
// Determine if stable or unstable
const minor = parseInt(config.resolvedVersion.split('.')[1]);
const minor = parseInt(config.resolvedVersion.split(".")[1]);
const isStable = minor % 2 === 0;
const baseUrl = isStable ? 'https://pkgs.tailscale.com/stable' : 'https://pkgs.tailscale.com/unstable';
const baseUrl = isStable
? "https://pkgs.tailscale.com/stable"
: "https://pkgs.tailscale.com/unstable";
// Get SHA256 if not provided
if (!config.sha256Sum) {
const shaUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi.sha256`;
const { stdout } = await exec.getExecOutput('curl', [
'-H', 'user-agent:action-setup-tailscale',
'-L', shaUrl, '--fail'
const { stdout } = await exec.getExecOutput("curl", [
"-H",
"user-agent:action-setup-tailscale",
"-L",
shaUrl,
"--fail",
]);
config.sha256Sum = stdout.trim();
}
@ -364,7 +409,7 @@ async function installTailscaleWindows(config: TailscaleConfig, toolPath: string
core.info(`Expected sha256: ${expectedSha}`);
core.info(`Actual sha256: ${actualSha}`);
if (actualSha !== expectedSha) {
throw new Error('SHA256 checksum mismatch');
throw new Error("SHA256 checksum mismatch");
}
// Keep the MSI file in toolPath for caching (don't delete it)
@ -375,62 +420,74 @@ async function installTailscaleWindows(config: TailscaleConfig, toolPath: string
}
// Install MSI (same for both fresh and cached)
await exec.exec('msiexec.exe', [
'/quiet',
`/l*v`, path.join(process.env.RUNNER_TEMP || '', 'tailscale.log'),
'/i', msiPath
await exec.exec("msiexec.exe", [
"/quiet",
`/l*v`,
path.join(process.env.RUNNER_TEMP || "", "tailscale.log"),
"/i",
msiPath,
]);
// Add to PATH
core.addPath('C:\\Program Files\\Tailscale\\');
core.addPath("C:\\Program Files\\Tailscale\\");
}
async function installTailscaleMacOS(config: TailscaleConfig, toolPath: string): Promise<void> {
core.info('Building tailscale from src on macOS...');
async function installTailscaleMacOS(
config: TailscaleConfig,
toolPath: string
): Promise<void> {
core.info("Building tailscale from src on macOS...");
// Clone the repo
await exec.exec('git clone https://github.com/tailscale/tailscale.git tailscale');
await exec.exec(
"git clone https://github.com/tailscale/tailscale.git tailscale"
);
// Checkout the resolved version
await exec.exec(`git checkout v${config.resolvedVersion}`, [], {
cwd: 'tailscale',
})
cwd: "tailscale",
});
// Create tool directory and copy binaries there for caching
fs.mkdirSync(toolPath, { recursive: true });
// Build tailscale and tailscaled into tool directory
for (const binary of ['tailscale', 'tailscaled']) {
await exec.exec(`./build_dist.sh -o ${path.join(toolPath, binary)} ./cmd/${binary}`, [], {
cwd: 'tailscale',
env: {
...process.env,
'TS_USE_TOOLCHAIN': '1',
for (const binary of ["tailscale", "tailscaled"]) {
await exec.exec(
`./build_dist.sh -o ${path.join(toolPath, binary)} ./cmd/${binary}`,
[],
{
cwd: "tailscale",
env: {
...process.env,
TS_USE_TOOLCHAIN: "1",
},
}
})
);
}
// Install binaries to /usr/local/bin
await exec.exec('sudo', ['cp',
path.join(toolPath, 'tailscale'),
path.join(toolPath, 'tailscaled'),
'/usr/local/bin'
await exec.exec("sudo", [
"cp",
path.join(toolPath, "tailscale"),
path.join(toolPath, "tailscaled"),
"/usr/local/bin",
]);
// Make sure they're executable
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
core.info('✅ Tailscale installed successfully on macOS from source');
core.info("✅ Tailscale installed successfully on macOS from source");
}
async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
const runnerOS = process.env.RUNNER_OS || '';
const runnerOS = process.env.RUNNER_OS || "";
// Manual daemon start
const stateArgs = config.stateDir ?
[`--statedir=${config.stateDir}`] :
['--state=mem:'];
const stateArgs = config.stateDir
? [`--statedir=${config.stateDir}`]
: ["--state=mem:"];
if (config.stateDir) {
fs.mkdirSync(config.stateDir, { recursive: true });
@ -438,43 +495,49 @@ async function startTailscaleDaemon(config: TailscaleConfig): Promise<void> {
const args = [
...stateArgs,
...config.tailscaledArgs.split(' ').filter(Boolean)
...config.tailscaledArgs.split(" ").filter(Boolean),
];
core.info('Starting tailscaled daemon...');
core.info("Starting tailscaled daemon...");
// Start daemon in background
const daemon = spawn('sudo', ['-E', 'tailscaled', ...args], {
const daemon = spawn("sudo", ["-E", "tailscaled", ...args], {
detached: true,
stdio: ['ignore', 'ignore', fs.openSync(path.join(os.homedir(), 'tailscaled.log'), 'w')]
stdio: [
"ignore",
"ignore",
fs.openSync(path.join(os.homedir(), "tailscaled.log"), "w"),
],
});
daemon.unref(); // Ensure daemon doesn't keep Node.js process alive
// Close stdin/stdout/stderr to fully detach
if (daemon.stdin) daemon.stdin.end();
if (daemon.stdout) daemon.stdout.destroy();
if (daemon.stderr) daemon.stderr.destroy();
// Poll the local API until daemon is responsive
await waitForDaemonReady();
core.info('✅ tailscaled daemon is up and running!');
core.info("✅ tailscaled daemon is up and running!");
}
async function waitForDaemonReady(): Promise<void> {
const maxWaitMs = 15000; // 15 seconds
const pollIntervalMs = 500;
let waited = 0;
core.info('Waiting for tailscaled daemon to become ready...');
core.info("Waiting for tailscaled daemon to become ready...");
while (waited < maxWaitMs) {
try {
const status = await getTailscaleStatus();
// If we get any valid response from the API, the daemon is ready
if (status) {
core.info(`Daemon ready! Initial state: ${status.BackendState || 'Unknown'}`);
core.info(
`Daemon ready! Initial state: ${status.BackendState || "Unknown"}`
);
return;
}
} catch (err) {
@ -484,18 +547,21 @@ async function waitForDaemonReady(): Promise<void> {
await sleep(pollIntervalMs);
waited += pollIntervalMs;
}
throw new Error('tailscaled daemon did not become ready within timeout');
throw new Error("tailscaled daemon did not become ready within timeout");
}
async function connectToTailscale(config: TailscaleConfig, runnerOS: string): Promise<void> {
async function connectToTailscale(
config: TailscaleConfig,
runnerOS: string
): Promise<void> {
// Determine hostname
let hostname = config.hostname;
if (!hostname) {
if (runnerOS === 'Windows') {
if (runnerOS === "Windows") {
hostname = `github-${process.env.COMPUTERNAME}`;
} else {
const { stdout } = await exec.getExecOutput('hostname');
const { stdout } = await exec.getExecOutput("hostname");
hostname = `github-${stdout.trim()}`;
}
}
@ -516,19 +582,19 @@ async function connectToTailscale(config: TailscaleConfig, runnerOS: string): Pr
// Platform-specific args
const platformArgs: string[] = [];
if (runnerOS === 'Windows') {
platformArgs.push('--unattended');
if (runnerOS === "Windows") {
platformArgs.push("--unattended");
}
// Build command
const upArgs = [
'up',
"up",
...tagsArg,
`--authkey=${finalAuthKey}`,
`--hostname=${hostname}`,
'--accept-routes',
"--accept-routes",
...platformArgs,
...config.args.split(' ').filter(Boolean)
...config.args.split(" ").filter(Boolean),
];
// Retry logic
@ -537,25 +603,27 @@ async function connectToTailscale(config: TailscaleConfig, runnerOS: string): Pr
core.info(`Attempt ${attempt} to bring up Tailscale...`);
let execArgs: string[];
if (runnerOS === 'Windows') {
execArgs = ['tailscale', ...upArgs];
if (runnerOS === "Windows") {
execArgs = ["tailscale", ...upArgs];
} else {
// Linux and macOS - use system-installed binary with sudo
execArgs = ['sudo', '-E', 'tailscale', ...upArgs];
execArgs = ["sudo", "-E", "tailscale", ...upArgs];
}
const timeoutMs = parseTimeout(config.timeout);
core.info(`Running: ${execArgs.join(' ')} (timeout: ${timeoutMs}ms)`);
core.info(`Running: ${execArgs.join(" ")} (timeout: ${timeoutMs}ms)`);
await Promise.race([
exec.exec(execArgs[0], execArgs.slice(1)),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
)
setTimeout(() => reject(new Error("Timeout")), timeoutMs)
),
]);
// Success
core.info(`✅ Tailscale up command completed successfully on attempt ${attempt}`);
core.info(
`✅ Tailscale up command completed successfully on attempt ${attempt}`
);
return;
} catch (error) {
core.warning(`Tailscale up attempt ${attempt} failed: ${error}`);
@ -575,23 +643,30 @@ function parseTimeout(timeout: string): number {
if (!match) return 120000; // default 2 minutes
const value = parseInt(match[1]);
const unit = match[2] || 's';
const unit = match[2] || "s";
switch (unit) {
case 's': return value * 1000;
case 'm': return value * 60 * 1000;
case 'h': return value * 60 * 60 * 1000;
default: return value * 1000;
case "s":
return value * 1000;
case "m":
return value * 60 * 1000;
case "h":
return value * 60 * 60 * 1000;
default:
return value * 1000;
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
function generateCacheKey(config: TailscaleConfig, runnerOS: string): string | undefined {
function generateCacheKey(
config: TailscaleConfig,
runnerOS: string
): string | undefined {
// Don't cache if version is latest or if caching is disabled
if (config.resolvedVersion === 'latest' || !config.useCache) {
if (config.resolvedVersion === "latest" || !config.useCache) {
return undefined;
}
@ -599,34 +674,41 @@ function generateCacheKey(config: TailscaleConfig, runnerOS: string): string | u
}
function getToolPath(config: TailscaleConfig, runnerOS: string): string {
const cacheDirectory = process.env.RUNNER_TOOL_CACHE || '';
if (cacheDirectory === '') {
core.warning('Expected RUNNER_TOOL_CACHE to be defined');
const cacheDirectory = process.env.RUNNER_TOOL_CACHE || "";
if (cacheDirectory === "") {
core.warning("Expected RUNNER_TOOL_CACHE to be defined");
}
return path.join(
cacheDirectory,
'tailscale',
"tailscale",
config.resolvedVersion,
`${runnerOS}-${config.arch}`
);
}
async function installCachedBinaries(toolPath: string, runnerOS: string): Promise<void> {
if (runnerOS === 'Linux' || runnerOS === 'macOS') {
async function installCachedBinaries(
toolPath: string,
runnerOS: string
): Promise<void> {
if (runnerOS === "Linux" || runnerOS === "macOS") {
// Copy cached binaries to /usr/local/bin
const tailscaleBin = path.join(toolPath, 'tailscale');
const tailscaledBin = path.join(toolPath, 'tailscaled');
const tailscaleBin = path.join(toolPath, "tailscale");
const tailscaledBin = path.join(toolPath, "tailscaled");
if (fs.existsSync(tailscaleBin) && fs.existsSync(tailscaledBin)) {
await exec.exec('sudo', ['cp', tailscaleBin, '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['cp', tailscaledBin, '/usr/local/bin/tailscaled']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscale']);
await exec.exec('sudo', ['chmod', '+x', '/usr/local/bin/tailscaled']);
await exec.exec("sudo", ["cp", tailscaleBin, "/usr/local/bin/tailscale"]);
await exec.exec("sudo", [
"cp",
tailscaledBin,
"/usr/local/bin/tailscaled",
]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscale"]);
await exec.exec("sudo", ["chmod", "+x", "/usr/local/bin/tailscaled"]);
} else {
throw new Error(`Cached binaries not found in ${toolPath}`);
}
}
}
run();
run();