diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfd8508..c6040c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,7 @@ jobs: runner-os: Linux arch: amd64 version: latest + ping: 100.99.0.2,lax-pve.pineapplefish.ts.net,lax-pve # Try unstable too - os: ubuntu-latest @@ -45,6 +46,7 @@ jobs: runner-os: Windows arch: amd64 version: latest + ping: 100.99.0.2,lax-pve.pineapplefish.ts.net,lax-pve - os: windows-latest runner-os: Windows @@ -62,6 +64,7 @@ jobs: runner-os: macOS arch: amd64 version: latest + ping: 100.99.0.2 # hostnames aren't resolving on MacOS, just ping IP lax-pve.pineapplefish.ts.net,lax-pve # macOS ARM - os: macos-14 @@ -99,6 +102,7 @@ jobs: use-cache: false timeout: "5m" retry: 3 + ping: "${{ matrix.ping }}" # Test Tailscale status command - name: Check Tailscale Status diff --git a/README.md b/README.md index 83165c6..22031fd 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ This action is written in Typescript using official GitHub SDKs. It provides som | `tailscaled-args` | Additional `tailscaled` arguments | false | | | `statedir` | State directory (if empty, uses memory) | false | | | `sha256sum` | Expected SHA256 checksum | false | | +| `ping` | 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 | false | | ## Authentication @@ -209,4 +210,6 @@ For security issues, please see our [security policy](SECURITY.md). ## CI Notes -CI tests run against the tailscalegithubactionbot.github tailnet. Check our usual credential store for credentials. \ No newline at end of file +CI tests run against the pineapplefish-tailnet.org.github tailnet. Check our usual credential store for credentials. + +`tag:ci` must have access to the `lax-pve` server. \ No newline at end of file diff --git a/action.yml b/action.yml index 7a5df69..7288802 100644 --- a/action.yml +++ b/action.yml @@ -53,7 +53,11 @@ inputs: description: 'Expected SHA256 checksum of the Tailscale package. If not provided, it will be fetched automatically.' required: false default: '' - + ping: + 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: '' + runs: using: 'node20' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index e211f71..ddca8b5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41241,6 +41241,7 @@ async function run() { core.debug(`Tailscale status: ${JSON.stringify(status)}`); if (status.BackendState === "Running") { core.info("✅ Tailscale is running and connected!"); + pingHostsIfNecessary(config); // Explicitly exit to prevent hanging process.exit(0); } @@ -41252,7 +41253,9 @@ 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 daemon is connected!"); + pingHostsIfNecessary(config); + // Explicitly exit to prevent hanging process.exit(0); } } @@ -41260,7 +41263,36 @@ async function run() { core.setFailed(error instanceof Error ? error.message : String(error)); } } +async function pingHostsIfNecessary(config) { + const directConnectionWarning = "direct connection not established"; + if (config.pingHosts.length == 0) { + return; + } + core.info(`Will ping hosts ${config.pingHosts.join(",")} up to 3 minutes in order to check connectivity`); + for (const host of config.pingHosts) { + core.info(`Pinging host ${host}`); + let result = await exec.getExecOutput(cmdTailscale, [ + "ping", + "-c", + "36", + host, + ]); + if (result.exitCode === 0) { + core.info(`✅ Ping host ${host} responded!`); + } + else if (result.stderr.includes(directConnectionWarning) || + result.stdout.includes(directConnectionWarning)) { + core.warning(`⚠️ Ping host ${host} reachable only via DERP, not direct connection.`); + } + else { + core.setFailed(`❌ Ping host ${host} did not respond`); + process.exit(1); + } + } +} async function getInputs() { + let ping = core.getInput("ping"); + let pingHosts = ping?.length > 0 ? ping.split(",") : []; return { version: core.getInput("version") || "1.82.0", resolvedVersion: "", @@ -41277,6 +41309,7 @@ async function getInputs() { retry: parseInt(core.getInput("retry") || "5"), useCache: core.getBooleanInput("use-cache"), sha256Sum: core.getInput("sha256sum") || "", + pingHosts: pingHosts, }; } function validateAuth(config) { diff --git a/src/main.ts b/src/main.ts index 2410981..01c5db8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,6 +40,7 @@ interface TailscaleConfig { retry: number; useCache: boolean; sha256Sum: string; + pingHosts: string[]; } // Cross-platform Tailscale local API status check @@ -166,6 +167,7 @@ async function run(): Promise { core.debug(`Tailscale status: ${JSON.stringify(status)}`); if (status.BackendState === "Running") { core.info("✅ Tailscale is running and connected!"); + pingHostsIfNecessary(config); // Explicitly exit to prevent hanging process.exit(0); } else { @@ -175,7 +177,9 @@ async function run(): Promise { } 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 daemon is connected!"); + pingHostsIfNecessary(config); + // Explicitly exit to prevent hanging process.exit(0); } } catch (error) { @@ -183,7 +187,46 @@ async function run(): Promise { } } +async function pingHostsIfNecessary(config: TailscaleConfig): Promise { + const directConnectionWarning = "direct connection not established"; + + if (config.pingHosts.length == 0) { + return; + } + + core.info( + `Will ping hosts ${config.pingHosts.join( + "," + )} up to 3 minutes in order to check connectivity` + ); + for (const host of config.pingHosts) { + core.info(`Pinging host ${host}`); + let result = await exec.getExecOutput(cmdTailscale, [ + "ping", + "-c", + "36", + host, + ]); + if (result.exitCode === 0) { + core.info(`✅ Ping host ${host} responded!`); + } else if ( + result.stderr.includes(directConnectionWarning) || + result.stdout.includes(directConnectionWarning) + ) { + core.warning( + `⚠️ Ping host ${host} reachable only via DERP, not direct connection.` + ); + } else { + core.setFailed(`❌ Ping host ${host} did not respond`); + process.exit(1); + } + } +} + async function getInputs(): Promise { + let ping = core.getInput("ping"); + let pingHosts = ping?.length > 0 ? ping.split(",") : []; + return { version: core.getInput("version") || "1.82.0", resolvedVersion: "", @@ -200,6 +243,7 @@ async function getInputs(): Promise { retry: parseInt(core.getInput("retry") || "5"), useCache: core.getBooleanInput("use-cache"), sha256Sum: core.getInput("sha256sum") || "", + pingHosts: pingHosts, }; }