Rearranged the code and added CLI that installs tools locally.
All checks were successful
Test Action / test (push) Successful in 3s
All checks were successful
Test Action / test (push) Successful in 3s
This commit is contained in:
52
src/core/downloader.ts
Normal file
52
src/core/downloader.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { getMatchingAsset } from './matcher';
|
||||
import { PlatformInfo } from './platform';
|
||||
|
||||
export interface ReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}
|
||||
|
||||
export interface ReleaseInfo {
|
||||
tag_name: string;
|
||||
assets: ReleaseAsset[];
|
||||
}
|
||||
|
||||
export async function fetchLatestRelease(repository: string, token?: string): Promise<ReleaseInfo> {
|
||||
const url = `https://api.github.com/repos/${repository}/releases/latest`;
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'setup-github-release-action'
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `token ${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() as ReleaseInfo;
|
||||
}
|
||||
|
||||
export async function downloadAsset(url: string, destPath: string, token?: string): Promise<void> {
|
||||
const headers: Record<string, string> = {
|
||||
'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 fs = await import('fs');
|
||||
const { Readable } = await import('stream');
|
||||
const { finished } = await import('stream/promises');
|
||||
|
||||
const fileStream = fs.createWriteStream(destPath);
|
||||
await finished(Readable.fromWeb(response.body as any).pipe(fileStream));
|
||||
}
|
||||
42
src/core/extractor.ts
Normal file
42
src/core/extractor.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export async function extractAsset(filePath: string, destDir: string): Promise<void> {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const name = path.basename(filePath).toLowerCase();
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (name.endsWith('.tar.gz') || name.endsWith('.tgz') || name.endsWith('.tar')) {
|
||||
const args = ['-xf', filePath, '-C', destDir];
|
||||
const result = 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 command = `Expand-Archive -Path "${filePath}" -DestinationPath "${destDir}" -Force`;
|
||||
const result = spawnSync('powershell', ['-Command', command]);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`powershell Expand-Archive failed with status ${result.status}: ${result.stderr.toString()}`);
|
||||
}
|
||||
} else {
|
||||
const result = 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 = 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 {
|
||||
// For other files, we just copy them to the destination directory
|
||||
const destPath = path.join(destDir, path.basename(filePath));
|
||||
fs.copyFileSync(filePath, destPath);
|
||||
}
|
||||
}
|
||||
38
src/core/finder.ts
Normal file
38
src/core/finder.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function findBinary(dir: string, pattern: string | RegExp, debug: boolean, logger: (msg: string) => void): string | undefined {
|
||||
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;
|
||||
// On Windows, also check for .exe extension if the pattern doesn't have it
|
||||
if (!isMatch && process.platform === 'win32' && !pattern.toLowerCase().endsWith('.exe')) {
|
||||
isMatch = item.toLowerCase() === `${pattern.toLowerCase()}.exe`;
|
||||
}
|
||||
}
|
||||
if (isMatch) return fullPath;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setExecutable(filePath: string): void {
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(filePath, '755');
|
||||
}
|
||||
}
|
||||
67
src/core/matcher.ts
Normal file
67
src/core/matcher.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { PlatformInfo } from './platform';
|
||||
|
||||
export interface MatchOptions {
|
||||
fileName?: string;
|
||||
fileType?: string;
|
||||
}
|
||||
|
||||
export function getMatchingAsset(assets: any[], platform: PlatformInfo, options: MatchOptions): any {
|
||||
const { fileName, fileType = 'archive' } = options;
|
||||
let extPattern: string;
|
||||
if (fileType === 'archive') {
|
||||
extPattern = '\\.(zip|tar\\.gz|tar|tgz|7z)';
|
||||
} else if (fileType === 'package') {
|
||||
extPattern = '\\.(deb|rpm|pkg)';
|
||||
} else {
|
||||
extPattern = fileType;
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
// Rule 1: Default matching rule
|
||||
const pattern = `${platform.systemPattern}[_-]${platform.archPattern}.*${extPattern}$`;
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
const matchingAssets = assets.filter((a: any) => 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: any) => a.name).join(', ')}`);
|
||||
}
|
||||
return matchingAssets[0];
|
||||
} else if (fileName.startsWith('~')) {
|
||||
// Rule 3: Regex matching rule
|
||||
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, platform.systemPattern)
|
||||
.replace(/{{ARCH}}/g, platform.archPattern)
|
||||
.replace(/{{EXT_PATTERN}}/g, extPattern);
|
||||
|
||||
const regex = new RegExp(finalPattern, 'i');
|
||||
const matchingAssets = assets.filter((a: any) => 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: any) => a.name).join(', ')}`);
|
||||
}
|
||||
return matchingAssets[0];
|
||||
} else {
|
||||
// Rule 2: Literal matching rule
|
||||
const asset = assets.find((a: any) => a.name === fileName);
|
||||
if (!asset) {
|
||||
throw new Error(`No asset found matching the exact name: ${fileName}`);
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
31
src/core/platform.ts
Normal file
31
src/core/platform.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as os from 'os';
|
||||
|
||||
export interface PlatformInfo {
|
||||
system: string;
|
||||
arch: string;
|
||||
systemPattern: string;
|
||||
archPattern: string;
|
||||
}
|
||||
|
||||
export const systemPatterns: Record<string, string> = {
|
||||
linux: 'linux',
|
||||
darwin: '(darwin|macos|mac|osx)',
|
||||
win32: '(windows|win)'
|
||||
};
|
||||
|
||||
export const archPatterns: Record<string, string> = {
|
||||
x64: '(x86_64|x64|amd64)',
|
||||
arm64: '(aarch64|arm64)'
|
||||
};
|
||||
|
||||
export function getPlatformInfo(): PlatformInfo {
|
||||
const system = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
return {
|
||||
system,
|
||||
arch,
|
||||
systemPattern: systemPatterns[system] || system,
|
||||
archPattern: archPatterns[arch] || arch
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user