#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/cli.ts var path3 = __toESM(require("path")); var fs3 = __toESM(require("fs")); var os2 = __toESM(require("os")); var import_child_process2 = require("child_process"); // src/core/platform.ts var os = __toESM(require("os")); var systemPatterns = { linux: "linux", darwin: "(darwin|macos|mac|osx)", win32: "(windows|win)" }; var archPatterns = { x64: "(x86_64|x64|amd64)", arm64: "(aarch64|arm64)" }; function getPlatformInfo(overrides) { const system = (overrides?.system || os.platform()).toLowerCase(); const arch2 = (overrides?.arch || os.arch()).toLowerCase(); return { system, arch: arch2, systemPattern: systemPatterns[system] || system, archPattern: archPatterns[arch2] || arch2 }; } // src/core/matcher.ts function normalizeCustomExtensionPattern(fileType) { let pattern = fileType; if (!pattern.endsWith("$")) { pattern += "$"; } if (!pattern.startsWith("\\.")) { pattern = `\\.${pattern}`; } return pattern; } function getExtPattern(fileType, system) { const normalizedType = (fileType || "").toLowerCase(); if (!normalizedType) { if (system === "linux") { return "\\.(deb|rpm|zip|tar\\.gz|tgz)$"; } if (system === "darwin" || system === "macos" || system === "mac" || system === "osx") { return "\\.(pkg|zip|tar\\.gz|tgz)$"; } return "\\.(zip|tar\\.gz|tgz)$"; } if (normalizedType === "archive") { return "\\.(zip|tar\\.gz|tgz)$"; } if (normalizedType === "package") { return "\\.(deb|pkg|rpm)$"; } const shorthandTypePatterns = { zip: "\\.(zip)$", gzip: "\\.(tar\\.gz|tgz)$", gz: "\\.(tar\\.gz|tgz)$", tar: "\\.(tar)$", "tar.gz": "\\.(tar\\.gz)$", tgz: "\\.(tgz)$", deb: "\\.(deb)$", pkg: "\\.(pkg)$", rpm: "\\.(rpm)$" }; if (shorthandTypePatterns[normalizedType]) { return shorthandTypePatterns[normalizedType]; } return normalizeCustomExtensionPattern(fileType || ""); } function getMatchingAsset(assets, platform2, options) { const { fileName, fileType } = options; const extPattern = getExtPattern(fileType, platform2.system); if (!fileName) { const pattern = `${platform2.systemPattern}[_-]${platform2.archPattern}.*${extPattern}`; const regex = new RegExp(pattern, "i"); const matchingAssets = assets.filter((a) => regex.test(a.name)); if (matchingAssets.length === 0) { throw new Error(`No assets matched the default criteria: ${pattern}`); } if (matchingAssets.length > 1) { throw new Error(`Multiple assets matched the default criteria: ${matchingAssets.map((a) => a.name).join(", ")}`); } return matchingAssets[0]; } else if (fileName.startsWith("~")) { let pattern = fileName.substring(1); const hasSystem = pattern.includes("{{SYSTEM}}"); const hasArch = pattern.includes("{{ARCH}}"); const hasExt = pattern.includes("{{EXT_PATTERN}}"); const hasEnd = pattern.endsWith("$"); if (!hasSystem && !hasArch && !hasExt && !hasEnd) { pattern += `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`; } else if (hasSystem && hasArch && !hasExt && !hasEnd) { pattern += `.*{{EXT_PATTERN}}$`; } const finalPattern = pattern.replace(/{{SYSTEM}}/g, platform2.systemPattern).replace(/{{ARCH}}/g, platform2.archPattern).replace(/{{EXT_PATTERN}}/g, extPattern); const regex = new RegExp(finalPattern, "i"); const matchingAssets = assets.filter((a) => regex.test(a.name)); if (matchingAssets.length === 0) { throw new Error(`No assets matched the regex: ${finalPattern}`); } if (matchingAssets.length > 1) { throw new Error(`Multiple assets matched the criteria: ${matchingAssets.map((a) => a.name).join(", ")}`); } return matchingAssets[0]; } else { const asset = assets.find((a) => a.name === fileName); if (!asset) { throw new Error(`No asset found matching the exact name: ${fileName}`); } return asset; } } // src/core/finder.ts var fs = __toESM(require("fs")); var path = __toESM(require("path")); function findBinary(dir, pattern, debug, logger) { const items = fs.readdirSync(dir); if (debug) { logger(`Searching for binary in ${dir}...`); items.forEach((item) => logger(` - ${item}`)); } for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const found = findBinary(fullPath, pattern, debug, logger); if (found) return found; } else { let isMatch = false; if (pattern instanceof RegExp) { isMatch = pattern.test(item); } else { isMatch = item === pattern; if (!isMatch && process.platform === "win32" && !pattern.toLowerCase().endsWith(".exe")) { isMatch = item.toLowerCase() === `${pattern.toLowerCase()}.exe`; } } if (isMatch) return fullPath; } } return void 0; } // src/core/downloader.ts function getGithubApiHeaders(token) { const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "setup-github-release-action" }; if (token) { headers["Authorization"] = `token ${token}`; } return headers; } async function fetchLatestRelease(repository, token) { const url = `https://api.github.com/repos/${repository}/releases/latest`; const headers = getGithubApiHeaders(token); const response = await fetch(url, { headers }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`Failed to fetch latest release for ${repository}: ${response.statusText}. ${errorBody}`); } return await response.json(); } async function fetchLatestReleaseRaw(repository, token) { const url = `https://api.github.com/repos/${repository}/releases/latest`; const headers = getGithubApiHeaders(token); const response = await fetch(url, { headers }); const body = await response.text(); if (!response.ok) { throw new Error(`Failed to fetch latest release for ${repository}: ${response.statusText}. ${body}`); } return body; } async function downloadAsset(url, destPath, token) { const headers = { "User-Agent": "setup-github-release-action" }; if (token) { headers["Authorization"] = `token ${token}`; } const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`Failed to download asset: ${response.statusText}`); } const fs4 = await import("fs"); const { Readable } = await import("stream"); const { finished } = await import("stream/promises"); const fileStream = fs4.createWriteStream(destPath); await finished(Readable.fromWeb(response.body).pipe(fileStream)); } // src/core/extractor.ts var import_child_process = require("child_process"); var path2 = __toESM(require("path")); var fs2 = __toESM(require("fs")); async function extractAsset(filePath, destDir) { const ext = path2.extname(filePath).toLowerCase(); const name = path2.basename(filePath).toLowerCase(); if (!fs2.existsSync(destDir)) { fs2.mkdirSync(destDir, { recursive: true }); } if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) { const args = ["-xf", filePath, "-C", destDir]; const result = (0, import_child_process.spawnSync)("tar", args); if (result.status !== 0) { throw new Error(`tar failed with status ${result.status}: ${result.stderr.toString()}`); } } else if (name.endsWith(".zip")) { if (process.platform === "win32") { const tarResult = (0, import_child_process.spawnSync)("tar", ["-xf", filePath, "-C", destDir]); if (tarResult.status === 0) return; const escapedFilePath = filePath.replace(/'/g, "''"); const escapedDestDir = destDir.replace(/'/g, "''"); const dotNetCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFilePath}', '${escapedDestDir}')`; for (const shell of ["pwsh", "powershell"]) { const result = (0, import_child_process.spawnSync)(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", dotNetCommand]); if (result.status === 0) return; } throw new Error(`Extraction failed: Both tar and PowerShell fallback failed. Make sure your system can extract ZIP files.`); } else { const result = (0, import_child_process.spawnSync)("unzip", ["-q", filePath, "-d", destDir]); if (result.status !== 0) { throw new Error(`unzip failed with status ${result.status}: ${result.stderr.toString()}`); } } } else if (name.endsWith(".7z")) { const result = (0, import_child_process.spawnSync)("7z", ["x", filePath, `-o${destDir}`, "-y"]); if (result.status !== 0) { throw new Error(`7z failed with status ${result.status}. Make sure 7z is installed.`); } } else { const destPath = path2.join(destDir, path2.basename(filePath)); fs2.copyFileSync(filePath, destPath); } } // src/cli.ts function usage() { return ` Usage: install-github-release [options] Arguments: repository The GitHub repository (owner/repo) Options: --dry-run [level] Run in test mode (default level: 1) -l, --list [repository] List available assets from latest release and exit -a, --app-name Application name (optional, for output messages) -f, --file-name Asset file name or regex pattern (prefixed with ~) -b, --binary-name Binary name (supports source:destination form) -t, --file-type archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm -p, --install-path Custom installation directory -o, --output-directory Only download selected asset to the specified directory -j, --releases-json Download latest release JSON only --system Override detected system for asset matching --arch Override detected architecture for asset matching -k, --token GitHub token -d, --debug Enable debug logging -h, --help Show this help message `; } function ensureOptionValue(argv, index, option) { const value = argv[index + 1]; if (!value || value.startsWith("-")) { throw new Error(`Missing value for ${option}.`); } return value; } function parseCliArgs(argv) { const envDryRun = process.env.TEST_MODE; const dryRunLevelFromEnv = envDryRun && /^\d+$/.test(envDryRun) ? parseInt(envDryRun, 10) : 0; const opts = { releasesJsonOnly: false, listOnly: false, debug: false, help: false, dryRunLevel: dryRunLevelFromEnv, positionals: [] }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; switch (arg) { case "-h": case "--help": opts.help = true; break; case "--dry-run": { const next = argv[i + 1]; if (next && /^\d+$/.test(next)) { opts.dryRunLevel = parseInt(next, 10); i++; } else { opts.dryRunLevel = 1; } break; } case "-l": case "--list": { opts.listOnly = true; const next = argv[i + 1]; if (next && !next.startsWith("-")) { opts.listRepo = next; i++; } break; } case "-a": case "--app-name": opts.appName = ensureOptionValue(argv, i, arg); i++; break; case "-f": case "--file-name": opts.fileName = ensureOptionValue(argv, i, arg); i++; break; case "-b": case "--binary-name": opts.binaryName = ensureOptionValue(argv, i, arg); i++; break; case "-t": case "--file-type": { const fileType = ensureOptionValue(argv, i, arg).toLowerCase(); const knownType = /^(archive|package|zip|gzip|gz|tar|tar\.gz|tgz|deb|pkg|rpm)$/i; if (!knownType.test(fileType)) { throw new Error(`Unknown asset type: ${fileType}`); } opts.fileType = fileType; i++; break; } case "-p": case "--install-path": opts.installPath = ensureOptionValue(argv, i, arg); i++; break; case "-o": case "--output-directory": opts.outputDirectory = ensureOptionValue(argv, i, arg); i++; break; case "-j": case "--releases-json": opts.releasesJsonOnly = true; break; case "--system": opts.systemOverride = ensureOptionValue(argv, i, arg); i++; break; case "--arch": opts.archOverride = ensureOptionValue(argv, i, arg); i++; break; case "-k": case "--token": opts.token = ensureOptionValue(argv, i, arg); i++; break; case "-d": case "--debug": opts.debug = true; break; default: if (arg.startsWith("-")) { throw new Error(`Unknown option: ${arg}`); } opts.positionals.push(arg); break; } } return opts; } function validateOutputDirectory(outputDirectory) { const resolvedPath = path3.resolve(outputDirectory); if (!fs3.existsSync(resolvedPath) || !fs3.statSync(resolvedPath).isDirectory()) { throw new Error(`Output directory "${resolvedPath}" does not exist.`); } return resolvedPath; } function getInstallDir(installPath) { if (installPath) { return path3.resolve(installPath); } if (process.platform === "win32") { const localAppData = process.env.LOCALAPPDATA || path3.join(os2.homedir(), "AppData", "Local"); return path3.join(localAppData, "bin"); } const isRoot = process.getuid && process.getuid() === 0; if (isRoot) { return "/usr/local/bin"; } const homeBin = path3.join(os2.homedir(), "bin"); if (fs3.existsSync(homeBin)) { return homeBin; } return "/usr/local/bin"; } function installSystemPackage(downloadPath) { const fileName = path3.basename(downloadPath).toLowerCase(); const command = fileName.endsWith(".deb") ? { binary: "dpkg", args: ["-i", downloadPath] } : fileName.endsWith(".pkg") ? { binary: "installer", args: ["-pkg", downloadPath, "-target", "/"] } : fileName.endsWith(".rpm") ? { binary: "rpm", args: ["-i", downloadPath] } : void 0; if (!command) { throw new Error(`Unsupported package type: ${fileName}`); } const isRoot = process.getuid && process.getuid() === 0; const commandToRun = isRoot ? command.binary : "sudo"; const argsToRun = isRoot ? command.args : [command.binary, ...command.args]; const result = (0, import_child_process2.spawnSync)(commandToRun, argsToRun, { stdio: "inherit" }); if (result.status !== 0) { throw new Error(`Failed to install package using ${commandToRun} ${argsToRun.join(" ")}.`); } } async function run() { let tempDir; try { const options = parseCliArgs(process.argv.slice(2)); const repository = options.listRepo || options.positionals[0]; if (options.help || !repository) { console.log(usage()); process.exit(options.help ? 0 : 1); } const token = options.token || process.env.GITHUB_TOKEN; if (options.listOnly) { const release2 = await fetchLatestRelease(repository, token); release2.assets.forEach((asset2) => console.log(`- ${asset2.browser_download_url}`)); process.exit(0); } const toolName = repository.split("/").pop() || repository; const appName = options.appName || toolName.charAt(0).toUpperCase() + toolName.slice(1); const binaryOption = options.binaryName || toolName; const [binarySource, binaryDestination] = binaryOption.includes(":") ? [binaryOption.split(":")[0], binaryOption.split(":")[1]] : [binaryOption, binaryOption]; if (options.releasesJsonOnly) { const rawRelease = await fetchLatestReleaseRaw(repository, token); const outputBase = binaryDestination || toolName; const outputName = `${outputBase}.releases.json`; const outputPath = options.outputDirectory ? path3.join(validateOutputDirectory(options.outputDirectory), outputName) : outputName; fs3.writeFileSync(outputPath, rawRelease, "utf8"); console.log(`Downloaded GitHub releases to ${outputPath}.`); process.exit(0); } const platformInfo = getPlatformInfo({ system: options.systemOverride, arch: options.archOverride }); console.log(`Fetching latest release for ${repository}...`); const release = await fetchLatestRelease(repository, token); const asset = getMatchingAsset(release.assets, platformInfo, { fileName: options.fileName, fileType: options.fileType }); const version = release.tag_name.replace(/^v/i, ""); const downloadUrl = asset.browser_download_url; console.log(`Will download '${appName}' version: ${version}`); console.log(`Download URL: "${downloadUrl}".`); if (options.dryRunLevel > 0) { process.exit(0); } if (options.outputDirectory) { const outputDir = validateOutputDirectory(options.outputDirectory); const outputPath = path3.join(outputDir, path3.basename(downloadUrl)); console.log(`Downloading '${appName}' version ${version} to '${outputPath}'...`); await downloadAsset(downloadUrl, outputPath, token); process.exit(0); } tempDir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "setup-gh-release-")); const downloadPath = path3.join(tempDir, asset.name); await downloadAsset(downloadUrl, downloadPath, token); if (/\.(deb|pkg|rpm)$/i.test(asset.name)) { installSystemPackage(downloadPath); console.log("Installation successful!"); process.exit(0); } const extractDir = path3.join(tempDir, "extract"); console.log(`Extracting ${asset.name}...`); await extractAsset(downloadPath, extractDir); let binaryPattern; if (binarySource.startsWith("~")) { binaryPattern = new RegExp(binarySource.substring(1), "i"); } else { binaryPattern = binarySource; } const binaryPath = findBinary(extractDir, binaryPattern, options.debug, console.log); if (!binaryPath) { throw new Error(`Could not find binary "${binarySource}" in the extracted asset.`); } const installDir = getInstallDir(options.installPath); if (!fs3.existsSync(installDir)) { fs3.mkdirSync(installDir, { recursive: true }); } const finalName = binaryDestination || path3.basename(binaryPath); const destPath = path3.join(installDir, finalName); console.log(`Installing ${finalName} to ${destPath}...`); try { fs3.copyFileSync(binaryPath, destPath); } catch (err) { if (err.code === "EBUSY") { throw new Error(`The file ${destPath} is currently in use. Please close any running instances and try again.`); } if (err.code === "EACCES" || err.code === "EPERM") { throw new Error(`Permission denied while installing to ${destPath}. Try running with sudo or as administrator, or use -p to specify a custom path.`); } throw err; } if (process.platform !== "win32") { fs3.chmodSync(destPath, "755"); } console.log("Installation successful!"); process.exit(0); } catch (error) { if (error?.message) { console.error(`Error: ${error.message}`); } else { console.error("Error: Unknown failure."); } process.exit(1); } finally { if (tempDir && fs3.existsSync(tempDir)) { fs3.rmSync(tempDir, { recursive: true, force: true }); } } } run();