github-action/action.yml
Sam Linville 8a5e5e971a
Update action.yml
Co-authored-by: Mario Minardi <mminardi@shaw.ca>
2025-08-12 09:16:12 -04:00

412 lines
18 KiB
YAML

# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
#
name: 'Connect Tailscale'
description: 'Connect your GitHub Action workflow to Tailscale'
branding:
icon: 'arrow-right-circle'
color: 'gray-dark'
inputs:
authkey:
description: 'Your Tailscale authentication key, from the admin panel.'
required: false
deprecationMessage: 'An OAuth API client https://tailscale.com/s/oauth-clients is recommended instead of an authkey'
oauth-client-id:
description: 'Your Tailscale OAuth Client ID.'
required: false
oauth-secret:
description: 'Your Tailscale OAuth Client Secret.'
required: false
oidc-client-id:
description: 'Your Tailscale OIDC Client ID.'
required: false
oidc-aud-claim:
description: 'Your Tailscale OIDC audience claim'
required: false
tailnet:
description: 'Your Tailscale tailnet name (e.g., example.com)'
required: false
tags:
description: 'Comma separated list of Tags to be applied to nodes. The OAuth client must have permission to apply these tags.'
required: false
version:
description: 'Tailscale version to use. Specify `latest` to use the latest stable version.'
required: true
default: '1.82.0'
sha256sum:
description: 'Expected SHA256 checksum of the tarball.'
required: false
default: ''
args:
description: 'Optional additional arguments to `tailscale up`'
required: false
default: ''
tailscaled-args:
description: 'Optional additional arguments to `tailscaled`'
required: false
default: ''
hostname:
description: 'Fixed hostname to use.'
required: false
default: ''
statedir:
description: 'Optional state directory to use (if unset, memory state is used)'
required: false
default: ''
timeout:
description: 'Timeout for `tailscale up`'
required: false
default: '2m'
retry:
description: 'Number of retries for `tailscale up`'
required: false
default: '5'
use-cache:
description: 'Whether to cache the Tailscale binaries (Linux/macOS) or installer (Windows)'
required: false
default: 'false'
runs:
using: 'composite'
steps:
- name: Check Runner OS
if: ${{ runner.os != 'Linux' && runner.os != 'Windows' && runner.os != 'macOS'}}
shell: bash
run: |
echo "::error title=⛔ error hint::Support Linux, Windows, and macOS Only"
exit 1
- name: Check Auth Info Empty
if: ${{ inputs.authkey == '' && (inputs.tags == '' || ((inputs['oauth-secret'] == '') && (inputs['oidc-client-id'] == '' || inputs['oidc-aud-claim'] == '')) }}
shell: bash
run: |
echo "::error title=⛔ error hint::OAuth and OIDC identity are empty, Maybe you need to populate it in the Secrets for your workflow, see more in https://docs.github.com/en/actions/security-guides/encrypted-secrets and https://tailscale.com/s/oauth-clients"
exit 1
- name: get OIDC token from GitHub Actions
if: ${{ inputs['oidc-client-id'] != '' && inputs['oidc-aud-claim'] != '' && inputs['oauth-secret'] == '' }}
shell: bash
run: |
JWT=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${{ inputs['oidc-aud-claim'] }}" | jq -r '.value')
if [ -z "$JWT" ]; then
echo "::error title=⛔ error hint::Failed to get JWT from GitHub Actions OIDC provider"
exit 1
fi
echo "::add-mask::$JWT" # Mask the JWT in the logs
echo "JWT=$JWT" >> $GITHUB_ENV
- name: Exchange JWT OIDC token and get authkey
if: ${{ inputs['oidc-client-id'] != '' && inputs['oidc-aud-claim'] != '' }}
shell: bash
run: |
echo "Exchanging JWT OIDC token for short-lived API token..."
RESPONSE=$(curl -X POST https://api.tailscale.com/api/v2/oauth/token-exchange \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${{ inputs['oidc-client-id'] }}" \
-d "jwt=$JWT")
export ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
echo "::add-mask::$ACCESS_TOKEN" # Mask the access token in the logs
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
echo "::error title=⛔ error hint::Failed to get ACCESS_TOKEN from OIDC token exchange, response: $RESPONSE"
exit 1
fi
echo "Retrieving authkey from Tailscale API"
# Convert comma-separated tags to JSON array format
TAGS_JSON=$(echo "${{ inputs.tags }}" | sed 's/,/","/g' | sed 's/^/["/' | sed 's/$/"]/')
# Make the API call and capture both the response and curl info
CURL_OUTPUT=$(curl -s -w "\n%{http_code}" -X POST https://api.tailscale.com/api/v2/tailnet/${{ inputs.tailnet }}/keys \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--data "{
\"keyType\": \"auth\",
\"capabilities\": {
\"devices\": {
\"create\": {
\"reusable\": false,
\"ephemeral\": true,
\"preauthorized\": true,
\"tags\": $TAGS_JSON
}
}
},
\"expirySeconds\": 3600
}")
# Extract HTTP status code (last line) and JSON response (everything else)
HTTP_STATUS=$(echo "$CURL_OUTPUT" | tail -n1)
JSON_RESPONSE=$(echo "$CURL_OUTPUT" | sed '$d')
echo "::add-mask::$JSON_RESPONSE" # Mask the JSON response in the logs
if [ "$HTTP_STATUS" != "200" ]; then
echo "::error title=⛔ error hint::Tailscale API returned HTTP $HTTP_STATUS"
exit 1
fi
export AUTHKEY=$(echo "$JSON_RESPONSE" | jq -r '.key')
echo "::add-mask::$AUTHKEY" # Mask the auth key in the logs
if [ -z "$AUTHKEY" ] || [ "$AUTHKEY" = "null" ]; then
echo "::error title=⛔ error hint::Failed to get AUTHKEY from Tailscale API"
exit 1
fi
echo "AUTHKEY=$AUTHKEY" >> $GITHUB_ENV
- name: Set Resolved Version
shell: bash
run: |
VERSION=${{ inputs.version }}
if [ "$VERSION" = "latest" ]; then
RESOLVED_VERSION=$(curl -H user-agent:tailscale-github-action -s "https://pkgs.tailscale.com/stable/?mode=json" | jq -r .Version)
else
RESOLVED_VERSION=$VERSION
fi
echo "RESOLVED_VERSION=$RESOLVED_VERSION" >> $GITHUB_ENV
echo "Resolved Tailscale version: $RESOLVED_VERSION"
- name: Set Tailscale Architecture - Linux
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
if [ ${{ runner.arch }} = "ARM64" ]; then
TS_ARCH="arm64"
elif [ ${{ runner.arch }} = "ARM" ]; then
TS_ARCH="arm"
elif [ ${{ runner.arch }} = "X86" ]; then
TS_ARCH="386"
else
TS_ARCH="amd64"
fi
echo "TS_ARCH=$TS_ARCH" >> $GITHUB_ENV
- name: Set Tailscale Architecture - Windows
if: ${{ runner.os == 'Windows' }}
shell: bash
run: |
if [ ${{ runner.arch }} = "ARM64" ]; then
TS_ARCH="arm64"
elif [ ${{ runner.arch }} = "X86" ]; then
TS_ARCH="x86"
else
TS_ARCH="amd64"
fi
echo "TS_ARCH=$TS_ARCH" >> $GITHUB_ENV
- name: Set SHA256 - Linux
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'})
if [ $((MINOR % 2)) -eq 0 ]; then
URL="https://pkgs.tailscale.com/stable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz.sha256"
else
URL="https://pkgs.tailscale.com/unstable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz.sha256"
fi
if [[ "${{ inputs.sha256sum }}" ]]; then
SHA256SUM="${{ inputs.sha256sum }}"
else
SHA256SUM="$(curl -H user-agent:tailscale-github-action -L "${URL}" --fail)"
fi
echo "SHA256SUM=$SHA256SUM" >> $GITHUB_ENV
- name: Restore Tailscale Binary - Linux
if: ${{ inputs.use-cache == 'true' && runner.os == 'Linux' }}
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: restore-cache-tailscale-linux
with:
path: tailscale.tgz
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }}
- name: Download Tailscale - Linux
if: ${{ runner.os == 'Linux' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-linux.outputs.cache-hit != 'true') }}
shell: bash
run: |
MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'})
if [ $((MINOR % 2)) -eq 0 ]; then
URL="https://pkgs.tailscale.com/stable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz"
else
URL="https://pkgs.tailscale.com/unstable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz"
fi
echo "Downloading $URL"
curl -H user-agent:tailscale-github-action -L "$URL" -o tailscale.tgz --max-time 300 --fail
echo "Expected sha256: $SHA256SUM"
echo "Actual sha256: $(sha256sum tailscale.tgz)"
echo "$SHA256SUM tailscale.tgz" | sha256sum -c
- name: Save Tailscale Binary - Linux
if: ${{ inputs.use-cache == 'true' && steps.restore-cache-tailscale-linux.outputs.cache-hit != 'true' && runner.os == 'Linux' }}
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: save-cache-tailscale-linux
with:
path: tailscale.tgz
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }}
- name: Install Tailscale - Linux
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
tar -C /tmp -xzf tailscale.tgz
rm tailscale.tgz
TSPATH=/tmp/tailscale_${RESOLVED_VERSION}_${TS_ARCH}
sudo mv "${TSPATH}/tailscale" "${TSPATH}/tailscaled" /usr/bin
- name: Set SHA256 - Windows
if: ${{ runner.os == 'Windows' }}
shell: bash
run: |
MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'})
if [ $((MINOR % 2)) -eq 0 ]; then
URL="https://pkgs.tailscale.com/stable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi.sha256"
else
URL="https://pkgs.tailscale.com/unstable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi.sha256"
fi
if [[ "${{ inputs.sha256sum }}" ]]; then
SHA256SUM="${{ inputs.sha256sum }}"
else
SHA256SUM="$(curl -H user-agent:tailscale-github-action -L "${URL}" --fail)"
fi
echo "SHA256SUM=$SHA256SUM" >> $GITHUB_ENV
- name: Restore Tailscale Binary - Windows
if: ${{ inputs.use-cache == 'true' && runner.os == 'Windows' }}
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: restore-cache-tailscale-windows
with:
path: tailscale.msi
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }}
- name: Download Tailscale - Windows
if: ${{ runner.os == 'Windows' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-windows.outputs.cache-hit != 'true') }}
shell: bash
run: |
MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'})
if [ $((MINOR % 2)) -eq 0 ]; then
URL="https://pkgs.tailscale.com/stable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi"
else
URL="https://pkgs.tailscale.com/unstable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi"
fi
echo "Downloading $URL"
curl -H user-agent:tailscale-github-action -L "$URL" -o tailscale.msi --max-time 300 --fail
echo "Expected sha256: $SHA256SUM"
echo "Actual sha256: $(sha256sum tailscale.msi)"
echo "$SHA256SUM tailscale.msi" | sha256sum -c
- name: Save Tailscale Binary - Windows
if: ${{ inputs.use-cache == 'true' && steps.restore-cache-tailscale-windows.outputs.cache-hit != 'true' && runner.os == 'Windows' }}
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: save-cache-tailscale-windows
with:
path: tailscale.msi
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }}
- name: Install Tailscale - Windows
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
Start-Process "C:\Windows\System32\msiexec.exe" -Wait -ArgumentList @('/quiet', '/l*v ${{ runner.temp }}/tailscale.log', '/i', 'tailscale.msi')
Add-Content $env:GITHUB_PATH "C:\Program Files\Tailscale\"
Remove-Item tailscale.msi -Force;
- name: Checkout Tailscale repo - macOS
id: checkout-tailscale-macos
if: ${{ runner.os == 'macOS' }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: tailscale/tailscale
path: ${{ github.workspace }}/tailscale
ref: v${{ env.RESOLVED_VERSION }}
- name: Restore Tailscale - macOS
if: ${{ inputs.use-cache == 'true' && runner.os == 'macOS' }}
id: restore-cache-tailscale-macos
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
/usr/local/bin/tailscale
/usr/local/bin/tailscaled
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ runner.arch }}-${{ steps.checkout-tailscale-macos.outputs.commit }}
- name: Build Tailscale binaries - macOS
if: ${{ runner.os == 'macOS' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-macos.outputs.cache-hit != 'true') }}
shell: bash
run: |
cd tailscale
export TS_USE_TOOLCHAIN=1
./build_dist.sh ./cmd/tailscale
./build_dist.sh ./cmd/tailscaled
sudo mv tailscale tailscaled /usr/local/bin
- name: Remove tailscale checkout - macOS
if: ${{ runner.os == 'macOS' }}
shell: bash
run: |
rm -Rf ${{ github.workspace }}/tailscale
- name: Save Tailscale - macOS
if: ${{ inputs.use-cache == 'true' && runner.os == 'macOS' }}
id: save-cache-tailscale-macos
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
/usr/local/bin/tailscale
/usr/local/bin/tailscaled
key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ runner.arch }}-${{ steps.checkout-tailscale-macos.outputs.commit }}
- name: Install timeout - macOS
if: ${{ runner.os == 'macOS' }}
shell: bash
run:
brew install coreutils # for 'timeout'
- name: Start Tailscale Daemon - non-Windows
if: ${{ runner.os != 'Windows' }}
shell: bash
env:
ADDITIONAL_DAEMON_ARGS: ${{ inputs.tailscaled-args }}
STATEDIR: ${{ inputs.statedir }}
run: |
if [ "$STATEDIR" == "" ]; then
STATE_ARGS="--state=mem:"
else
STATE_ARGS="--statedir=${STATEDIR}"
mkdir -p "$STATEDIR"
fi
sudo -E tailscaled ${STATE_ARGS} ${ADDITIONAL_DAEMON_ARGS} 2>~/tailscaled.log &
# And check that tailscaled came up. The CLI will block for a bit waiting
# for it. And --json will make it exit with status 0 even if we're logged
# out (as we will be). Without --json it returns an error if we're not up.
sudo -E tailscale status --json >/dev/null
- name: Connect to Tailscale
shell: bash
env:
ADDITIONAL_ARGS: ${{ inputs.args }}
HOSTNAME: ${{ inputs.hostname }}
TAILSCALE_AUTHKEY: ${{ inputs.authkey }}
TIMEOUT: ${{ inputs.timeout }}
RETRY: ${{ inputs.retry }}
run: |
if [ -z "${HOSTNAME}" ]; then
if [ "${{ runner.os }}" == "Windows" ]; then
HOSTNAME="github-$COMPUTERNAME"
else
HOSTNAME="github-$(hostname)"
fi
fi
if [ -n "${{ inputs['oauth-secret'] }}" ]; then
TAILSCALE_AUTHKEY="${{ inputs['oauth-secret'] }}?preauthorized=true&ephemeral=true"
TAGS_ARG="--advertise-tags=${{ inputs.tags }}"
fi
if [ -n "${{ inputs['oidc-aud-claim'] }}" ] && [ -n "${{ inputs['oidc-client-id'] }}" ]; then
if [ -z "$AUTHKEY" ]; then
echo "::error title=⛔ error hint::Failed to get AUTHKEY from OIDC token exchange"
exit 1
fi
TAILSCALE_AUTHKEY="$AUTHKEY"
TAGS_ARG="--advertise-tags=${{ inputs.tags }}"
fi
if [ "${{ runner.os }}" != "Windows" ]; then
MAYBE_SUDO="sudo -E"
fi
if [ "${{ runner.os }}" == "Windows" ]; then
PLATFORM_SPECIFIC_ARGS="--unattended"
fi
for ((i=1;i<=$RETRY;i++)); do
echo "Attempt $i to bring up Tailscale..."
timeout --verbose --kill-after=1s ${TIMEOUT} ${MAYBE_SUDO} tailscale up ${TAGS_ARG} --authkey=${TAILSCALE_AUTHKEY} --hostname=${HOSTNAME} --accept-routes ${PLATFORM_SPECIFIC_ARGS} ${ADDITIONAL_ARGS} && break
echo "Tailscale up failed. Retrying in $((i * 5)) seconds..."
sleep $((i * 5))
done