From 3a91841745f171023de16dc7a0fb5cfb0f7599ae Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Wed, 14 Jan 2026 10:07:09 +0200 Subject: [PATCH] fix: Windows MSI download on self-hosted runners On self-hosted runners, the tool cache directory persists between runs. When GitHub's cloud cache doesn't have an entry (first run, evicted, etc.), but the local MSI file exists from a previous run, tc.downloadTool() fails with "Destination file path already exists". This fix checks if the existing MSI has a valid checksum before downloading: - If valid: reuse it (skip download) - If invalid: delete and re-download Signed-off-by: Dorin Geman --- dist/index.js | 41 ++++++++++++++++++++++++++++------------- src/main.ts | 43 +++++++++++++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/dist/index.js b/dist/index.js index 5b10392..0a9a0bf 100644 --- a/dist/index.js +++ b/dist/index.js @@ -52873,20 +52873,35 @@ async function installTailscaleWindows(config, toolPath, fromCache = false) { } // Download MSI const downloadUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi`; - core.info(`Downloading ${downloadUrl}`); - const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath); - // Verify checksum - const actualSha = await calculateFileSha256(downloadedMsiPath); const expectedSha = config.sha256Sum.trim().toLowerCase(); - core.info(`Expected sha256: ${expectedSha}`); - core.info(`Actual sha256: ${actualSha}`); - if (actualSha !== expectedSha) { - throw new Error("SHA256 checksum mismatch"); - } - // Keep the MSI file in toolPath for caching (don't delete it) - // The downloadedMsiPath is in temp, but we want to keep it in toolPath - if (downloadedMsiPath !== msiPath) { - fs.copyFileSync(downloadedMsiPath, msiPath); + // Check if MSI already exists with correct checksum (for self-hosted runners) + let needsDownload = true; + if (fs.existsSync(msiPath)) { + const existingSha = await calculateFileSha256(msiPath); + if (existingSha === expectedSha) { + core.info(`Using existing MSI at ${msiPath} (checksum verified)`); + needsDownload = false; + } + else { + core.info(`Existing MSI checksum mismatch, re-downloading`); + fs.unlinkSync(msiPath); + } + } + if (needsDownload) { + core.info(`Downloading ${downloadUrl}`); + const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath); + // Verify checksum + const actualSha = await calculateFileSha256(downloadedMsiPath); + core.info(`Expected sha256: ${expectedSha}`); + core.info(`Actual sha256: ${actualSha}`); + if (actualSha !== expectedSha) { + throw new Error("SHA256 checksum mismatch"); + } + // Keep the MSI file in toolPath for caching (don't delete it) + // The downloadedMsiPath is in temp, but we want to keep it in toolPath + if (downloadedMsiPath !== msiPath) { + fs.copyFileSync(downloadedMsiPath, msiPath); + } } } // Install MSI (same for both fresh and cached) diff --git a/src/main.ts b/src/main.ts index e6018ae..e2bb739 100644 --- a/src/main.ts +++ b/src/main.ts @@ -509,23 +509,38 @@ async function installTailscaleWindows( // Download MSI const downloadUrl = `${baseUrl}/tailscale-setup-${config.resolvedVersion}-${config.arch}.msi`; - core.info(`Downloading ${downloadUrl}`); - - const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath); - - // Verify checksum - const actualSha = await calculateFileSha256(downloadedMsiPath); const expectedSha = config.sha256Sum.trim().toLowerCase(); - core.info(`Expected sha256: ${expectedSha}`); - core.info(`Actual sha256: ${actualSha}`); - if (actualSha !== expectedSha) { - throw new Error("SHA256 checksum mismatch"); + + // Check if MSI already exists with correct checksum (for self-hosted runners) + let needsDownload = true; + if (fs.existsSync(msiPath)) { + const existingSha = await calculateFileSha256(msiPath); + if (existingSha === expectedSha) { + core.info(`Using existing MSI at ${msiPath} (checksum verified)`); + needsDownload = false; + } else { + core.info(`Existing MSI checksum mismatch, re-downloading`); + fs.unlinkSync(msiPath); + } } - // Keep the MSI file in toolPath for caching (don't delete it) - // The downloadedMsiPath is in temp, but we want to keep it in toolPath - if (downloadedMsiPath !== msiPath) { - fs.copyFileSync(downloadedMsiPath, msiPath); + if (needsDownload) { + core.info(`Downloading ${downloadUrl}`); + const downloadedMsiPath = await tc.downloadTool(downloadUrl, msiPath); + + // Verify checksum + const actualSha = await calculateFileSha256(downloadedMsiPath); + core.info(`Expected sha256: ${expectedSha}`); + core.info(`Actual sha256: ${actualSha}`); + if (actualSha !== expectedSha) { + throw new Error("SHA256 checksum mismatch"); + } + + // Keep the MSI file in toolPath for caching (don't delete it) + // The downloadedMsiPath is in temp, but we want to keep it in toolPath + if (downloadedMsiPath !== msiPath) { + fs.copyFileSync(downloadedMsiPath, msiPath); + } } }