The README says persistent self-hosted runners are supported and that the action "leaves tailscale binaries installed but stops the tailscale background processes" (added in #206). However, on persistent runners where GitHub's actions/cache backend doesn't return a cache hit, the action still fails on the second job with:
Downloading https://pkgs.tailscale.com/stable/tailscale_<version>_amd64.tgz
Destination file path /home/<user>/.cache/tailscale.tgz already exists
##[error]Destination file path /home/<user>/.cache/tailscale.tgz already exists
(repeated 3× by the retry helper)
Repro
- Persistent self-hosted runner where
$HOME is preserved across jobs (Aspect Workflows in our case).
cache.restoreCache(...) from @actions/cache does not hit (no Found Tailscale … in cache line in the logs — it goes straight to Downloading …). This happens whenever the runner platform doesn't expose a working GH Actions cache backend.
- Run two consecutive jobs that use this action. The second one fails as above.
Real-world failure: https://github.com/sandbox-quantum/yak/actions/runs/25177419595/job/73813221947 (logs, reproduced on v4.1.2).
Root cause
src/main.ts#L432-L434:
const tarDest = path.join(xdgCacheDir(), "tailscale.tgz");
fs.mkdirSync(path.dirname(tarDest), { recursive: true });
const tarPath = await tc.downloadTool(downloadUrl, tarDest);
tc.downloadTool from @actions/tool-cache refuses to overwrite an existing destination (source — downloadToolAttempt throws if fs.existsSync(dest)). The action only avoids hitting this code path when the cloud cache restore succeeds, so on runners without a working cache backend the leaked .tgz from the previous job collides on every subsequent job.
#206 addressed a related but distinct bug (lingering tailscaled daemon → Text file busy); the tarball-leak case wasn't covered. The Windows MSI path has a needsDownload check (L538), but the Linux path is unconditional.
Suggested fix
Either of:
- Pre-clean the destination before downloading:
fs.rmSync(tarDest, { force: true });
const tarPath = await tc.downloadTool(downloadUrl, tarDest);
- Pass no
dest argument so tc.downloadTool uses a fresh temp path each time, then delete it after extraction.
- Mirror the Windows
needsDownload pattern: if the .tgz already exists and its sha256 matches config.sha256Sum, skip the download.
Happy to send a PR if option 1 or 3 is acceptable.
Workaround
Add a step before the action:
- run: rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/tailscale.tgz"
The README says persistent self-hosted runners are supported and that the action "leaves tailscale binaries installed but stops the tailscale background processes" (added in #206). However, on persistent runners where GitHub's
actions/cachebackend doesn't return a cache hit, the action still fails on the second job with:(repeated 3× by the retry helper)
Repro
$HOMEis preserved across jobs (Aspect Workflows in our case).cache.restoreCache(...)from@actions/cachedoes not hit (noFound Tailscale … in cacheline in the logs — it goes straight toDownloading …). This happens whenever the runner platform doesn't expose a working GH Actions cache backend.Real-world failure: https://github.com/sandbox-quantum/yak/actions/runs/25177419595/job/73813221947 (logs, reproduced on v4.1.2).
Root cause
src/main.ts#L432-L434:tc.downloadToolfrom@actions/tool-cacherefuses to overwrite an existing destination (source —downloadToolAttemptthrows iffs.existsSync(dest)). The action only avoids hitting this code path when the cloud cache restore succeeds, so on runners without a working cache backend the leaked.tgzfrom the previous job collides on every subsequent job.#206 addressed a related but distinct bug (lingering
tailscaleddaemon →Text file busy); the tarball-leak case wasn't covered. The Windows MSI path has aneedsDownloadcheck (L538), but the Linux path is unconditional.Suggested fix
Either of:
destargument sotc.downloadTooluses a fresh temp path each time, then delete it after extraction.needsDownloadpattern: if the.tgzalready exists and its sha256 matchesconfig.sha256Sum, skip the download.Happy to send a PR if option 1 or 3 is acceptable.
Workaround
Add a step before the action: