diff --git a/README.md b/README.md index 540ea41..3732a4f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ Can be an exact version (e.g., `3.4.2`) or a version range (e.g., `3.x`). **Default**: [`GITHUB_TOKEN`](https://docs.github.com/actions/security-guides/automatic-token-authentication) +### `checksum` + +Expected SHA256 checksum of the downloaded Task archive. + ## Usage To get the action's default version of Task just add this step: diff --git a/action.yml b/action.yml index e2adae3..b9c26c0 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: description: "Maximum number of retry attempts for HTTP requests" required: false default: "3" + checksum: + description: "Expected SHA256 checksum of the downloaded Task archive" + required: false runs: using: "node24" diff --git a/dist/index.js b/dist/index.js index d04273d..25acf30 100644 --- a/dist/index.js +++ b/dist/index.js @@ -33714,6 +33714,10 @@ const external_node_os_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import const external_node_path_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:path"); // EXTERNAL MODULE: external "node:util" var external_node_util_ = __nccwpck_require__(7975); +// EXTERNAL MODULE: external "node:crypto" +var external_node_crypto_ = __nccwpck_require__(7598); +;// CONCATENATED MODULE: external "node:fs" +const external_node_fs_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:fs"); // EXTERNAL MODULE: ./node_modules/semver/index.js var node_modules_semver = __nccwpck_require__(2088); ;// CONCATENATED MODULE: ./node_modules/@actions/tool-cache/lib/manifest.js @@ -34534,6 +34538,8 @@ function _unique(values) { + + const osPlat = (0,external_node_os_namespaceObject.platform)(); const osArch = (0,external_node_os_namespaceObject.arch)(); // Retrieve a list of versions scraping tags from the Github API @@ -34623,11 +34629,26 @@ function getFileName() { const filename = (0,external_node_util_.format)("task_%s_%s.%s", taskPlatform, taskArch, ext); return filename; } -async function downloadRelease(version) { +function verifyChecksum(path, expectedChecksum) { + if (!expectedChecksum) { + return; + } + const normalizedExpected = expectedChecksum.trim().toLowerCase(); + if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) { + throw new Error("The checksum input must be a SHA256 hex digest."); + } + const actualChecksum = (0,external_node_crypto_.createHash)("sha256") + .update((0,external_node_fs_namespaceObject.readFileSync)(path)) + .digest("hex"); + if (actualChecksum !== normalizedExpected) { + throw new Error(`Downloaded Task archive checksum mismatch. Expected ${normalizedExpected}, got ${actualChecksum}.`); + } +} +async function downloadRelease(version, checksum) { // Download const fileName = getFileName(); const downloadUrl = (0,external_node_util_.format)("https://github.com/go-task/task/releases/download/%s/%s", version, fileName); - let downloadPath = null; + let downloadPath = ""; try { downloadPath = await downloadTool(downloadUrl); } @@ -34637,8 +34658,9 @@ async function downloadRelease(version) { } throw new Error(`Failed to download version ${version}: ${error}`); } + verifyChecksum(downloadPath, checksum); // Extract - let extPath = null; + let extPath = ""; if (osPlat === "win32") { extPath = await extractZip(downloadPath); // Create a bin/ folder and move `task` there @@ -34654,7 +34676,7 @@ async function downloadRelease(version) { // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded return cacheDir(extPath, "task", version); } -async function getTask(version, repoToken, maxRetries = 3) { +async function getTask(version, repoToken, maxRetries = 3, checksum) { // resolve the version number const targetVersion = await computeVersion(version, repoToken, maxRetries); // look if the binary is cached @@ -34662,7 +34684,7 @@ async function getTask(version, repoToken, maxRetries = 3) { toolPath = find("task", targetVersion); // if not: download, extract and cache if (!toolPath) { - toolPath = await downloadRelease(targetVersion); + toolPath = await downloadRelease(targetVersion, checksum); core_debug(`Task cached under ${toolPath}`); } toolPath = (0,external_node_path_namespaceObject.join)(toolPath, "bin"); @@ -34689,7 +34711,8 @@ async function run() { const version = getInput("version", { required: true }); const repoToken = getInput("repo-token"); const maxRetries = parseInt(getInput("max-retries") || "3", 10); - await getTask(version, repoToken, maxRetries); + const checksum = getInput("checksum"); + await getTask(version, repoToken, maxRetries, checksum || undefined); } catch (error) { if (error instanceof Error) { diff --git a/src/installer.ts b/src/installer.ts index a2ce4f8..143c1c8 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -13,6 +13,8 @@ import { arch, platform } from "node:os"; import { join } from "node:path"; import { format } from "node:util"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; import { HttpClient } from "@actions/http-client"; import { rcompare, valid } from "semver"; import { addPath, debug, info } from "@actions/core"; @@ -142,7 +144,30 @@ function getFileName() { return filename; } -async function downloadRelease(version: string): Promise { +function verifyChecksum(path: string, expectedChecksum?: string): void { + if (!expectedChecksum) { + return; + } + + const normalizedExpected = expectedChecksum.trim().toLowerCase(); + if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) { + throw new Error("The checksum input must be a SHA256 hex digest."); + } + + const actualChecksum = createHash("sha256") + .update(readFileSync(path)) + .digest("hex"); + if (actualChecksum !== normalizedExpected) { + throw new Error( + `Downloaded Task archive checksum mismatch. Expected ${normalizedExpected}, got ${actualChecksum}.`, + ); + } +} + +async function downloadRelease( + version: string, + checksum?: string, +): Promise { // Download const fileName: string = getFileName(); const downloadUrl: string = format( @@ -150,7 +175,7 @@ async function downloadRelease(version: string): Promise { version, fileName, ); - let downloadPath: string | null = null; + let downloadPath = ""; try { downloadPath = await downloadTool(downloadUrl); } catch (error) { @@ -159,9 +184,10 @@ async function downloadRelease(version: string): Promise { } throw new Error(`Failed to download version ${version}: ${error}`); } + verifyChecksum(downloadPath, checksum); // Extract - let extPath: string | null = null; + let extPath = ""; if (osPlat === "win32") { extPath = await extractZip(downloadPath); // Create a bin/ folder and move `task` there @@ -178,7 +204,12 @@ async function downloadRelease(version: string): Promise { return cacheDir(extPath, "task", version); } -export async function getTask(version: string, repoToken: string, maxRetries: number = 3) { +export async function getTask( + version: string, + repoToken: string, + maxRetries: number = 3, + checksum?: string, +) { // resolve the version number const targetVersion = await computeVersion(version, repoToken, maxRetries); @@ -188,7 +219,7 @@ export async function getTask(version: string, repoToken: string, maxRetries: nu // if not: download, extract and cache if (!toolPath) { - toolPath = await downloadRelease(targetVersion); + toolPath = await downloadRelease(targetVersion, checksum); debug(`Task cached under ${toolPath}`); } diff --git a/src/main.ts b/src/main.ts index 787daa3..63da1c4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,8 +18,9 @@ async function run() { const version = getInput("version", { required: true }); const repoToken = getInput("repo-token"); const maxRetries = parseInt(getInput("max-retries") || "3", 10); + const checksum = getInput("checksum"); - await getTask(version, repoToken, maxRetries); + await getTask(version, repoToken, maxRetries, checksum || undefined); } catch (error) { if (error instanceof Error) { setFailed(error.message);