Files
headscale/.github/workflows/tailscale-action-integration.yaml
T
Kristoffer Dalby c3e48f039c ci: integration-test the tailscale GitHub action against headscale
Stand up a nix-built headscale over self-signed TLS with embedded DERP and a
regular node joined via pre-auth, then drive the official tailscale/github-action
against it: connect over OAuth (tskey-client-) and an auth key with ping as the
success gate, and exercise its hostname, version, tailscaled-args, statedir and
args inputs.
2026-09-25 17:09:19 +02:00

156 lines
5.8 KiB
YAML

name: tailscale-action integration
# Integration tests for the official Tailscale GitHub Action
# (https://github.com/tailscale/github-action) against a self-hosted headscale.
# A nix-built headscale runs over self-signed TLS with embedded DERP and a
# regular node already joined; the action then connects a runner to that tailnet
# and its built-in ping to the existing node is the success gate. SQLite only.
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
paths:
- ".github/workflows/tailscale-action-integration.yaml"
- ".github/actions/headscale-up/**"
- "hscontrol/api/v2/**"
- "hscontrol/scope/**"
- "hscontrol/db/oauth*.go"
- "hscontrol/db/preauth_keys.go"
- "hscontrol/types/oauth.go"
- "hscontrol/state/**"
- "hscontrol/auth*.go"
- "hscontrol/noise.go"
- "cmd/headscale/cli/oauth_client.go"
- "flake.*"
- "go.mod"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# The action connects a runner to headscale and pings the already-joined node.
# Both of the action's auth modes are exercised: an OAuth client (delivered as a
# tskey-client- secret the client resolves against headscale) and a plain auth
# key. Ping is the pass/fail gate — real connectivity, not just registration.
connect:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
auth: [oauth, authkey]
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- id: hs
uses: ./.github/actions/headscale-up
- name: Prepare ${{ matrix.auth }} credential
id: prep
env:
URL: ${{ steps.hs.outputs.url }}
PREAUTH: ${{ steps.hs.outputs.preauth }}
run: |
set -euo pipefail
if [ "${{ matrix.auth }}" = "oauth" ]; then
FULL=$(./result/bin/headscale oauth-clients create -s auth_keys -t tag:ci -o json | jq -r .key)
# The upstream client only runs its OAuth exchange for tskey-client-
# secrets, and reads the control URL from a baseURL= attribute on the
# secret itself (--login-server governs registration, not the exchange).
TS="tskey-${FULL#hskey-}?baseURL=${URL}&ephemeral=true&preauthorized=true"
echo "::add-mask::$TS"
echo "authkey=$TS" >> "$GITHUB_OUTPUT"
echo "args=--login-server=${URL} --advertise-tags=tag:ci" >> "$GITHUB_OUTPUT"
else
echo "authkey=${PREAUTH}" >> "$GITHUB_OUTPUT"
echo "args=--login-server=${URL}" >> "$GITHUB_OUTPUT"
fi
- name: Connect via the tailscale action + ping
uses: tailscale/github-action@d1b6cd204f8dceda5b3eaad7f1f767be390056cd # v4.2.0
with:
authkey: ${{ steps.prep.outputs.authkey }}
args: ${{ steps.prep.outputs.args }}
ping: ${{ steps.hs.outputs.pre_ip }}
- name: Assert node registered
run: |
set -euo pipefail
./result/bin/headscale nodes list -o json > nodes.json
test "$(jq length nodes.json)" -ge 2
if [ "${{ matrix.auth }}" = "oauth" ]; then
jq -e '[.[] | select((.tags // []) | index("tag:ci"))] | length >= 1' nodes.json
fi
- name: Dump logs on failure
if: failure()
run: |
for f in /tmp/hs/serve.log /tmp/pre-tailscaled.log "$HOME/tailscaled.log"; do
echo "::group::$f"
sudo tail -n 300 "$f" || true
echo "::endgroup::"
done
# The action's remaining inputs, each connecting via an auth key with ping as
# the connectivity gate plus a per-input assertion. Unused inputs are empty and
# fall back to the action's defaults.
inputs:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- input: hostname
hostname: feature-host
assert: ./result/bin/headscale nodes list -o json | jq -e '[.[]|select(.givenName=="feature-host")]|length>=1'
- input: version
version: "1.98.4"
# The running daemon, not just the CLI binary, must be the pinned one.
assert: tailscale status --json | jq -e '.Version | startswith("1.98.4")'
- input: tailscaled-args
tailscaled_args: --verbose=1
assert: pgrep -af 'tailscaled.*--verbose=1'
- input: statedir
statedir: /tmp/ts-state
assert: sudo ls -A /tmp/ts-state | grep -q .
- input: args
extra_args: --accept-dns=false
assert: sudo tailscale debug prefs | jq -e '.CorpDNS == false'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- id: hs
uses: ./.github/actions/headscale-up
- name: Connect with ${{ matrix.input }}
uses: tailscale/github-action@d1b6cd204f8dceda5b3eaad7f1f767be390056cd # v4.2.0
with:
authkey: ${{ steps.hs.outputs.preauth }}
args: --login-server=${{ steps.hs.outputs.url }} ${{ matrix.extra_args }}
hostname: ${{ matrix.hostname }}
version: ${{ matrix.version }}
tailscaled-args: ${{ matrix.tailscaled_args }}
statedir: ${{ matrix.statedir }}
ping: ${{ steps.hs.outputs.pre_ip }}
- name: Assert ${{ matrix.input }}
env:
ASSERT: ${{ matrix.assert }}
run: |
set -euo pipefail
bash -c "$ASSERT"
- name: Dump logs on failure
if: failure()
run: |
for f in /tmp/hs/serve.log /tmp/pre-tailscaled.log "$HOME/tailscaled.log"; do
echo "::group::$f"
sudo tail -n 300 "$f" || true
echo "::endgroup::"
done