197 lines
7.0 KiB
TypeScript
197 lines
7.0 KiB
TypeScript
import * as core from '@actions/core';
|
|
import * as tc from '@actions/tool-cache';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import { spawnSync } from 'child_process';
|
|
import { getPlatformInfo } from './core/platform';
|
|
import { getMatchingAsset } from './core/matcher';
|
|
import { findBinary } from './core/finder';
|
|
import { fetchLatestRelease } from './core/downloader';
|
|
|
|
function installSystemPackage(downloadPath: string): void {
|
|
const fileName = path.basename(downloadPath).toLowerCase();
|
|
|
|
const command: { binary: string; args: string[] } | undefined = 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] }
|
|
: undefined;
|
|
|
|
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 = spawnSync(commandToRun, argsToRun, { stdio: 'inherit' });
|
|
if (result.status !== 0) {
|
|
throw new Error(`Failed to install package using ${commandToRun} ${argsToRun.join(' ')}.`);
|
|
}
|
|
}
|
|
|
|
function findInstalledBinary(binaryName: string): string | undefined {
|
|
const isRegex = binaryName.startsWith('~');
|
|
if (!isRegex) {
|
|
const whichResult = spawnSync('which', [binaryName], { encoding: 'utf8' });
|
|
if (whichResult.status === 0) {
|
|
const resolvedPath = (whichResult.stdout || '').trim();
|
|
if (resolvedPath) {
|
|
return resolvedPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
const candidates = ['/usr/local/bin', '/usr/bin', '/opt/homebrew/bin', '/opt/local/bin'];
|
|
const pattern: string | RegExp = isRegex ? new RegExp(binaryName.substring(1), 'i') : binaryName;
|
|
for (const candidateDir of candidates) {
|
|
if (!fs.existsSync(candidateDir)) {
|
|
continue;
|
|
}
|
|
const candidatePath = findBinary(candidateDir, pattern, false, () => undefined);
|
|
if (candidatePath) {
|
|
return candidatePath;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
async function run() {
|
|
try {
|
|
const repository = core.getInput('repository', { required: true });
|
|
const fileNameInput = core.getInput('file-name');
|
|
const binaryInput = core.getInput('binary-name');
|
|
const fileType = core.getInput('file-type');
|
|
const updateCache = core.getInput('update-cache') || 'false';
|
|
const debug = core.getBooleanInput('debug');
|
|
const token = core.getInput('token') || process.env.GITHUB_TOKEN;
|
|
|
|
const platformInfo = getPlatformInfo();
|
|
const toolName = repository.split('/').pop() || repository;
|
|
|
|
// Rule for update-cache: 'false' means use ANY cached version if available
|
|
if (updateCache === 'false') {
|
|
const allVersions = tc.findAllVersions(toolName, platformInfo.arch);
|
|
if (allVersions.length > 0) {
|
|
const latestVersion = allVersions.sort().pop();
|
|
if (latestVersion) {
|
|
const cachedDir = tc.find(toolName, latestVersion, platformInfo.arch);
|
|
if (cachedDir) {
|
|
core.info(`Found ${toolName} version ${latestVersion} in local cache (update-cache: false)`);
|
|
core.addPath(cachedDir);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
core.info(`Fetching latest release information for ${repository}...`);
|
|
const release = await fetchLatestRelease(repository, token);
|
|
const asset = getMatchingAsset(release.assets, platformInfo, fileNameInput, fileType);
|
|
|
|
core.info(`Selected asset: ${asset.name}`);
|
|
|
|
const version = release.tag_name.replace(/^v/, '');
|
|
const binaryName = binaryInput || toolName;
|
|
|
|
// Check if the tool is already in the cache (if not 'always' update)
|
|
if (updateCache !== 'always') {
|
|
const cachedDir = tc.find(toolName, version, platformInfo.arch);
|
|
if (cachedDir) {
|
|
core.info(`Found ${toolName} version ${version} in cache at ${cachedDir}`);
|
|
core.addPath(cachedDir);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const downloadUrl = asset.browser_download_url;
|
|
core.info(`Downloading ${asset.name} from ${downloadUrl}...`);
|
|
|
|
const downloadPath = await tc.downloadTool(downloadUrl, undefined, token ? `token ${token}` : undefined);
|
|
|
|
const nameLower = asset.name.toLowerCase();
|
|
let toolDir: string;
|
|
|
|
if (/\.(deb|pkg|rpm)$/i.test(nameLower)) {
|
|
core.info(`Installing package asset ${asset.name}...`);
|
|
installSystemPackage(downloadPath);
|
|
|
|
const binaryPath = findInstalledBinary(binaryName);
|
|
if (!binaryPath) {
|
|
throw new Error(`Package installed, but binary "${binaryName}" could not be located in common executable paths.`);
|
|
}
|
|
|
|
const binaryDir = path.dirname(binaryPath);
|
|
core.addPath(binaryDir);
|
|
core.info(`Binary found at ${binaryPath}. Added ${binaryDir} to PATH.`);
|
|
return;
|
|
}
|
|
|
|
// Determine extraction method based on extension
|
|
if (/\.(tar\.gz|tar|tgz)$/i.test(nameLower)) {
|
|
toolDir = await tc.extractTar(downloadPath);
|
|
} else if (/\.zip$/i.test(nameLower)) {
|
|
toolDir = await tc.extractZip(downloadPath);
|
|
} else if (/\.7z$/i.test(nameLower)) {
|
|
toolDir = await tc.extract7z(downloadPath);
|
|
} else if (/\.(xar|pkg)$/i.test(nameLower)) {
|
|
toolDir = await tc.extractXar(downloadPath);
|
|
} else {
|
|
// Treat as a direct binary or non-extractable file
|
|
toolDir = path.join(path.dirname(downloadPath), 'bin');
|
|
const destPath = path.join(toolDir, asset.name);
|
|
|
|
if (!fs.existsSync(toolDir)) {
|
|
fs.mkdirSync(toolDir, { recursive: true });
|
|
}
|
|
fs.renameSync(downloadPath, destPath);
|
|
|
|
if (process.platform !== 'win32') {
|
|
fs.chmodSync(destPath, '755');
|
|
}
|
|
}
|
|
|
|
// Find the binary within the extracted/prepared directory
|
|
let binaryPattern: string | RegExp;
|
|
if (binaryName.startsWith('~')) {
|
|
binaryPattern = new RegExp(binaryName.substring(1), 'i');
|
|
core.info(`Searching for binary matching regex: ${binaryName.substring(1)}`);
|
|
} else {
|
|
binaryPattern = binaryName;
|
|
core.info(`Searching for binary named: ${binaryName}`);
|
|
}
|
|
|
|
const binaryPath = findBinary(toolDir, binaryPattern, debug, (msg) => core.info(msg));
|
|
if (!binaryPath) {
|
|
throw new Error(`Could not find binary "${binaryName}" in the extracted asset.`);
|
|
}
|
|
|
|
// The tool directory is the one containing the binary
|
|
toolDir = path.dirname(binaryPath);
|
|
core.info(`Binary found at ${binaryPath}.`);
|
|
|
|
// Make binary executable just in case it's not
|
|
if (process.platform !== 'win32') {
|
|
fs.chmodSync(binaryPath, '755');
|
|
}
|
|
|
|
// Cache the tool
|
|
const finalCachedDir = await tc.cacheDir(toolDir, toolName, version, platformInfo.arch);
|
|
core.info(`Cached ${toolName} version ${version} to ${finalCachedDir}`);
|
|
|
|
core.addPath(finalCachedDir);
|
|
core.info(`Added ${finalCachedDir} to PATH`);
|
|
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
core.setFailed(error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
run();
|