├── .github └── workflows │ └── tailscale.yml ├── LICENSE ├── README.md └── action.yml /.github/workflows/tailscale.yml: -------------------------------------------------------------------------------- 1 | name: tailscale 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macos-latest] 17 | cache: ['false', 'true'] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | 23 | - name: Tailscale Action 24 | uses: ./ 25 | with: 26 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 27 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 28 | tags: tag:ci 29 | use-cache: ${{ matrix.cache }} 30 | 31 | - name: check for tailscale connection 32 | shell: bash 33 | run: 34 | tailscale status -json | jq -r .BackendState | grep -q Running 35 | 36 | - name: ensure no dirty files from Tailscale Action remain 37 | shell: bash 38 | run: | 39 | extra_files=$(git ls-files . --exclude-standard --others) 40 | if [ ! -z "$extra_files" ]; then 41 | echo "::error::Unexpected extra files: $extra_files" 42 | exit 1 43 | fi 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020 Tailscale & AUTHORS. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailscale GitHub Action 2 | 3 | This GitHub Action connects to your [Tailscale network](https://tailscale.com) 4 | by adding a step to your workflow. 5 | 6 | ```yaml 7 | - name: Tailscale 8 | uses: tailscale/github-action@v3 9 | with: 10 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 11 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 12 | tags: tag:ci 13 | ``` 14 | 15 | Subsequent steps in the Action can then access nodes in your Tailnet. 16 | 17 | oauth-client-id and oauth-secret are an [OAuth client](https://tailscale.com/s/oauth-clients/) 18 | for the tailnet to be accessed. We recommend storing these as 19 | [GitHub Encrypted Secrets.](https://docs.github.com/en/actions/security-guides/encrypted-secrets) 20 | OAuth clients used for this purpose must have the 21 | [`auth_keys` scope.](https://tailscale.com/kb/1215/oauth-clients#scopes) 22 | 23 | tags is a comma-separated list of one or more [ACL Tags](https://tailscale.com/kb/1068/acl-tags/) 24 | for the node. At least one tag is required: an OAuth client is not associated 25 | with any of the Users on the tailnet, it has to Tag its nodes. 26 | 27 | Nodes created by this Action are [marked as Ephemeral](https://tailscale.com/s/ephemeral-nodes) to 28 | be automatically removed by the coordination server a short time after they 29 | finish their run. The nodes are also [marked Preapproved](https://tailscale.com/kb/1085/auth-keys/) 30 | on tailnets which use [Device Approval](https://tailscale.com/kb/1099/device-approval/) 31 | 32 | ## Tailnet Lock 33 | 34 | If you are using this Action in a [Tailnet 35 | Lock](https://tailscale.com/kb/1226/tailnet-lock) enabled network, you need to: 36 | 37 | * Authenticate using an ephemeral reusable [pre-signed auth key]( 38 | https://tailscale.com/kb/1226/tailnet-lock#add-a-node-using-a-pre-signed-auth-key) 39 | rather than an OAuth client. 40 | * Specify a [state directory]( 41 | https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled) for the 42 | client to store the Tailnet Key Authority data in. 43 | 44 | ```yaml 45 | - name: Tailscale 46 | uses: tailscale/github-action@v3 47 | with: 48 | authkey: tskey-auth-... 49 | statedir: /tmp/tailscale-state/ 50 | ``` 51 | 52 | ## Defining Tailscale version 53 | 54 | Which Tailscale version to use can be set like this: 55 | 56 | ```yaml 57 | - name: Tailscale 58 | uses: tailscale/github-action@v3 59 | with: 60 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 61 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 62 | tags: tag:ci 63 | version: 1.52.0 64 | ``` 65 | 66 | If you'd like to specify the latest version, simply set the version as `latest` 67 | 68 | ```yaml 69 | - name: Tailscale 70 | uses: tailscale/github-action@v3 71 | with: 72 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 73 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 74 | tags: tag:ci 75 | version: latest 76 | ``` 77 | 78 | You can find the latest Tailscale stable version number at 79 | https://pkgs.tailscale.com/stable/#static. 80 | 81 | 82 | ## Cache Tailscale binaries 83 | 84 | Caching can reduce download times and download failures on runners with slower network connectivity. Although caching is not enabled by default, it is generally recommended. 85 | 86 | You can opt in to caching Tailscale binaries by passing `'true'` to the `use-cache` input: 87 | 88 | ```yaml 89 | - name: Tailscale 90 | uses: tailscale/github-action@v3 91 | with: 92 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 93 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 94 | use-cache: 'true' 95 | ``` 96 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Tailscale Inc & AUTHORS 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # 4 | name: 'Connect Tailscale' 5 | description: 'Connect your GitHub Action workflow to Tailscale' 6 | branding: 7 | icon: 'arrow-right-circle' 8 | color: 'gray-dark' 9 | inputs: 10 | authkey: 11 | description: 'Your Tailscale authentication key, from the admin panel.' 12 | required: false 13 | deprecationMessage: 'An OAuth API client https://tailscale.com/s/oauth-clients is recommended instead of an authkey' 14 | oauth-client-id: 15 | description: 'Your Tailscale OAuth Client ID.' 16 | required: false 17 | oauth-secret: 18 | description: 'Your Tailscale OAuth Client Secret.' 19 | required: false 20 | tags: 21 | description: 'Comma separated list of Tags to be applied to nodes. The OAuth client must have permission to apply these tags.' 22 | required: false 23 | version: 24 | description: 'Tailscale version to use. Specify `latest` to use the latest stable version.' 25 | required: true 26 | default: '1.82.0' 27 | sha256sum: 28 | description: 'Expected SHA256 checksum of the tarball.' 29 | required: false 30 | default: '' 31 | args: 32 | description: 'Optional additional arguments to `tailscale up`' 33 | required: false 34 | default: '' 35 | tailscaled-args: 36 | description: 'Optional additional arguments to `tailscaled`' 37 | required: false 38 | default: '' 39 | hostname: 40 | description: 'Fixed hostname to use.' 41 | required: false 42 | default: '' 43 | statedir: 44 | description: 'Optional state directory to use (if unset, memory state is used)' 45 | required: false 46 | default: '' 47 | timeout: 48 | description: 'Timeout for `tailscale up`' 49 | required: false 50 | default: '2m' 51 | retry: 52 | description: 'Number of retries for `tailscale up`' 53 | required: false 54 | default: '5' 55 | use-cache: 56 | description: 'Whether to cache the Tailscale binaries (Linux/macOS) or installer (Windows)' 57 | required: false 58 | default: 'false' 59 | runs: 60 | using: 'composite' 61 | steps: 62 | - name: Check Runner OS 63 | if: ${{ runner.os != 'Linux' && runner.os != 'Windows' && runner.os != 'macOS'}} 64 | shell: bash 65 | run: | 66 | echo "::error title=⛔ error hint::Support Linux, Windows, and macOS Only" 67 | exit 1 68 | - name: Check Auth Info Empty 69 | if: ${{ inputs.authkey == '' && (inputs['oauth-secret'] == '' || inputs.tags == '') }} 70 | shell: bash 71 | run: | 72 | echo "::error title=⛔ error hint::OAuth identity 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" 73 | exit 1 74 | 75 | - name: Set Resolved Version 76 | shell: bash 77 | run: | 78 | VERSION=${{ inputs.version }} 79 | if [ "$VERSION" = "latest" ]; then 80 | RESOLVED_VERSION=$(curl -H user-agent:tailscale-github-action -s "https://pkgs.tailscale.com/stable/?mode=json" | jq -r .Version) 81 | else 82 | RESOLVED_VERSION=$VERSION 83 | fi 84 | echo "RESOLVED_VERSION=$RESOLVED_VERSION" >> $GITHUB_ENV 85 | echo "Resolved Tailscale version: $RESOLVED_VERSION" 86 | - name: Set Tailscale Architecture - Linux 87 | if: ${{ runner.os == 'Linux' }} 88 | shell: bash 89 | run: | 90 | if [ ${{ runner.arch }} = "ARM64" ]; then 91 | TS_ARCH="arm64" 92 | elif [ ${{ runner.arch }} = "ARM" ]; then 93 | TS_ARCH="arm" 94 | elif [ ${{ runner.arch }} = "X86" ]; then 95 | TS_ARCH="386" 96 | else 97 | TS_ARCH="amd64" 98 | fi 99 | echo "TS_ARCH=$TS_ARCH" >> $GITHUB_ENV 100 | 101 | - name: Set Tailscale Architecture - Windows 102 | if: ${{ runner.os == 'Windows' }} 103 | shell: bash 104 | run: | 105 | if [ ${{ runner.arch }} = "ARM64" ]; then 106 | TS_ARCH="arm64" 107 | elif [ ${{ runner.arch }} = "X86" ]; then 108 | TS_ARCH="x86" 109 | else 110 | TS_ARCH="amd64" 111 | fi 112 | echo "TS_ARCH=$TS_ARCH" >> $GITHUB_ENV 113 | 114 | - name: Set SHA256 - Linux 115 | if: ${{ runner.os == 'Linux' }} 116 | shell: bash 117 | run: | 118 | MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'}) 119 | if [ $((MINOR % 2)) -eq 0 ]; then 120 | URL="https://pkgs.tailscale.com/stable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz.sha256" 121 | else 122 | URL="https://pkgs.tailscale.com/unstable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz.sha256" 123 | fi 124 | 125 | if [[ "${{ inputs.sha256sum }}" ]]; then 126 | SHA256SUM="${{ inputs.sha256sum }}" 127 | else 128 | SHA256SUM="$(curl -H user-agent:tailscale-github-action -L "${URL}" --fail)" 129 | fi 130 | echo "SHA256SUM=$SHA256SUM" >> $GITHUB_ENV 131 | 132 | - name: Restore Tailscale Binary - Linux 133 | if: ${{ inputs.use-cache == 'true' && runner.os == 'Linux' }} 134 | uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 135 | id: restore-cache-tailscale-linux 136 | with: 137 | path: tailscale.tgz 138 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }} 139 | 140 | - name: Download Tailscale - Linux 141 | if: ${{ runner.os == 'Linux' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-linux.outputs.cache-hit != 'true') }} 142 | shell: bash 143 | run: | 144 | MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'}) 145 | if [ $((MINOR % 2)) -eq 0 ]; then 146 | URL="https://pkgs.tailscale.com/stable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz" 147 | else 148 | URL="https://pkgs.tailscale.com/unstable/tailscale_${RESOLVED_VERSION}_${TS_ARCH}.tgz" 149 | fi 150 | echo "Downloading $URL" 151 | curl -H user-agent:tailscale-github-action -L "$URL" -o tailscale.tgz --max-time 300 --fail 152 | echo "Expected sha256: $SHA256SUM" 153 | echo "Actual sha256: $(sha256sum tailscale.tgz)" 154 | echo "$SHA256SUM tailscale.tgz" | sha256sum -c 155 | 156 | - name: Save Tailscale Binary - Linux 157 | if: ${{ inputs.use-cache == 'true' && steps.restore-cache-tailscale-linux.outputs.cache-hit != 'true' && runner.os == 'Linux' }} 158 | uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 159 | id: save-cache-tailscale-linux 160 | with: 161 | path: tailscale.tgz 162 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }} 163 | 164 | - name: Install Tailscale - Linux 165 | if: ${{ runner.os == 'Linux' }} 166 | shell: bash 167 | run: | 168 | tar -C /tmp -xzf tailscale.tgz 169 | rm tailscale.tgz 170 | TSPATH=/tmp/tailscale_${RESOLVED_VERSION}_${TS_ARCH} 171 | sudo mv "${TSPATH}/tailscale" "${TSPATH}/tailscaled" /usr/bin 172 | 173 | - name: Set SHA256 - Windows 174 | if: ${{ runner.os == 'Windows' }} 175 | shell: bash 176 | run: | 177 | MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'}) 178 | if [ $((MINOR % 2)) -eq 0 ]; then 179 | URL="https://pkgs.tailscale.com/stable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi.sha256" 180 | else 181 | URL="https://pkgs.tailscale.com/unstable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi.sha256" 182 | fi 183 | 184 | if [[ "${{ inputs.sha256sum }}" ]]; then 185 | SHA256SUM="${{ inputs.sha256sum }}" 186 | else 187 | SHA256SUM="$(curl -H user-agent:tailscale-github-action -L "${URL}" --fail)" 188 | fi 189 | echo "SHA256SUM=$SHA256SUM" >> $GITHUB_ENV 190 | 191 | - name: Restore Tailscale Binary - Windows 192 | if: ${{ inputs.use-cache == 'true' && runner.os == 'Windows' }} 193 | uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 194 | id: restore-cache-tailscale-windows 195 | with: 196 | path: tailscale.msi 197 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }} 198 | 199 | - name: Download Tailscale - Windows 200 | if: ${{ runner.os == 'Windows' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-windows.outputs.cache-hit != 'true') }} 201 | shell: bash 202 | run: | 203 | MINOR=$(echo "$RESOLVED_VERSION" | awk -F '.' {'print $2'}) 204 | if [ $((MINOR % 2)) -eq 0 ]; then 205 | URL="https://pkgs.tailscale.com/stable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi" 206 | else 207 | URL="https://pkgs.tailscale.com/unstable/tailscale-setup-${RESOLVED_VERSION}-${TS_ARCH}.msi" 208 | fi 209 | echo "Downloading $URL" 210 | curl -H user-agent:tailscale-github-action -L "$URL" -o tailscale.msi --max-time 300 --fail 211 | echo "Expected sha256: $SHA256SUM" 212 | echo "Actual sha256: $(sha256sum tailscale.msi)" 213 | echo "$SHA256SUM tailscale.msi" | sha256sum -c 214 | 215 | - name: Save Tailscale Binary - Windows 216 | if: ${{ inputs.use-cache == 'true' && steps.restore-cache-tailscale-windows.outputs.cache-hit != 'true' && runner.os == 'Windows' }} 217 | uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 218 | id: save-cache-tailscale-windows 219 | with: 220 | path: tailscale.msi 221 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ env.TS_ARCH }}-${{ env.SHA256SUM }} 222 | 223 | - name: Install Tailscale - Windows 224 | if: ${{ runner.os == 'Windows' }} 225 | shell: pwsh 226 | run: | 227 | Start-Process "C:\Windows\System32\msiexec.exe" -Wait -ArgumentList @('/quiet', '/l*v ${{ runner.temp }}/tailscale.log', '/i', 'tailscale.msi') 228 | Add-Content $env:GITHUB_PATH "C:\Program Files\Tailscale\" 229 | Remove-Item tailscale.msi -Force; 230 | - name: Checkout Tailscale repo - macOS 231 | id: checkout-tailscale-macos 232 | if: ${{ runner.os == 'macOS' }} 233 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 234 | with: 235 | repository: tailscale/tailscale 236 | path: ${{ github.workspace }}/tailscale 237 | ref: v${{ env.RESOLVED_VERSION }} 238 | - name: Restore Tailscale - macOS 239 | if: ${{ inputs.use-cache == 'true' && runner.os == 'macOS' }} 240 | id: restore-cache-tailscale-macos 241 | uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 242 | with: 243 | path: | 244 | /usr/local/bin/tailscale 245 | /usr/local/bin/tailscaled 246 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ runner.arch }}-${{ steps.checkout-tailscale-macos.outputs.commit }} 247 | - name: Build Tailscale binaries - macOS 248 | if: ${{ runner.os == 'macOS' && (inputs.use-cache != 'true' || steps.restore-cache-tailscale-macos.outputs.cache-hit != 'true') }} 249 | shell: bash 250 | run: | 251 | cd tailscale 252 | export TS_USE_TOOLCHAIN=1 253 | ./build_dist.sh ./cmd/tailscale 254 | ./build_dist.sh ./cmd/tailscaled 255 | sudo mv tailscale tailscaled /usr/local/bin 256 | - name: Remove tailscale checkout - macOS 257 | if: ${{ runner.os == 'macOS' }} 258 | shell: bash 259 | run: | 260 | rm -Rf ${{ github.workspace }}/tailscale 261 | - name: Save Tailscale - macOS 262 | if: ${{ inputs.use-cache == 'true' && runner.os == 'macOS' }} 263 | id: save-cache-tailscale-macos 264 | uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 265 | with: 266 | path: | 267 | /usr/local/bin/tailscale 268 | /usr/local/bin/tailscaled 269 | key: ${{ runner.os }}-tailscale-${{ env.RESOLVED_VERSION }}-${{ runner.arch }}-${{ steps.checkout-tailscale-macos.outputs.commit }} 270 | - name: Install timeout - macOS 271 | if: ${{ runner.os == 'macOS' }} 272 | shell: bash 273 | run: 274 | brew install coreutils # for 'timeout' 275 | - name: Start Tailscale Daemon - non-Windows 276 | if: ${{ runner.os != 'Windows' }} 277 | shell: bash 278 | env: 279 | ADDITIONAL_DAEMON_ARGS: ${{ inputs.tailscaled-args }} 280 | STATEDIR: ${{ inputs.statedir }} 281 | run: | 282 | if [ "$STATEDIR" == "" ]; then 283 | STATE_ARGS="--state=mem:" 284 | else 285 | STATE_ARGS="--statedir=${STATEDIR}" 286 | mkdir -p "$STATEDIR" 287 | fi 288 | sudo -E tailscaled ${STATE_ARGS} ${ADDITIONAL_DAEMON_ARGS} 2>~/tailscaled.log & 289 | # And check that tailscaled came up. The CLI will block for a bit waiting 290 | # for it. And --json will make it exit with status 0 even if we're logged 291 | # out (as we will be). Without --json it returns an error if we're not up. 292 | sudo -E tailscale status --json >/dev/null 293 | - name: Connect to Tailscale 294 | shell: bash 295 | env: 296 | ADDITIONAL_ARGS: ${{ inputs.args }} 297 | HOSTNAME: ${{ inputs.hostname }} 298 | TAILSCALE_AUTHKEY: ${{ inputs.authkey }} 299 | TIMEOUT: ${{ inputs.timeout }} 300 | RETRY: ${{ inputs.retry }} 301 | run: | 302 | if [ -z "${HOSTNAME}" ]; then 303 | if [ "${{ runner.os }}" == "Windows" ]; then 304 | HOSTNAME="github-$COMPUTERNAME" 305 | else 306 | HOSTNAME="github-$(hostname)" 307 | fi 308 | fi 309 | if [ -n "${{ inputs['oauth-secret'] }}" ]; then 310 | TAILSCALE_AUTHKEY="${{ inputs['oauth-secret'] }}?preauthorized=true&ephemeral=true" 311 | TAGS_ARG="--advertise-tags=${{ inputs.tags }}" 312 | fi 313 | if [ "${{ runner.os }}" != "Windows" ]; then 314 | MAYBE_SUDO="sudo -E" 315 | fi 316 | for ((i=1;i<=$RETRY;i++)); do 317 | echo "Attempt $i to bring up Tailscale..." 318 | timeout --verbose --kill-after=1s ${TIMEOUT} ${MAYBE_SUDO} tailscale up ${TAGS_ARG} --authkey=${TAILSCALE_AUTHKEY} --hostname=${HOSTNAME} --accept-routes ${ADDITIONAL_ARGS} && break 319 | echo "Tailscale up failed. Retrying in $((i * 5)) seconds..." 320 | sleep $((i * 5)) 321 | done 322 | --------------------------------------------------------------------------------