Matched functionality of the CLI with the Bash predecessor.
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Slawomir Koszewski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
36
README.md
36
README.md
@@ -115,11 +115,17 @@ The following inputs are available for the GitHub Action, and as options for the
|
||||
- `repository` (required): The GitHub repository in the format `owner/repo` from which to download the release.
|
||||
- `file-name` (optional): The name or the regex pattern (prefixed with `~`) of the asset file to download from the release.
|
||||
- `binary-name` (optional): The name or regex pattern (prefixed with `~`) of the binary to search for within the downloaded asset. Defaults to the repository name.
|
||||
- `file-type` (optional, default: 'archive'): The regex pattern to identify the type of the file to be downloaded. There are two predefined keywords:
|
||||
- `file-type` (optional): Asset type selector.
|
||||
|
||||
- 'archive': matches common archive file extensions like .zip, .tar.gz, .tar, .tgz, .7z.
|
||||
- 'package': matches common package file extensions like .deb, .rpm, .pkg.
|
||||
- or a custom regex pattern can be provided to match specific file types.
|
||||
- `archive`: matches `.zip`, `.tar.gz`, `.tgz`.
|
||||
- `package`: matches `.deb`, `.pkg`, `.rpm`.
|
||||
- short forms: `zip`, `gzip`, `gz`, `tar`, `tar.gz`, `tgz`, `deb`, `pkg`, `rpm`.
|
||||
|
||||
If not provided, selection defaults to OS-aware combined package/archive patterns:
|
||||
|
||||
- Linux: `.deb`, `.rpm`, `.zip`, `.tar.gz`, `.tgz`
|
||||
- macOS: `.pkg`, `.zip`, `.tar.gz`, `.tgz`
|
||||
- other: `.zip`, `.tar.gz`, `.tgz`
|
||||
|
||||
- `install-path` (optional, CLI only): Custom installation directory for the CLI tool.
|
||||
- `update-cache` (optional, default: 'false', Action only): When set to 'false', the action will use the cached version of the tool if it is already available. If set to 'true', the action will check the latest release and update the cache if a newer version is found. If set to 'always', it will always download and install, updating the cache regardless.
|
||||
@@ -139,10 +145,19 @@ Arguments:
|
||||
repository The GitHub repository (owner/repo)
|
||||
|
||||
Options:
|
||||
--dry-run [level] Run in test mode (default level: 1)
|
||||
Or set TEST_MODE environment variable to a value > 0
|
||||
-l, --list [repository] List available assets from latest release and exit
|
||||
-a, --app-name <name> Application name (optional, for output messages)
|
||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||
-b, --binary-name <name> Binary name (supports source:destination form)
|
||||
-t, --file-type <type> archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm
|
||||
-p, --install-path <path> Custom installation directory
|
||||
-o, --output-directory <path>
|
||||
Only download selected asset to the specified directory
|
||||
-j, --releases-json Download latest release JSON only
|
||||
--system <name> Override detected system for asset matching
|
||||
--arch <name> Override detected architecture for asset matching
|
||||
-k, --token <token> GitHub token
|
||||
-d, --debug Enable debug logging
|
||||
-h, --help Show this help message
|
||||
@@ -178,11 +193,14 @@ If the `token` input is not provided, it will read from the `GITHUB_TOKEN` envir
|
||||
|
||||
The list of assets from the latest release is filtered based on the following rules:
|
||||
|
||||
1. If neither `file-name` nor `file-type` is provided, the tool defaults to selecting assets that match the following regular expression: `{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`, where:
|
||||
1. If neither `file-name` nor `file-type` is provided, the tool defaults to selecting assets with an OS-aware extension pattern and this regular expression shape: `{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}`, where:
|
||||
|
||||
- `{{SYSTEM}}` is replaced with the detected operating system regex.
|
||||
- `{{ARCH}}` is replaced with the detected architecture regex.
|
||||
- `{{EXT_PATTERN}}` is a regex pattern defined by the `file-type` input (defaulting to 'archive' if not specified).
|
||||
- `{{EXT_PATTERN}}` is selected by OS:
|
||||
Linux: `\.(deb|rpm|zip|tar\.gz|tgz)$`
|
||||
macOS: `\.(pkg|zip|tar\.gz|tgz)$`
|
||||
other: `\.(zip|tar\.gz|tgz)$`
|
||||
|
||||
2. If `file-name` is provided literally, the tool uses it directly to match the asset name by using exact string comparison.
|
||||
|
||||
@@ -192,7 +210,7 @@ The list of assets from the latest release is filtered based on the following ru
|
||||
- If it already ends with `$` or includes all three placeholders, the tool uses it as-is to match the asset name using regex.
|
||||
- If only `{{SYSTEM}}` and `{{ARCH}}` placeholders are included, the tool appends `.*{{EXT_PATTERN}}$`.
|
||||
|
||||
4. If `file-type` is not equal to 'archive' or 'package', it is treated as a custom regex pattern to match the file extension.
|
||||
4. If `file-type` is provided, supported values are: `archive`, `package`, `zip`, `gzip`, `gz`, `tar`, `tar.gz`, `tgz`, `deb`, `pkg`, `rpm`.
|
||||
|
||||
5. The tool applies the constructed regex pattern to filter the assets from the latest release.
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ inputs:
|
||||
description: 'The name or regex pattern (prefixed with ~) of the binary to search for within the asset. Defaults to the repository name.'
|
||||
required: false
|
||||
file-type:
|
||||
description: 'The type of the file to be downloaded (archive, package, or custom regex).'
|
||||
description: 'Asset type selector: archive, package, zip, gzip, gz, tar, tar.gz, tgz, deb, pkg, rpm.'
|
||||
required: false
|
||||
default: 'archive'
|
||||
update-cache:
|
||||
description: 'How to handle the tool cache (false, true, or always). Defaults to false.'
|
||||
required: false
|
||||
|
||||
19907
dist/check-token-action.js
vendored
19907
dist/check-token-action.js
vendored
File diff suppressed because one or more lines are too long
49
dist/check-token.js
vendored
Normal file → Executable file
49
dist/check-token.js
vendored
Normal file → Executable file
@@ -1,2 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";var n=require("util");async function i(t,e){let s=`https://api.github.com/repos/${t}/releases/latest`,r={Accept:"application/vnd.github.v3+json","User-Agent":"setup-github-release-action"};e&&(r.Authorization=`token ${e}`);let o=await fetch(s,{headers:r});if(!o.ok){let a=await o.text();throw new Error(`Failed to fetch latest release for ${t}: ${o.statusText}. ${a}`)}return await o.json()}async function c(){let{positionals:t}=(0,n.parseArgs)({allowPositionals:!0}),e=t[0]||process.env.GITHUB_TOKEN;e||(console.error("Error: No GitHub token provided as an argument or found in GITHUB_TOKEN environment variable."),process.exit(1));try{console.log("Verifying GitHub token..."),await i("actions/checkout",e),console.log("\x1B[32mSuccess: The provided GitHub token is valid and has sufficient permissions to access public repositories.\x1B[0m")}catch(s){console.error("\x1B[31mError: GitHub token verification failed.\x1B[0m"),console.error(`Reason: ${s.message}`),process.exit(1)}}c();
|
||||
"use strict";
|
||||
|
||||
// src/check-token.ts
|
||||
var import_util = require("util");
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// src/check-token.ts
|
||||
async function run() {
|
||||
const { positionals } = (0, import_util.parseArgs)({
|
||||
allowPositionals: true
|
||||
});
|
||||
const token = positionals[0] || process.env.GITHUB_TOKEN;
|
||||
if (!token) {
|
||||
console.error("Error: No GitHub token provided as an argument or found in GITHUB_TOKEN environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
console.log("Verifying GitHub token...");
|
||||
await fetchLatestRelease("actions/checkout", token);
|
||||
console.log("\x1B[32mSuccess: The provided GitHub token is valid and has sufficient permissions to access public repositories.\x1B[0m");
|
||||
} catch (error) {
|
||||
console.error("\x1B[31mError: GitHub token verification failed.\x1B[0m");
|
||||
console.error(`Reason: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
run();
|
||||
|
||||
545
dist/cli.js
vendored
545
dist/cli.js
vendored
@@ -1,16 +1,553 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";var O=Object.create;var z=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var D=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var Y=(t,e,i,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of U(e))!X.call(t,s)&&s!==i&&z(t,s,{get:()=>e[s],enumerable:!(r=H(e,s))||r.enumerable});return t};var p=(t,e,i)=>(i=t!=null?O(D(t)):{},Y(e||!t||!t.__esModule?z(i,"default",{value:t,enumerable:!0}):i,t));var F=require("util"),f=p(require("path")),l=p(require("fs")),P=p(require("os"));var T=p(require("os")),G={linux:"linux",darwin:"(darwin|macos|mac|osx)",win32:"(windows|win)"},q={x64:"(x86_64|x64|amd64)",arm64:"(aarch64|arm64)"};function M(){let t=T.platform(),e=T.arch();return{system:t,arch:e,systemPattern:G[t]||t,archPattern:q[e]||e}}function j(t,e,i){let{fileName:r,fileType:s="archive"}=i,o;if(s==="archive"?o="\\.(zip|tar\\.gz|tar|tgz|7z)":s==="package"?o="\\.(deb|rpm|pkg)":o=s,r)if(r.startsWith("~")){let a=r.substring(1),c=a.includes("{{SYSTEM}}"),n=a.includes("{{ARCH}}"),m=a.includes("{{EXT_PATTERN}}"),S=a.endsWith("$");!c&&!n&&!m&&!S?a+=".*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$":c&&n&&!m&&!S&&(a+=".*{{EXT_PATTERN}}$");let h=a.replace(/{{SYSTEM}}/g,e.systemPattern).replace(/{{ARCH}}/g,e.archPattern).replace(/{{EXT_PATTERN}}/g,o),b=new RegExp(h,"i"),u=t.filter(w=>b.test(w.name));if(u.length===0)throw new Error(`No assets matched the regex: ${h}`);if(u.length>1)throw new Error(`Multiple assets matched the criteria: ${u.map(w=>w.name).join(", ")}`);return u[0]}else{let a=t.find(c=>c.name===r);if(!a)throw new Error(`No asset found matching the exact name: ${r}`);return a}else{let a=`${e.systemPattern}[_-]${e.archPattern}.*${o}$`,c=new RegExp(a,"i"),n=t.filter(m=>c.test(m.name));if(n.length===0)throw new Error(`No assets matched the default criteria: ${a}`);if(n.length>1)throw new Error(`Multiple assets matched the default criteria: ${n.map(m=>m.name).join(", ")}`);return n[0]}}var A=p(require("fs")),_=p(require("path"));function I(t,e,i,r){let s=A.readdirSync(t);i&&(r(`Searching for binary in ${t}...`),s.forEach(o=>r(` - ${o}`)));for(let o of s){let a=_.join(t,o);if(A.statSync(a).isDirectory()){let n=I(a,e,i,r);if(n)return n}else{let n=!1;if(e instanceof RegExp?n=e.test(o):(n=o===e,!n&&process.platform==="win32"&&!e.toLowerCase().endsWith(".exe")&&(n=o.toLowerCase()===`${e.toLowerCase()}.exe`)),n)return a}}}async function W(t,e){let i=`https://api.github.com/repos/${t}/releases/latest`,r={Accept:"application/vnd.github.v3+json","User-Agent":"setup-github-release-action"};e&&(r.Authorization=`token ${e}`);let s=await fetch(i,{headers:r});if(!s.ok){let o=await s.text();throw new Error(`Failed to fetch latest release for ${t}: ${s.statusText}. ${o}`)}return await s.json()}async function L(t,e,i){let r={"User-Agent":"setup-github-release-action"};i&&(r.Authorization=`token ${i}`);let s=await fetch(t,{headers:r});if(!s.ok)throw new Error(`Failed to download asset: ${s.statusText}`);let o=await import("fs"),{Readable:a}=await import("stream"),{finished:c}=await import("stream/promises"),n=o.createWriteStream(e);await c(a.fromWeb(s.body).pipe(n))}var x=require("child_process"),y=p(require("path")),E=p(require("fs"));async function B(t,e){let i=y.extname(t).toLowerCase(),r=y.basename(t).toLowerCase();if(E.existsSync(e)||E.mkdirSync(e,{recursive:!0}),r.endsWith(".tar.gz")||r.endsWith(".tgz")||r.endsWith(".tar")){let o=(0,x.spawnSync)("tar",["-xf",t,"-C",e]);if(o.status!==0)throw new Error(`tar failed with status ${o.status}: ${o.stderr.toString()}`)}else if(r.endsWith(".zip"))if(process.platform==="win32"){if((0,x.spawnSync)("tar",["-xf",t,"-C",e]).status===0)return;let o=t.replace(/'/g,"''"),a=e.replace(/'/g,"''"),c=`Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${o}', '${a}')`;for(let n of["pwsh","powershell"])if((0,x.spawnSync)(n,["-NoProfile","-ExecutionPolicy","Bypass","-Command",c]).status===0)return;throw new Error("Extraction failed: Both tar and PowerShell fallback failed. Make sure your system can extract ZIP files.")}else{let s=(0,x.spawnSync)("unzip",["-q",t,"-d",e]);if(s.status!==0)throw new Error(`unzip failed with status ${s.status}: ${s.stderr.toString()}`)}else if(r.endsWith(".7z")){let s=(0,x.spawnSync)("7z",["x",t,`-o${e}`,"-y"]);if(s.status!==0)throw new Error(`7z failed with status ${s.status}. Make sure 7z is installed.`)}else{let s=y.join(e,y.basename(t));E.copyFileSync(t,s)}}async function Z(){let{values:t,positionals:e}=(0,F.parseArgs)({options:{"file-name":{type:"string",short:"f"},"binary-name":{type:"string",short:"b"},"file-type":{type:"string",short:"t",default:"archive"},"install-path":{type:"string",short:"p"},token:{type:"string",short:"k"},debug:{type:"boolean",short:"d",default:!1},help:{type:"boolean",short:"h"}},allowPositionals:!0});(t.help||e.length===0)&&(console.log(`
|
||||
"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] <repository>
|
||||
|
||||
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 <name> Application name (optional, for output messages)
|
||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||
-b, --binary-name <name> Binary name (supports source:destination form)
|
||||
-t, --file-type <type> archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm
|
||||
-p, --install-path <path> Custom installation directory
|
||||
-o, --output-directory <path>
|
||||
Only download selected asset to the specified directory
|
||||
-j, --releases-json Download latest release JSON only
|
||||
--system <name> Override detected system for asset matching
|
||||
--arch <name> Override detected architecture for asset matching
|
||||
-k, --token <token> GitHub token
|
||||
-d, --debug Enable debug logging
|
||||
-h, --help Show this help message
|
||||
`),process.exit(0));let i=e[0];i||(console.error("Error: Repository is required."),process.exit(1));let r=t["file-name"],s=t["binary-name"],o=t["file-type"],a=!!t.debug,c=t.token||process.env.GITHUB_TOKEN;try{let n=M(),m=i.split("/").pop()||i;console.log(`Fetching latest release for ${i}...`);let S=await W(i,c),h=j(S.assets,n,{fileName:r,fileType:o});console.log(`Selected asset: ${h.name}`);let b=l.mkdtempSync(f.join(P.tmpdir(),"setup-gh-release-")),u=f.join(b,h.name);console.log(`Downloading ${h.name}...`),await L(h.browser_download_url,u,c);let w=f.join(b,"extract");console.log(`Extracting ${h.name}...`),await B(u,w);let R=s||m,k;R.startsWith("~")?k=new RegExp(R.substring(1),"i"):k=R;let C=I(w,k,a,console.log);if(!C)throw new Error(`Could not find binary "${R}" in the extracted asset.`);let g;if(t["install-path"])g=f.resolve(t["install-path"]);else if(process.platform==="win32"){let d=process.env.LOCALAPPDATA||f.join(P.homedir(),"AppData","Local");g=f.join(d,"bin")}else if(process.getuid&&process.getuid()===0)g="/usr/local/bin";else{let N=f.join(P.homedir(),"bin");l.existsSync(N)?g=N:g="/usr/local/bin"}l.existsSync(g)||l.mkdirSync(g,{recursive:!0});let v=f.basename(C),$=f.join(g,v);console.log(`Installing ${v} to ${$}...`);try{l.copyFileSync(C,$)}catch(d){throw d.code==="EBUSY"?new Error(`The file ${$} is currently in use. Please close any running instances and try again.`):d.code==="EACCES"||d.code==="EPERM"?new Error(`Permission denied while installing to ${$}. Try running with sudo or as administrator, or use -p to specify a custom path.`):d}process.platform!=="win32"&&l.chmodSync($,"755"),l.rmSync(b,{recursive:!0,force:!0}),console.log("Installation successful!")}catch(n){console.error(`Error: ${n.message}`),process.exit(1)}}Z();
|
||||
`;
|
||||
}
|
||||
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();
|
||||
|
||||
22126
dist/index.js
vendored
22126
dist/index.js
vendored
File diff suppressed because one or more lines are too long
18
package.json
18
package.json
@@ -8,24 +8,18 @@
|
||||
"check-github-token": "dist/check-token.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build:action": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js --minify",
|
||||
"build:cli": "esbuild src/cli.ts --bundle --platform=node --target=node24 --outfile=dist/cli.js --minify --banner:js=\"#!/usr/bin/env node\"",
|
||||
"build:check-token": "esbuild src/check-token.ts --bundle --platform=node --target=node24 --outfile=dist/check-token.js --minify --banner:js=\"#!/usr/bin/env node\"",
|
||||
"build:check-token-action": "esbuild src/check-token-action.ts --bundle --platform=node --target=node24 --outfile=dist/check-token-action.js --minify",
|
||||
"build": "npm run build:action && npm run build:cli && npm run build:check-token && npm run build:check-token-action",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"package": "npm run build",
|
||||
"test": "jest",
|
||||
"all": "npm run format && npm run lint && npm run test && npm run package"
|
||||
"build:action": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js",
|
||||
"build:cli": "esbuild src/cli.ts --bundle --platform=node --target=node24 --outfile=dist/cli.js --banner:js=\"#!/usr/bin/env node\"",
|
||||
"build:check-token": "esbuild src/check-token.ts --bundle --platform=node --target=node24 --outfile=dist/check-token.js --banner:js=\"#!/usr/bin/env node\"",
|
||||
"build:check-token-action": "esbuild src/check-token-action.ts --bundle --platform=node --target=node24 --outfile=dist/check-token-action.js",
|
||||
"build": "npm run build:action && npm run build:cli && npm run build:check-token && npm run build:check-token-action"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"node",
|
||||
"setup"
|
||||
],
|
||||
"author": "",
|
||||
"author": "Sławomir Koszewski",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
|
||||
364
src/cli.ts
364
src/cli.ts
@@ -1,123 +1,332 @@
|
||||
import { parseArgs } from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { getPlatformInfo } from './core/platform';
|
||||
import { getMatchingAsset } from './core/matcher';
|
||||
import { findBinary } from './core/finder';
|
||||
import { fetchLatestRelease, downloadAsset } from './core/downloader';
|
||||
import { fetchLatestRelease, fetchLatestReleaseRaw, downloadAsset } from './core/downloader';
|
||||
import { extractAsset } from './core/extractor';
|
||||
|
||||
async function run() {
|
||||
const { values, positionals } = parseArgs({
|
||||
options: {
|
||||
'file-name': { type: 'string', short: 'f' },
|
||||
'binary-name': { type: 'string', short: 'b' },
|
||||
'file-type': { type: 'string', short: 't', default: 'archive' },
|
||||
'install-path': { type: 'string', short: 'p' },
|
||||
'token': { type: 'string', short: 'k' },
|
||||
'debug': { type: 'boolean', short: 'd', default: false },
|
||||
'help': { type: 'boolean', short: 'h' }
|
||||
},
|
||||
allowPositionals: true
|
||||
});
|
||||
interface CliOptions {
|
||||
appName?: string;
|
||||
fileName?: string;
|
||||
binaryName?: string;
|
||||
fileType?: string;
|
||||
installPath?: string;
|
||||
outputDirectory?: string;
|
||||
releasesJsonOnly: boolean;
|
||||
listOnly: boolean;
|
||||
token?: string;
|
||||
debug: boolean;
|
||||
help: boolean;
|
||||
dryRunLevel: number;
|
||||
systemOverride?: string;
|
||||
archOverride?: string;
|
||||
listRepo?: string;
|
||||
positionals: string[];
|
||||
}
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
console.log(`
|
||||
function usage(): string {
|
||||
return `
|
||||
Usage: install-github-release [options] <repository>
|
||||
|
||||
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 <name> Application name (optional, for output messages)
|
||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||
-b, --binary-name <name> Binary name (supports source:destination form)
|
||||
-t, --file-type <type> archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm
|
||||
-p, --install-path <path> Custom installation directory
|
||||
-o, --output-directory <path>
|
||||
Only download selected asset to the specified directory
|
||||
-j, --releases-json Download latest release JSON only
|
||||
--system <name> Override detected system for asset matching
|
||||
--arch <name> Override detected architecture for asset matching
|
||||
-k, --token <token> GitHub token
|
||||
-d, --debug Enable debug logging
|
||||
-h, --help Show this help message
|
||||
`);
|
||||
process.exit(0);
|
||||
`;
|
||||
}
|
||||
|
||||
function ensureOptionValue(argv: string[], index: number, option: string): string {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith('-')) {
|
||||
throw new Error(`Missing value for ${option}.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): CliOptions {
|
||||
const envDryRun = process.env.TEST_MODE;
|
||||
const dryRunLevelFromEnv = envDryRun && /^\d+$/.test(envDryRun) ? parseInt(envDryRun, 10) : 0;
|
||||
|
||||
const opts: CliOptions = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const repository = positionals[0];
|
||||
if (!repository) {
|
||||
console.error('Error: Repository is required.');
|
||||
process.exit(1);
|
||||
return opts;
|
||||
}
|
||||
|
||||
function validateOutputDirectory(outputDirectory: string): string {
|
||||
const resolvedPath = path.resolve(outputDirectory);
|
||||
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
|
||||
throw new Error(`Output directory "${resolvedPath}" does not exist.`);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
function getInstallDir(installPath?: string): string {
|
||||
if (installPath) {
|
||||
return path.resolve(installPath);
|
||||
}
|
||||
|
||||
const fileNameInput = values['file-name'];
|
||||
const binaryInput = values['binary-name'];
|
||||
const fileType = values['file-type'];
|
||||
const debug = !!values.debug;
|
||||
const token = values.token || process.env.GITHUB_TOKEN;
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||
return path.join(localAppData, 'bin');
|
||||
}
|
||||
|
||||
const isRoot = process.getuid && process.getuid() === 0;
|
||||
if (isRoot) {
|
||||
return '/usr/local/bin';
|
||||
}
|
||||
|
||||
const homeBin = path.join(os.homedir(), 'bin');
|
||||
if (fs.existsSync(homeBin)) {
|
||||
return homeBin;
|
||||
}
|
||||
|
||||
return '/usr/local/bin';
|
||||
}
|
||||
|
||||
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(' ')}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let tempDir: string | undefined;
|
||||
try {
|
||||
const platformInfo = getPlatformInfo();
|
||||
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 release = await fetchLatestRelease(repository, token);
|
||||
release.assets.forEach((asset) => console.log(`- ${asset.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
|
||||
? path.join(validateOutputDirectory(options.outputDirectory), outputName)
|
||||
: outputName;
|
||||
|
||||
fs.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: fileNameInput,
|
||||
fileType: fileType
|
||||
fileName: options.fileName,
|
||||
fileType: options.fileType
|
||||
});
|
||||
|
||||
console.log(`Selected asset: ${asset.name}`);
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-gh-release-'));
|
||||
const downloadPath = path.join(tempDir, asset.name);
|
||||
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}".`);
|
||||
|
||||
console.log(`Downloading ${asset.name}...`);
|
||||
await downloadAsset(asset.browser_download_url, downloadPath, token);
|
||||
if (options.dryRunLevel > 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.outputDirectory) {
|
||||
const outputDir = validateOutputDirectory(options.outputDirectory);
|
||||
const outputPath = path.join(outputDir, path.basename(downloadUrl));
|
||||
console.log(`Downloading '${appName}' version ${version} to '${outputPath}'...`);
|
||||
await downloadAsset(downloadUrl, outputPath, token);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-gh-release-'));
|
||||
const downloadPath = path.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 = path.join(tempDir, 'extract');
|
||||
console.log(`Extracting ${asset.name}...`);
|
||||
await extractAsset(downloadPath, extractDir);
|
||||
|
||||
const binaryName = binaryInput || toolName;
|
||||
let binaryPattern: string | RegExp;
|
||||
if (binaryName.startsWith('~')) {
|
||||
binaryPattern = new RegExp(binaryName.substring(1), 'i');
|
||||
if (binarySource.startsWith('~')) {
|
||||
binaryPattern = new RegExp(binarySource.substring(1), 'i');
|
||||
} else {
|
||||
binaryPattern = binaryName;
|
||||
binaryPattern = binarySource;
|
||||
}
|
||||
|
||||
const binaryPath = findBinary(extractDir, binaryPattern, debug, console.log);
|
||||
const binaryPath = findBinary(extractDir, binaryPattern, options.debug, console.log);
|
||||
if (!binaryPath) {
|
||||
throw new Error(`Could not find binary "${binaryName}" in the extracted asset.`);
|
||||
}
|
||||
|
||||
// Determine install directory
|
||||
let installDir: string;
|
||||
|
||||
if (values['install-path']) {
|
||||
installDir = path.resolve(values['install-path']);
|
||||
} else {
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||
installDir = path.join(localAppData, 'bin');
|
||||
} else {
|
||||
const isRoot = process.getuid && process.getuid() === 0;
|
||||
|
||||
if (isRoot) {
|
||||
installDir = '/usr/local/bin';
|
||||
} else {
|
||||
const homeBin = path.join(os.homedir(), 'bin');
|
||||
if (fs.existsSync(homeBin)) {
|
||||
installDir = homeBin;
|
||||
} else {
|
||||
installDir = '/usr/local/bin';
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find binary "${binarySource}" in the extracted asset.`);
|
||||
}
|
||||
|
||||
const installDir = getInstallDir(options.installPath);
|
||||
if (!fs.existsSync(installDir)) {
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
}
|
||||
|
||||
const finalName = path.basename(binaryPath);
|
||||
const finalName = binaryDestination || path.basename(binaryPath);
|
||||
const destPath = path.join(installDir, finalName);
|
||||
|
||||
console.log(`Installing ${finalName} to ${destPath}...`);
|
||||
@@ -137,14 +346,19 @@ Options:
|
||||
fs.chmodSync(destPath, '755');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
|
||||
console.log('Installation successful!');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
if (error?.message) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('Error: Unknown failure.');
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (tempDir && fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { getMatchingAsset } from './matcher';
|
||||
import { PlatformInfo } from './platform';
|
||||
|
||||
export interface ReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
@@ -11,8 +8,7 @@ export interface ReleaseInfo {
|
||||
assets: ReleaseAsset[];
|
||||
}
|
||||
|
||||
export async function fetchLatestRelease(repository: string, token?: string): Promise<ReleaseInfo> {
|
||||
const url = `https://api.github.com/repos/${repository}/releases/latest`;
|
||||
function getGithubApiHeaders(token?: string): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'setup-github-release-action'
|
||||
@@ -20,6 +16,12 @@ export async function fetchLatestRelease(repository: string, token?: string): Pr
|
||||
if (token) {
|
||||
headers['Authorization'] = `token ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function fetchLatestRelease(repository: string, token?: string): Promise<ReleaseInfo> {
|
||||
const url = `https://api.github.com/repos/${repository}/releases/latest`;
|
||||
const headers = getGithubApiHeaders(token);
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
if (!response.ok) {
|
||||
@@ -30,6 +32,18 @@ export async function fetchLatestRelease(repository: string, token?: string): Pr
|
||||
return await response.json() as ReleaseInfo;
|
||||
}
|
||||
|
||||
export async function fetchLatestReleaseRaw(repository: string, token?: string): Promise<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
export async function downloadAsset(url: string, destPath: string, token?: string): Promise<void> {
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent': 'setup-github-release-action'
|
||||
|
||||
@@ -5,20 +5,67 @@ export interface MatchOptions {
|
||||
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;
|
||||
function normalizeCustomExtensionPattern(fileType: string): string {
|
||||
let pattern = fileType;
|
||||
|
||||
if (!pattern.endsWith('$')) {
|
||||
pattern += '$';
|
||||
}
|
||||
|
||||
if (!pattern.startsWith('\\.')) {
|
||||
pattern = `\\.${pattern}`;
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
function getExtPattern(fileType: string | undefined, system: string): string {
|
||||
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: Record<string, string> = {
|
||||
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 || '');
|
||||
}
|
||||
|
||||
export function getMatchingAsset(assets: any[], platform: PlatformInfo, options: MatchOptions): any {
|
||||
const { fileName, fileType } = options;
|
||||
const extPattern = getExtPattern(fileType, platform.system);
|
||||
|
||||
if (!fileName) {
|
||||
// Rule 1: Default matching rule
|
||||
const pattern = `${platform.systemPattern}[_-]${platform.archPattern}.*${extPattern}$`;
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,11 @@ export interface PlatformInfo {
|
||||
archPattern: string;
|
||||
}
|
||||
|
||||
export interface PlatformOverrides {
|
||||
system?: string;
|
||||
arch?: string;
|
||||
}
|
||||
|
||||
export const systemPatterns: Record<string, string> = {
|
||||
linux: 'linux',
|
||||
darwin: '(darwin|macos|mac|osx)',
|
||||
@@ -18,9 +23,9 @@ export const archPatterns: Record<string, string> = {
|
||||
arm64: '(aarch64|arm64)'
|
||||
};
|
||||
|
||||
export function getPlatformInfo(): PlatformInfo {
|
||||
const system = os.platform();
|
||||
const arch = os.arch();
|
||||
export function getPlatformInfo(overrides?: PlatformOverrides): PlatformInfo {
|
||||
const system = (overrides?.system || os.platform()).toLowerCase();
|
||||
const arch = (overrides?.arch || os.arch()).toLowerCase();
|
||||
|
||||
return {
|
||||
system,
|
||||
|
||||
71
src/index.ts
71
src/index.ts
@@ -1,19 +1,71 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
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') || 'archive';
|
||||
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;
|
||||
@@ -67,6 +119,21 @@ async function run() {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user