tailscale-localapi 0.6.0

Client for the Tailscale local API
Documentation
name: Test

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

env:
  TAILSCALE_VERSION: 1.98.9

jobs:
  check:
    name: Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo check
      - run: cargo fmt --check
      - run: cargo clippy --all-targets -- -D warnings

  headscale:
    name: Headscale (Linux)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - name: Run Headscale integration test
        run: docker compose -f tests/headscale/compose.yaml up --build --abort-on-container-exit --exit-code-from tests
      - name: Print container logs
        if: failure()
        run: docker compose -f tests/headscale/compose.yaml logs --no-color
      - name: Remove integration environment
        if: always()
        run: docker compose -f tests/headscale/compose.yaml down -v --remove-orphans

  headscale-windows:
    name: Headscale (Windows)
    runs-on: windows-latest
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Verify WSL2 availability
        shell: pwsh
        run: |
          wsl.exe --status
          wsl.exe --version
      - name: Install Ubuntu 24.04 in WSL2
        uses: Vampire/setup-wsl@v7
        with:
          distribution: Ubuntu-24.04
          wsl-version: 2
          update: "true"
          additional-packages: docker.io docker-compose-v2 ca-certificates curl
          wsl-conf: |
            [boot]
            systemd=true
      - name: Restart Ubuntu with systemd
        shell: pwsh
        run: wsl.exe --terminate Ubuntu-24.04
      - name: Verify Docker Engine in WSL2
        shell: wsl-bash {0}
        run: |
          systemctl start docker
          docker version
          docker compose version
      - name: Start Headscale and node-b
        shell: wsl-bash {0}
        run: |
          docker compose -f tests/headscale/compose.yaml up -d --wait --wait-timeout 60 headscale bootstrap node-b
          docker compose -f tests/headscale/compose.yaml ps
      - name: Read Headscale preauth key
        id: headscale-auth
        shell: pwsh
        run: |
          if (($env:GITHUB_WORKSPACE.Length -lt 3) -or ($env:GITHUB_WORKSPACE[1] -ne ':')) {
            throw "GITHUB_WORKSPACE is not a drive-qualified Windows path"
          }
          $drive = $env:GITHUB_WORKSPACE[0].ToString().ToLowerInvariant()
          $path = $env:GITHUB_WORKSPACE.Substring(2).Replace('\', '/')
          $workspace = "/mnt/$drive$path"
          $authKey = (& wsl.exe -d Ubuntu-24.04 -- bash -lc "cd '$workspace' && docker compose -f tests/headscale/compose.yaml exec -T bootstrap cat /auth/authkey").Trim()
          if (($LASTEXITCODE -ne 0) -or [string]::IsNullOrWhiteSpace($authKey)) {
            throw "Unable to read the Headscale preauth key"
          }
          Write-Output "::add-mask::$authKey"
          "auth_key=$authKey" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
      - name: Verify Headscale from Windows
        shell: pwsh
        run: |
          Add-Content -Path "$env:SystemRoot\System32\drivers\etc\hosts" -Value "`n127.0.0.1 headscale"
          $response = Invoke-WebRequest -UseBasicParsing -Uri "http://headscale:8080/health"
          if ($response.StatusCode -ne 200) {
            throw "Headscale health endpoint returned $($response.StatusCode)"
          }
      - name: Install Tailscale for Windows
        shell: pwsh
        run: |
          $msi = Join-Path $env:RUNNER_TEMP "tailscale.msi"
          $log = Join-Path $env:RUNNER_TEMP "tailscale-msi.log"
          $url = "https://pkgs.tailscale.com/stable/tailscale-setup-$env:TAILSCALE_VERSION-amd64.msi"
          Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $msi
          $process = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @(
            "/i", $msi,
            "/qn",
            "/norestart",
            "TS_NOLAUNCH=1",
            "/L*v", $log
          )
          if ($process.ExitCode -ne 0) {
            Get-Content $log
            throw "Tailscale MSI installation failed with exit code $($process.ExitCode)"
          }
          Start-Service Tailscale
          Get-Service Tailscale
      - name: Register Windows node-a with Headscale
        shell: pwsh
        env:
          HEADSCALE_AUTH_KEY: ${{ steps.headscale-auth.outputs.auth_key }}
        run: |
          $tailscale = "$env:ProgramFiles\Tailscale\tailscale.exe"
          & $tailscale up `
            --unattended=true `
            --login-server=http://headscale:8080 `
            --auth-key=$env:HEADSCALE_AUTH_KEY `
            --hostname=node-a `
            --accept-dns=false
          if ($LASTEXITCODE -ne 0) {
            throw "tailscale up failed with exit code $LASTEXITCODE"
          }

          $deadline = (Get-Date).AddSeconds(30)
          do {
            $status = & $tailscale status --json | ConvertFrom-Json
            $hasNodeB = @($status.Peer.PSObject.Properties.Value) |
              Where-Object { $_.HostName -eq "node-b" }
            if (($status.BackendState -eq "Running") -and $hasNodeB) {
              exit 0
            }
            Start-Sleep -Seconds 1
          } while ((Get-Date) -lt $deadline)

          & $tailscale status --json
          throw "Windows Tailscale did not reach Running with node-b in its peer map"
      - name: Run Headscale integration test through Windows LocalAPI
        shell: pwsh
        env:
          TAILSCALE_PIPE: \\.\pipe\ProtectedPrefix\Administrators\Tailscale\tailscaled
        run: cargo test --test headscale -- --ignored --nocapture
      - name: Print Windows Tailscale diagnostics
        if: failure()
        shell: pwsh
        run: |
          Get-Service Tailscale -ErrorAction Continue | Format-List *
          $tailscale = "$env:ProgramFiles\Tailscale\tailscale.exe"
          if (Test-Path $tailscale) {
            & $tailscale status --json
          }
          $log = Join-Path $env:RUNNER_TEMP "tailscale-msi.log"
          if (Test-Path $log) {
            Get-Content $log
          }
      - name: Print WSL Compose diagnostics
        if: failure()
        shell: wsl-bash {0}
        run: |
          uname -a
          docker version || true
          docker compose version || true
          docker compose -f tests/headscale/compose.yaml ps || true
          docker compose -f tests/headscale/compose.yaml logs --no-color || true
      - name: Log out Windows Tailscale
        if: always()
        continue-on-error: true
        shell: pwsh
        run: |
          $tailscale = "$env:ProgramFiles\Tailscale\tailscale.exe"
          if (Test-Path $tailscale) {
            & $tailscale logout
            if ($LASTEXITCODE -ne 0) {
              Write-Warning "tailscale logout failed with exit code $LASTEXITCODE"
            }
          }
      - name: Remove WSL integration environment
        if: always()
        shell: wsl-bash {0}
        run: docker compose -f tests/headscale/compose.yaml down -v --remove-orphans