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" ;
15import * as os from "node:os" ;
26import * as path from "node:path" ;
3- import * as fs from "node:fs/promises" ;
47import process from "node:process" ;
8+ import { pipeline } from "node:stream/promises" ;
9+
10+ // External Packages
511import core from "@actions/core" ;
12+ import exec from "@actions/exec" ;
613import tc from "@actions/tool-cache" ;
14+
15+ // Project-Specific Types
716import type { Version } from "./version.ts" ;
817
918export 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 - F a - f 0 - 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 ( / l i b [ \w \d \. ] + \. s o \. \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