Skip to content

Commit 47386d7

Browse files
committed
fix(install): verify the archive integrity
1 parent 390061a commit 47386d7

File tree

1 file changed

+128
-27
lines changed

1 file changed

+128
-27
lines changed

src/install.ts

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
1+
// Node.js Built-in Modules
2+
import * as crypto from "node:crypto";
3+
import * as fs from "node:fs";
4+
import * as fsPromises from "node:fs/promises";
15
import * as os from "node:os";
26
import * as path from "node:path";
3-
import * as fs from "node:fs/promises";
47
import process from "node:process";
8+
import { pipeline } from "node:stream/promises";
9+
10+
// External Packages
511
import core from "@actions/core";
12+
import exec from "@actions/exec";
613
import tc from "@actions/tool-cache";
14+
15+
// Project-Specific Types
716
import type { Version } from "./version.ts";
817

918
export async function install(version: Version) {
19+
const HOSTS = {
20+
docs: "docs.deno.com",
21+
dl: "dl.deno.land",
22+
github: "github.com",
23+
} as const;
24+
25+
const docLink =
26+
`https://${HOSTS.docs}/runtime/manual/getting_started/installation`;
27+
1028
const cachedPath = tc.find(
1129
"deno",
1230
version.kind === "canary" ? `0.0.0-${version.version}` : version.version,
@@ -17,47 +35,130 @@ export async function install(version: Version) {
1735
return;
1836
}
1937

38+
const getUrl = (file: string, ext = "") => {
39+
const filename = file + ext;
40+
const suffix = `${version.version}/${filename}`;
41+
42+
switch (version.kind) {
43+
case "canary":
44+
return `https://${HOSTS.dl}/canary/${suffix}`;
45+
case "rc":
46+
return `https://${HOSTS.dl}/release/v${suffix}`;
47+
default:
48+
return `https://${HOSTS.github}/denoland/deno/releases/download/v${suffix}`;
49+
}
50+
};
51+
2052
const zip = zipName();
21-
let url;
53+
core.info(`Downloading Deno from ${getUrl(zip)}.`);
54+
const zipPath = await tc.downloadTool(getUrl(zip));
2255

23-
switch (version.kind) {
24-
case "canary":
25-
url = `https://dl.deno.land/canary/${version.version}/${zip}`;
26-
break;
27-
case "rc":
28-
url = `https://dl.deno.land/release/v${version.version}/${zip}`;
29-
break;
30-
case "stable":
31-
case "lts":
32-
url =
33-
`https://github.com/denoland/deno/releases/download/v${version.version}/${zip}`;
34-
break;
35-
}
56+
try {
57+
const shaPath = await tc.downloadTool(getUrl(zip, ".sha256sum"));
58+
const shaContent = await fsPromises.readFile(shaPath, "utf8");
59+
60+
if (!shaContent.includes(zip)) {
61+
core.warning(
62+
`The .sha256sum file does not explicitly mention the remote filename: '${zip}'.`,
63+
);
64+
}
3665

37-
core.info(`Downloading Deno from ${url}.`);
66+
const match = shaContent.match(/[A-Fa-f0-9]{64}/);
67+
if (!match) throw new Error("FORMAT_ERROR");
68+
const expectedHash = match[0].toLowerCase();
69+
70+
const hash = crypto.createHash("sha256");
71+
await pipeline(fs.createReadStream(zipPath), hash);
72+
const actualHash = hash.digest("hex");
73+
74+
if (actualHash !== expectedHash) {
75+
await fsPromises.unlink(zipPath);
76+
core.setFailed(
77+
`Integrity mismatch! Expected ${expectedHash}, got ${actualHash}.`,
78+
);
79+
return;
80+
}
81+
core.info("Checksum verified successfully.");
82+
} catch (err: unknown) {
83+
const message = err instanceof Error ? err.message : String(err);
84+
if (message.includes("404")) {
85+
core.warning(
86+
"No .sha256sum found. Continuing without integrity verification.",
87+
);
88+
} else if (message === "FORMAT_ERROR") {
89+
core.warning(".sha256sum found but no valid hash detected. Continuing.");
90+
} else {
91+
core.warning(`Verification skipped: ${message}`);
92+
}
93+
}
3894

39-
const zipPath = await tc.downloadTool(url);
4095
const extractedFolder = await tc.extractZip(zipPath);
96+
const binaryName = core.getInput("deno-binary-name") || "deno";
97+
const exeSuffix = process.platform === "win32" ? ".exe" : "";
98+
let binaryPath = path.join(extractedFolder, `deno${exeSuffix}`);
4199

42-
const binaryName = core.getInput("deno-binary-name");
43100
if (binaryName !== "deno") {
44-
await fs.rename(
45-
path.join(
46-
extractedFolder,
47-
process.platform === "win32" ? "deno.exe" : "deno",
48-
),
49-
path.join(
50-
extractedFolder,
51-
process.platform === "win32" ? binaryName + ".exe" : binaryName,
52-
),
101+
const newPath = path.join(extractedFolder, `${binaryName}${exeSuffix}`);
102+
await fsPromises.rename(binaryPath, newPath);
103+
binaryPath = newPath;
104+
}
105+
106+
try {
107+
core.info("Verifying Deno binary functional integrity...");
108+
let stdout = "";
109+
let stderr = "";
110+
111+
await exec.exec(binaryPath, ["--version"], {
112+
silent: true,
113+
delay: (1000) * 10, // seconds
114+
listeners: {
115+
stdout: (data) => {
116+
stdout += data.toString();
117+
},
118+
stderr: (data) => {
119+
stderr += data.toString();
120+
},
121+
},
122+
});
123+
124+
const expectedArch = process.arch === "x64" ? "x86_64" : "aarch64";
125+
if (!stdout.includes(expectedArch)) {
126+
throw new Error(
127+
`Arch mismatch! Runner is ${process.arch}, Deno reported: ${stdout.trim()}`,
128+
);
129+
}
130+
} catch (err) {
131+
const errorMsg = stderr.trim() ||
132+
(err instanceof Error ? err.message : String(err));
133+
const missingLibs = [...errorMsg.matchAll(/lib[\w\d\.]+\.so\.\d+/g)].map(
134+
(m) => m[0],
53135
);
136+
137+
if (errorMsg.includes("GLIBC_") || errorMsg.includes("GLIBCXX_")) {
138+
core.setFailed(
139+
`Deno requires a newer version of glibc/libstdc++ than this runner provides. See: ${docLink}`,
140+
);
141+
} else if (missingLibs.length > 0) {
142+
const libs = [...new Set(missingLibs)].join(", ");
143+
core.setFailed(
144+
`Deno failed to start due to missing shared libraries: ${libs}. See: ${docLink}`,
145+
);
146+
} else if (errorMsg.includes("Permission denied")) {
147+
core.setFailed(
148+
`Execute permission denied. The distribution archive may be missing the executable bit.`,
149+
);
150+
} else {
151+
core.setFailed(`Binary verification failed: ${errorMsg}`);
152+
}
153+
return;
54154
}
55155

56156
const newCachedPath = await tc.cacheDir(
57157
extractedFolder,
58158
binaryName,
59159
version.kind === "canary" ? `0.0.0-${version.version}` : version.version,
60160
);
161+
61162
core.info(`Cached Deno to ${newCachedPath}.`);
62163
core.addPath(newCachedPath);
63164
const denoInstallRoot = process.env.DENO_INSTALL_ROOT ||

0 commit comments

Comments
 (0)