12 Commits
v1.2.2 ... main

Author SHA1 Message Date
483a1c5f13 Fix: README corrections. 2026-01-11 20:03:25 +01:00
90033e0b9e A dummy change to trigger the workflow. 2026-01-11 19:56:45 +01:00
f8a559538e Add default token to check-token subaction 2026-01-11 19:54:25 +01:00
09f36edc01 Updated GitHub workflow with new Check Token action. 2026-01-11 19:46:28 +01:00
55d6019d0f Fix: call correct action.
All checks were successful
Test Action / test (push) Successful in 4s
2026-01-11 19:31:53 +01:00
11a26bd176 Added check-token-action build and action.
Some checks failed
Test Action / test (push) Failing after 4s
2026-01-11 19:25:14 +01:00
32a4011b54 Added Check Token subaction.
All checks were successful
Test Action / test (push) Successful in 4s
2026-01-11 19:24:23 +01:00
2bb60fc0ed Added an utility that validates GitHub token.
All checks were successful
Test Action / test (push) Successful in 3s
2026-01-11 14:27:32 +01:00
fc727877e6 Fix: Minor formatting issue. 2026-01-11 14:27:08 +01:00
120b16b56e Update: Eliminated minor formatting issues.
All checks were successful
Test Action / test (push) Successful in 3s
2026-01-11 14:03:36 +01:00
4ee62009bb Fix: Windows decompression and installation paths. 2026-01-11 14:03:10 +01:00
b20a066030 Added test workflow for Github. 2026-01-11 11:38:24 +01:00
12 changed files with 289 additions and 31 deletions

View File

@@ -18,6 +18,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Check Authentication
uses: ./check-token
with:
token: ${{ secrets.GH_TOKEN }}
- name: Go ACME Setup - name: Go ACME Setup
uses: ./ uses: ./
with: with:

49
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Test Action
on:
push:
branches:
- main
paths:
- '.github/workflows/test.yml'
- 'action.yml'
- 'src/**'
- 'dist/**'
- 'package.json'
- 'package-lock.json'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Let's test the token first.
- name: Check Authentication
uses: skoszewski/setup-github-release/check-token@v1
- name: Go ACME Setup
uses: skoszewski/setup-github-release@v1
with:
repository: 'go-acme/lego'
- name: Setup Hugo
uses: skoszewski/setup-github-release@v1
with:
repository: 'gohugoio/hugo'
file-name: '~hugo_extended_[^a-z]'
- name: Setup RClone
uses: skoszewski/setup-github-release@v1
with:
repository: 'rclone/rclone'
- name: Verify Installation
run: |
echo "Verifying installed tools..."
printf "\nGo ACME Lego:\n"
lego -v
printf "\nHugo:\n"
hugo version
printf "\nRClone:\n"
rclone version

View File

@@ -10,7 +10,7 @@ Add the action to your workflow. Authenticate with `github.token` (default) or a
```yaml ```yaml
- name: Install Tool - name: Install Tool
uses: koszewscy/setup-github-release@v1 uses: skoszewski/setup-github-release@v1
with: with:
repository: 'owner/repo' repository: 'owner/repo'
``` ```
@@ -24,7 +24,7 @@ Install the CLI tool on any destination system with Node.js 24 or newer.
1. Clone the repository: 1. Clone the repository:
```bash ```bash
git clone https://github.com/koszewscy/setup-github-release git clone https://github.com/skoszewski/setup-github-release
cd setup-github-release cd setup-github-release
``` ```
@@ -41,7 +41,11 @@ npm run build
npm install -g . npm install -g .
``` ```
After installation, the tool will be available as `install-github-release`: After installation, the tool will be available as `install-github-release`. By default, it installs binaries to:
- Linux/macOS (root): `/usr/local/bin`
- Linux/macOS (user): `~/bin` (if exists) or `/usr/local/bin`
- Windows: `%LOCALAPPDATA%\bin`
```bash ```bash
install-github-release rclone/rclone install-github-release rclone/rclone
@@ -61,8 +65,8 @@ install-github-release rclone/rclone
The action will automatically detect your OS (Linux, Windows, macOS) and architecture (x64, ARM64) and look for a matching archive. It will search for a binary named after the repository. The action will automatically detect your OS (Linux, Windows, macOS) and architecture (x64, ARM64) and look for a matching archive. It will search for a binary named after the repository.
```yaml ```yaml
- name: Install Hugo - name: Install LEGO
uses: koszewscy/setup-github-release@v1 uses: skoszewski/setup-github-release@v1
with: with:
repository: 'go-acme/lego' repository: 'go-acme/lego'
``` ```
@@ -75,7 +79,7 @@ For projects with multiple binary versions, you can use a regex pattern (prefixe
```yaml ```yaml
- name: Install Extended Hugo - name: Install Extended Hugo
uses: koszewscy/setup-github-release@v1 uses: skoszewski/setup-github-release@v1
with: with:
repository: 'gohugoio/hugo' repository: 'gohugoio/hugo'
file-name: '~hugo_extended_[^a-z]' # Regex to match extended version file-name: '~hugo_extended_[^a-z]' # Regex to match extended version
@@ -87,7 +91,7 @@ If the binary name is different from the repository name, like in the example of
```yaml ```yaml
- name: Install GitHub CLI - name: Install GitHub CLI
uses: koszewscy/setup-github-release@v1 uses: skoszewski/setup-github-release@v1
with: with:
repository: 'cli/cli' repository: 'cli/cli'
binary-name: 'gh' # Searches for 'gh' (or 'gh.exe') inside the extracted release binary-name: 'gh' # Searches for 'gh' (or 'gh.exe') inside the extracted release
@@ -98,7 +102,7 @@ If the binary name is different from the repository name, like in the example of
If you are unsure how the binary is named, use the `debug` flag to list all files in the unpacked asset, or download the asset manually to inspect its structure. If you are unsure how the binary is named, use the `debug` flag to list all files in the unpacked asset, or download the asset manually to inspect its structure.
```yaml ```yaml
- uses: koszewscy/setup-github-release@v1 - uses: skoszewski/setup-github-release@v1
with: with:
repository: 'owner/repo' repository: 'owner/repo'
debug: true debug: true
@@ -112,9 +116,11 @@ The following inputs are available for the GitHub Action, and as options for the
- `file-name` (optional): The name or the regex pattern (prefixed with `~`) of the asset file to download from 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. - `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, default: 'archive'): The regex pattern to identify the type of the file to be downloaded. There are two predefined keywords:
- 'archive': matches common archive file extensions like .zip, .tar.gz, .tar, .tgz, .7z. - 'archive': matches common archive file extensions like .zip, .tar.gz, .tar, .tgz, .7z.
- 'package': matches common package file extensions like .deb, .rpm, .pkg. - 'package': matches common package file extensions like .deb, .rpm, .pkg.
- or a custom regex pattern can be provided to match specific file types. - or a custom regex pattern can be provided to match specific file types.
- `install-path` (optional, CLI only): Custom installation directory for the CLI tool. - `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. - `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.
- `debug` (optional, default: 'false'): When set to `true`, the action will log the contents of the unpacked directory to the console. - `debug` (optional, default: 'false'): When set to `true`, the action will log the contents of the unpacked directory to the console.
@@ -142,11 +148,38 @@ Options:
-h, --help Show this help message -h, --help Show this help message
``` ```
## GitHub Token Verification
The project includes a utility to verify the validity of your GitHub token.
### CLI Utility
```bash
check-github-token <token>
```
If no token is provided as an argument, it will attempt to read from the `GITHUB_TOKEN` environment variable.
### GitHub Action
You can also use the `check-token` subaction in your workflows:
```yaml
- name: Verify Token
uses: skoszewski/setup-github-release/check-token@v1
with:
repository: 'actions/checkout' # Optional, defaults to actions/checkout
token: ${{ secrets.MY_TOKEN }}
```
If the `token` input is not provided, it will read from the `GITHUB_TOKEN` environment variable.
## Asset Selection Procedure ## Asset Selection Procedure
The list of assets from the latest release is filtered based on the following rules: 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 that match the following regular expression: `{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`, where:
- `{{SYSTEM}}` is replaced with the detected operating system regex. - `{{SYSTEM}}` is replaced with the detected operating system regex.
- `{{ARCH}}` is replaced with the detected architecture 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 a regex pattern defined by the `file-type` input (defaulting to 'archive' if not specified).
@@ -154,6 +187,7 @@ The list of assets from the latest release is filtered based on the following ru
2. If `file-name` is provided literally, the tool uses it directly to match the asset name by using exact string comparison. 2. If `file-name` is provided literally, the tool uses it directly to match the asset name by using exact string comparison.
3. If `file-name` is provided as a regex pattern (prefixed with `~`), then: 3. If `file-name` is provided as a regex pattern (prefixed with `~`), then:
- If the pattern does not end with `$` and does not include any placeholders, the tool appends `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$` to the provided pattern. - If the pattern does not end with `$` and does not include any placeholders, the tool appends `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$` to the provided pattern.
- If it already ends with `$` or includes all three placeholders, the tool uses it as-is to match the asset name using regex. - 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}}$`. - If only `{{SYSTEM}}` and `{{ARCH}}` placeholders are included, the tool appends `.*{{EXT_PATTERN}}$`.
@@ -167,11 +201,13 @@ The list of assets from the latest release is filtered based on the following ru
7. After download and extraction, the tool recursively searches for the binary specified by `binary-name` (or the repository name). If found, the directory containing the binary is used as the tool directory and added to the PATH (or used for installation). If the binary is not found, the tool fails. 7. After download and extraction, the tool recursively searches for the binary specified by `binary-name` (or the repository name). If found, the directory containing the binary is used as the tool directory and added to the PATH (or used for installation). If the binary is not found, the tool fails.
8. `{{SYSTEM}}` is replaced with the detected operating system regex: 8. `{{SYSTEM}}` is replaced with the detected operating system regex:
- For Linux: `linux`. - For Linux: `linux`.
- For MacOS: `(darwin|macos|mac|osx)`. - For MacOS: `(darwin|macos|mac)`.
- For Windows: `(windows|win)`. - For Windows: `(windows|win)`.
9. `{{ARCH}}` is replaced with the detected architecture regex: 9. `{{ARCH}}` is replaced with the detected architecture regex:
- For x64: `(x86_64|x64|amd64)`. - For x64: `(x86_64|x64|amd64)`.
- For arm64: `(aarch64|arm64)`. - For arm64: `(aarch64|arm64)`.

15
check-token/action.yml Normal file
View File

@@ -0,0 +1,15 @@
name: 'check-token'
description: 'Verify the validity of a GitHub token'
author: 'Slawomir Koszewski with GitHub Copilot assistance'
inputs:
repository:
description: 'The GitHub repository to check (e.g., owner/repo)'
required: false
default: 'actions/checkout'
token:
description: 'The GitHub token to verify'
required: false
default: ${{ github.token }}
runs:
using: 'node24'
main: '../dist/check-token-action.js'

69
dist/check-token-action.js vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/check-token.js vendored Normal file
View File

@@ -0,0 +1,2 @@
#!/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();

4
dist/cli.js vendored
View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
"use strict";var F=Object.create;var N=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var U=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var G=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of O(e))!X.call(t,s)&&s!==n&&N(t,s,{get:()=>e[s],enumerable:!(r=H(e,s))||r.enumerable});return t};var f=(t,e,n)=>(n=t!=null?F(U(t)):{},G(e||!t||!t.__esModule?N(n,"default",{value:t,enumerable:!0}):n,t));var B=require("util"),p=f(require("path")),c=f(require("fs")),P=f(require("os"));var S=f(require("os")),Y={linux:"linux",darwin:"(darwin|macos|mac|osx)",win32:"(windows|win)"},q={x64:"(x86_64|x64|amd64)",arm64:"(aarch64|arm64)"};function _(){let t=S.platform(),e=S.arch();return{system:t,arch:e,systemPattern:Y[t]||t,archPattern:q[e]||e}}function C(t,e,n){let{fileName:r,fileType:s="archive"}=n,o;if(s==="archive"?o="\\.(zip|tar\\.gz|tar|tgz|7z)":s==="package"?o="\\.(deb|rpm|pkg)":o=s,r)if(r.startsWith("~")){let i=r.substring(1),l=i.includes("{{SYSTEM}}"),a=i.includes("{{ARCH}}"),h=i.includes("{{EXT_PATTERN}}"),$=i.endsWith("$");!l&&!a&&!h&&!$?i+=".*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$":l&&a&&!h&&!$&&(i+=".*{{EXT_PATTERN}}$");let m=i.replace(/{{SYSTEM}}/g,e.systemPattern).replace(/{{ARCH}}/g,e.archPattern).replace(/{{EXT_PATTERN}}/g,o),x=new RegExp(m,"i"),g=t.filter(w=>x.test(w.name));if(g.length===0)throw new Error(`No assets matched the regex: ${m}`);if(g.length>1)throw new Error(`Multiple assets matched the criteria: ${g.map(w=>w.name).join(", ")}`);return g[0]}else{let i=t.find(l=>l.name===r);if(!i)throw new Error(`No asset found matching the exact name: ${r}`);return i}else{let i=`${e.systemPattern}[_-]${e.archPattern}.*${o}$`,l=new RegExp(i,"i"),a=t.filter(h=>l.test(h.name));if(a.length===0)throw new Error(`No assets matched the default criteria: ${i}`);if(a.length>1)throw new Error(`Multiple assets matched the default criteria: ${a.map(h=>h.name).join(", ")}`);return a[0]}}var b=f(require("fs")),M=f(require("path"));function k(t,e,n,r){let s=b.readdirSync(t);n&&(r(`Searching for binary in ${t}...`),s.forEach(o=>r(` - ${o}`)));for(let o of s){let i=M.join(t,o);if(b.statSync(i).isDirectory()){let a=k(i,e,n,r);if(a)return a}else{let a=!1;if(e instanceof RegExp?a=e.test(o):(a=o===e,!a&&process.platform==="win32"&&!e.toLowerCase().endsWith(".exe")&&(a=o.toLowerCase()===`${e.toLowerCase()}.exe`)),a)return i}}}async function j(t,e){let n=`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(n,{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 W(t,e,n){let r={"User-Agent":"setup-github-release-action"};n&&(r.Authorization=`token ${n}`);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:i}=await import("stream"),{finished:l}=await import("stream/promises"),a=o.createWriteStream(e);await l(i.fromWeb(s.body).pipe(a))}var E=require("child_process"),d=f(require("path")),y=f(require("fs"));async function L(t,e){let n=d.extname(t).toLowerCase(),r=d.basename(t).toLowerCase();if(y.existsSync(e)||y.mkdirSync(e,{recursive:!0}),r.endsWith(".tar.gz")||r.endsWith(".tgz")||r.endsWith(".tar")){let o=(0,E.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"){let s=`Expand-Archive -Path "${t}" -DestinationPath "${e}" -Force`,o=(0,E.spawnSync)("powershell",["-Command",s]);if(o.status!==0)throw new Error(`powershell Expand-Archive failed with status ${o.status}: ${o.stderr.toString()}`)}else{let s=(0,E.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,E.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=d.join(e,d.basename(t));y.copyFileSync(t,s)}}async function K(){let{values:t,positionals:e}=(0,B.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 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(`
Usage: install-github-release [options] <repository> Usage: install-github-release [options] <repository>
Arguments: Arguments:
@@ -13,4 +13,4 @@ Options:
-k, --token <token> GitHub token -k, --token <token> GitHub token
-d, --debug Enable debug logging -d, --debug Enable debug logging
-h, --help Show this help message -h, --help Show this help message
`),process.exit(0));let n=e[0];n||(console.error("Error: Repository is required."),process.exit(1));let r=t["file-name"],s=t["binary-name"],o=t["file-type"],i=!!t.debug,l=t.token||process.env.GITHUB_TOKEN;try{let a=_(),h=n.split("/").pop()||n;console.log(`Fetching latest release for ${n}...`);let $=await j(n,l),m=C($.assets,a,{fileName:r,fileType:o});console.log(`Selected asset: ${m.name}`);let x=c.mkdtempSync(p.join(P.tmpdir(),"setup-gh-release-")),g=p.join(x,m.name);console.log(`Downloading ${m.name}...`),await W(m.browser_download_url,g,l);let w=p.join(x,"extract");console.log(`Extracting ${m.name}...`),await L(g,w);let A=s||h,R;A.startsWith("~")?R=new RegExp(A.substring(1),"i"):R=A;let T=k(w,R,i,console.log);if(!T)throw new Error(`Could not find binary "${A}" in the extracted asset.`);let u;if(t["install-path"])u=p.resolve(t["install-path"]);else if(process.getuid&&process.getuid()===0)u="/usr/local/bin";else{let z=p.join(P.homedir(),"bin");c.existsSync(z)?u=z:u="/usr/local/bin"}c.existsSync(u)||c.mkdirSync(u,{recursive:!0});let I=p.basename(T),v=p.join(u,I);console.log(`Installing ${I} to ${v}...`),c.copyFileSync(T,v),process.platform!=="win32"&&c.chmodSync(v,"755"),c.rmSync(x,{recursive:!0,force:!0}),console.log("Installation successful!")}catch(a){console.error(`Error: ${a.message}`),process.exit(1)}}K(); `),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();

View File

@@ -4,12 +4,15 @@
"description": "A GitHub Action and CLI tool to download and install binaries from GitHub releases", "description": "A GitHub Action and CLI tool to download and install binaries from GitHub releases",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
"install-github-release": "dist/cli.js" "install-github-release": "dist/cli.js",
"check-github-token": "dist/check-token.js"
}, },
"scripts": { "scripts": {
"build:action": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js --minify", "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:cli": "esbuild src/cli.ts --bundle --platform=node --target=node24 --outfile=dist/cli.js --minify --banner:js=\"#!/usr/bin/env node\"",
"build": "npm run build:action && npm run build:cli", "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": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'", "format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",

24
src/check-token-action.ts Normal file
View File

@@ -0,0 +1,24 @@
import * as core from '@actions/core';
import { fetchLatestRelease } from './core/downloader';
async function run() {
try {
const repository = core.getInput('repository') || 'actions/checkout';
const token = core.getInput('token') || process.env.GITHUB_TOKEN;
if (!token) {
core.setFailed('No GitHub token provided as an input or found in GITHUB_TOKEN environment variable.');
return;
}
core.info(`Verifying GitHub token using repository ${repository}...`);
// Attempt to list latest release of the specified repository as a test
await fetchLatestRelease(repository, token);
core.info('Success: The provided GitHub token is valid and has sufficient permissions to access the repository.');
} catch (error: any) {
core.setFailed(`GitHub token verification failed. Reason: ${error.message}`);
}
}
run();

29
src/check-token.ts Normal file
View File

@@ -0,0 +1,29 @@
import { parseArgs } from 'util';
import { fetchLatestRelease } from './core/downloader';
async function run() {
const { positionals } = 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...');
// Attempt to list latest release of actions/checkout as a test
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: any) {
console.error('\x1b[31mError: GitHub token verification failed.\x1b[0m');
console.error(`Reason: ${error.message}`);
process.exit(1);
}
}
run();

View File

@@ -93,6 +93,10 @@ Options:
if (values['install-path']) { if (values['install-path']) {
installDir = path.resolve(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 { } else {
const isRoot = process.getuid && process.getuid() === 0; const isRoot = process.getuid && process.getuid() === 0;
@@ -103,11 +107,11 @@ Options:
if (fs.existsSync(homeBin)) { if (fs.existsSync(homeBin)) {
installDir = homeBin; installDir = homeBin;
} else { } else {
// Fallback or error? Let's use a local bin if possible or /usr/local/bin (might fail)
installDir = '/usr/local/bin'; installDir = '/usr/local/bin';
} }
} }
} }
}
if (!fs.existsSync(installDir)) { if (!fs.existsSync(installDir)) {
fs.mkdirSync(installDir, { recursive: true }); fs.mkdirSync(installDir, { recursive: true });
@@ -117,7 +121,17 @@ Options:
const destPath = path.join(installDir, finalName); const destPath = path.join(installDir, finalName);
console.log(`Installing ${finalName} to ${destPath}...`); console.log(`Installing ${finalName} to ${destPath}...`);
try {
fs.copyFileSync(binaryPath, destPath); fs.copyFileSync(binaryPath, destPath);
} catch (err: any) {
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') { if (process.platform !== 'win32') {
fs.chmodSync(destPath, '755'); fs.chmodSync(destPath, '755');

View File

@@ -18,11 +18,23 @@ export async function extractAsset(filePath: string, destDir: string): Promise<v
} }
} else if (name.endsWith('.zip')) { } else if (name.endsWith('.zip')) {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const command = `Expand-Archive -Path "${filePath}" -DestinationPath "${destDir}" -Force`; // Modern Windows 10/11 has tar that handles zip
const result = spawnSync('powershell', ['-Command', command]); const tarResult = spawnSync('tar', ['-xf', filePath, '-C', destDir]);
if (result.status !== 0) { if (tarResult.status === 0) return;
throw new Error(`powershell Expand-Archive failed with status ${result.status}: ${result.stderr.toString()}`);
// Fallback: Use .NET ZipFile class to bypass PowerShell module trust issues (Microsoft.PowerShell.Archive)
// We escape single quotes for PowerShell.
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}')`;
// Try pwsh (PowerShell 7) then powershell (Windows PowerShell)
for (const shell of ['pwsh', 'powershell']) {
const result = 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 { } else {
const result = spawnSync('unzip', ['-q', filePath, '-d', destDir]); const result = spawnSync('unzip', ['-q', filePath, '-d', destDir]);
if (result.status !== 0) { if (result.status !== 0) {