diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 7f1ae24..c39f21f 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -3,7 +3,7 @@ name: Goreleaser on: push: tags: - - "*" + - "v[0-9]+.[0-9]+.[0-9]+" permissions: contents: write diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 0000000..0af4890 --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,51 @@ +name: Trivy Security Scan + +on: + schedule: + - cron: '0 0 * * *' + pull_request: + branches: + - master + push: + branches: + - master + workflow_dispatch: + +jobs: + trivy-scan: + name: Trivy Security Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'vuln,secret,misconfig' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH,MEDIUM' + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + - name: Run Trivy vulnerability scanner (table format) + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'vuln,secret,misconfig' + format: 'table' + severity: 'CRITICAL,HIGH,MEDIUM' + exit-code: '1' diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d4f115f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,178 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a GitHub Action for executing remote SSH commands. Built using a composite action pattern, it downloads and runs the [drone-ssh](https://github.com/appleboy/drone-ssh) binary (written in Go) to execute SSH commands on remote hosts. + +**Key characteristics:** + +- No local compilation required - downloads pre-built binaries from drone-ssh releases +- Shell-based composite action using `entrypoint.sh` +- Supports password and SSH key authentication, proxies, multiple hosts, and environment variable passing +- All input parameters are passed via `INPUT_*` environment variables + +## Architecture + +### Execution Flow + +1. **action.yml** - GitHub Actions composite action definition + - Defines all input parameters and their descriptions + - Sets up environment variables with `INPUT_*` prefix + - Calls `entrypoint.sh` + +2. **entrypoint.sh** - Main entry point + - Detects platform (Linux/Darwin/Windows) and architecture (amd64/arm64) + - Downloads appropriate `drone-ssh` binary from GitHub releases + - Executes the binary with all environment variables + - Handles `capture_stdout` for output capture + +3. **drone-ssh binary** - The actual SSH client (separate Go project) + - Performs SSH connection and command execution + - Not part of this repository + +### Key Files + +- `action.yml` - Action metadata and input/output definitions +- `entrypoint.sh` - Platform detection, binary download, and execution +- `testdata/` - Test scripts and SSH keys for CI workflows +- `.github/workflows/main.yml` - Comprehensive test suite using Docker containers (tests `./` local action) +- `.github/workflows/stable.yml` - Tests against published `appleboy/ssh-action@v1` tag +- `.github/workflows/trivy-scan.yml` - Automated security scanning for vulnerabilities and misconfigurations + +## Testing + +### Running Tests + +Tests run automatically via GitHub Actions workflows. The test suite uses Docker containers running OpenSSH servers: + +```bash +# Tests run automatically on push via .github/workflows/main.yml +# Tests create Docker containers with openssh-server and test various scenarios +``` + +### Test Categories (from main.yml) + +The test workflow covers: + +- **default-user-name-password**: Basic password authentication +- **check-ssh-key**: RSA key authentication, key priority over password +- **support-key-passphrase**: Encrypted SSH keys +- **multiple-server**: Multiple hosts with different ports +- **support-ed25519-key**: ED25519 key format +- **testing-with-env**: Environment variable passing, `allenvs`, custom formats +- **testing06**: IPv6 connectivity +- **testing07**: Special characters in passwords +- **testing-capturing-output**: Output capture functionality +- **testing-script-stop**: Script error handling with `set -e` +- **testing-script-error**: Error propagation + +### Testing Locally + +Since this action downloads binaries, local testing should: + +1. Create a test SSH server (Docker or VM) +2. Test `entrypoint.sh` directly with appropriate `INPUT_*` environment variables + +Example: + +```bash +export INPUT_HOST="192.168.1.100" +export INPUT_USERNAME="testuser" +export INPUT_PASSWORD="testpass" +export INPUT_PORT="22" +export INPUT_SCRIPT="whoami" +export GITHUB_ACTION_PATH="$(pwd)" +./entrypoint.sh +``` + +## Important Patterns + +### Script Execution + +Users can provide scripts in two ways: + +- `script`: Inline commands (via `INPUT_SCRIPT`) +- `script_path`: Path to a file in the repository (maps to `INPUT_SCRIPT_FILE` env var - note the naming difference) + +### Error Handling + +To stop execution on first error (mimics removed `script_stop` option), users should add `set -e` to their scripts: + +```yaml +script: | + #!/usr/bin/env bash + set -e + command1 + command2 +``` + +### Environment Variables + +The action passes GitHub Action inputs as environment variables with `INPUT_*` prefix. The drone-ssh binary reads these to configure SSH behavior. + +Special handling: + +- `envs`: Comma-separated list of environment variables to pass to remote script +- `allenvs`: Pass all `GITHUB_*` and `INPUT_*` variables +- `envs_format`: Custom format for environment variable export (e.g., `export TEST_{NAME}={VALUE}`) + +### Multiple Hosts + +- Comma-separated hosts: `"host1,host2"` (executes in parallel by default) +- With custom ports: `"host1:2222,host2:5678"` +- Synchronous execution: Set `sync: true` + +## Common Issues + +### Command Not Found + +Non-interactive shells may skip `.bashrc`/`.bash_profile`. See README section "Command not found" for details. Solutions: + +- Use absolute paths in commands +- Comment out early return in `/etc/bash.bashrc` + +### OpenSSH Compatibility + +Ubuntu 20.04+ may require enabling `ssh-rsa` algorithm in sshd_config: + +```txt +CASignatureAlgorithms +ssh-rsa +``` + +Or use ED25519 keys instead (preferred). + +## Development Guidelines + +### Adding New Parameters + +1. Add input definition to `action.yml` with description +2. Add corresponding `INPUT_*` environment variable mapping in `action.yml` runs.steps +3. Update README.md parameter tables +4. The drone-ssh binary handles the actual parameter logic (separate repo) + +### Documentation + +- Keep parameter tables in README.md synchronized with `action.yml` +- Maintain Chinese translations (README.zh-cn.md, README.zh-tw.md) +- Use clear examples for each feature + +### Version Management + +The action pins to specific drone-ssh versions via: + +- Default: `DRONE_SSH_VERSION="1.8.2"` in `entrypoint.sh` +- Override: Users can specify `version` input parameter + +Update the default version when new drone-ssh releases are available. + +## Release Process + +This action uses semantic versioning with major version tags: + +- Tags: `v1.0.0`, `v1.0.1`, etc. +- Major version tag: `v1` (points to latest v1.x.x) +- Users reference: `uses: appleboy/ssh-action@v1` + +GoReleaser config (`.goreleaser.yaml`) is present but set to `skip: true` since this action doesn't build Go code - it downloads pre-built binaries. diff --git a/README.md b/README.md index 0a89eed..dc7da87 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ English | [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) - [🔌 Connection Settings](#-connection-settings) - [🛠️ SSH Command Settings](#️-ssh-command-settings) - [🌐 Proxy Settings](#-proxy-settings) + - [📤 Output Variables](#-output-variables) - [⚡ Quick Start](#-quick-start) - [🔑 SSH Key Setup \& OpenSSH Compatibility](#-ssh-key-setup--openssh-compatibility) - [Setting Up SSH Keys](#setting-up-ssh-keys) @@ -26,6 +27,7 @@ English | [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) - [Multiple hosts with different ports](#multiple-hosts-with-different-ports) - [Synchronous execution on multiple hosts](#synchronous-execution-on-multiple-hosts) - [Pass environment variables to shell script](#pass-environment-variables-to-shell-script) + - [Capturing command output](#capturing-command-output) - [🌐 Proxy \& Jump Host Usage](#-proxy--jump-host-usage) - [🛡️ Security Best Practices](#️-security-best-practices) - [Protecting Your Private Key](#protecting-your-private-key) @@ -46,6 +48,7 @@ Built with [Golang](https://go.dev) and [drone-ssh](https://github.com/appleboy/ ![ssh workflow](./images/ssh-workflow.png) [![testing main branch](https://github.com/appleboy/ssh-action/actions/workflows/main.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/main.yml) +[![Trivy Security Scan](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml) --- @@ -84,7 +87,7 @@ These parameters control the commands executed on the remote host and related be | Parameter | Description | Default | | --------------- | --------------------------------------------------------------------------------- | ------- | | script | Commands to execute remotely | | -| script_path | Path to a file containing commands to execute | | +| script_path | Path to a file in the repository containing commands to execute remotely | | | envs | Environment variables to pass to the shell script | | | envs_format | Flexible configuration for environment variable transfer | | | allenvs | Pass all environment variables with `GITHUB_` and `INPUT_` prefixes to the script | false | @@ -92,6 +95,7 @@ These parameters control the commands executed on the remote host and related be | debug | Enable debug mode | false | | request_pty | Request a pseudo-terminal from the server | false | | curl_insecure | Allow curl to connect to SSL sites without certificates | false | +| capture_stdout | Capture standard output from commands as action output | false | | version | drone-ssh binary version. If not specified, the latest version will be used. | | --- @@ -119,6 +123,16 @@ These parameters control the use of a proxy (jump host) for connecting to your t --- +## 📤 Output Variables + +This action provides the following outputs that you can use in subsequent steps: + +| Output | Description | +| ------ | ----------------------------------------------------------------- | +| stdout | Standard output of the executed commands (requires `capture_stdout: true`) | + +--- + ## ⚡ Quick Start Run remote SSH commands in your workflow with minimal configuration: @@ -135,7 +149,7 @@ jobs: uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} - username: linuxserver.io + username: ${{ secrets.USERNAME }} password: ${{ secrets.PASSWORD }} port: ${{ secrets.PORT }} script: whoami @@ -147,7 +161,7 @@ jobs: ======CMD====== whoami ======END====== -linuxserver.io +out: your_username =============================================== ✅ Successfully executed commands to all hosts. =============================================== @@ -221,7 +235,7 @@ ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publ On Ubuntu 20.04+ you may need to explicitly allow the `ssh-rsa` algorithm. Add this to your OpenSSH daemon config (`/etc/ssh/sshd_config` or a drop-in under `/etc/ssh/sshd_config.d/`): -```bash +```text CASignatureAlgorithms +ssh-rsa ``` @@ -365,6 +379,28 @@ Default `port` is `22`. > _All environment variables in the `env` object must be strings. Using integers or other types may cause unexpected results._ +### Capturing command output + +You can capture the standard output of remote commands and use it in subsequent steps: + +```yaml +- name: Execute and capture output + id: ssh + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + port: ${{ secrets.PORT }} + capture_stdout: true + script: | + echo "Hello World" + hostname + +- name: Use captured output + run: echo "SSH output was ${{ steps.ssh.outputs.stdout }}" +``` + --- ## 🌐 Proxy & Jump Host Usage @@ -379,7 +415,7 @@ You can connect to remote hosts via a proxy (jump host) for advanced network top Example `~/.ssh/config`: -```bash +```text Host Jumphost HostName Jumphost User ubuntu diff --git a/README.zh-cn.md b/README.zh-cn.md index 6886c25..fdf99ce 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -11,6 +11,7 @@ - [🔌 连接设置](#-连接设置) - [🛠️ 指令设置](#️-指令设置) - [🌐 代理设置](#-代理设置) + - [📤 输出变量](#-输出变量) - [⚡ 快速开始](#-快速开始) - [🔑 SSH 密钥配置与 OpenSSH 兼容性](#-ssh-密钥配置与-openssh-兼容性) - [配置 SSH 密钥](#配置-ssh-密钥) @@ -26,6 +27,7 @@ - [多主机不同端口](#多主机不同端口) - [多主机同步执行](#多主机同步执行) - [传递环境变量到 shell 脚本](#传递环境变量到-shell-脚本) + - [捕获命令输出](#捕获命令输出) - [🌐 代理与跳板机用法](#-代理与跳板机用法) - [🛡️ 安全最佳实践](#️-安全最佳实践) - [保护你的私钥](#保护你的私钥) @@ -46,6 +48,7 @@ ![ssh workflow](./images/ssh-workflow.png) [![testing main branch](https://github.com/appleboy/ssh-action/actions/workflows/main.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/main.yml) +[![Trivy Security Scan](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml) --- @@ -84,7 +87,7 @@ | 参数 | 描述 | 默认值 | | --------------- | ----------------------------------------------------- | ------ | | script | 远程执行的命令 | | -| script_path | 包含要执行命令的文件路径 | | +| script_path | 仓库中包含要远程执行命令的文件路径 | | | envs | 传递给 shell 脚本的环境变量 | | | envs_format | 环境变量传递的灵活配置 | | | allenvs | 传递所有带 `GITHUB_` 和 `INPUT_` 前缀的环境变量到脚本 | false | @@ -92,6 +95,7 @@ | debug | 启用调试模式 | false | | request_pty | 向服务器请求伪终端 | false | | curl_insecure | 允许 curl 连接无证书的 SSL 站点 | false | +| capture_stdout | 捕获命令的标准输出作为 Action 输出 | false | | version | drone-ssh 二进制版本,未指定时使用最新版本 | | --- @@ -119,6 +123,16 @@ --- +## 📤 输出变量 + +本 Action 提供以下输出,可在后续步骤中使用: + +| 输出 | 描述 | +| ------ | ----------------------------------------------------- | +| stdout | 执行命令的标准输出(需设置 `capture_stdout: true`) | + +--- + ## ⚡ 快速开始 只需简单配置,即可在工作流中执行远程 SSH 命令: @@ -135,7 +149,7 @@ jobs: uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} - username: linuxserver.io + username: ${{ secrets.USERNAME }} password: ${{ secrets.PASSWORD }} port: ${{ secrets.PORT }} script: whoami @@ -147,7 +161,7 @@ jobs: ======CMD====== whoami ======END====== -linuxserver.io +out: your_username =============================================== ✅ Successfully executed commands to all hosts. =============================================== @@ -221,7 +235,7 @@ ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publ 在 Ubuntu 20.04+,你可能需要显式允许 `ssh-rsa` 算法。请在 OpenSSH 配置文件(`/etc/ssh/sshd_config` 或 `/etc/ssh/sshd_config.d/` 下的 drop-in 文件)中添加: -```bash +```text CASignatureAlgorithms +ssh-rsa ``` @@ -365,6 +379,28 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com" > _`env` 对象中的所有环境变量必须为字符串。传递整数或其他类型可能导致意外结果。_ +### 捕获命令输出 + +你可以捕获远程命令的标准输出,并在后续步骤中使用: + +```yaml +- name: 执行并捕获输出 + id: ssh + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + port: ${{ secrets.PORT }} + capture_stdout: true + script: | + echo "Hello World" + hostname + +- name: 使用捕获的输出 + run: echo "SSH 输出为 ${{ steps.ssh.outputs.stdout }}" +``` + --- ## 🌐 代理与跳板机用法 @@ -379,7 +415,7 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com" 示例 `~/.ssh/config`: -```bash +```text Host Jumphost HostName Jumphost User ubuntu diff --git a/README.zh-tw.md b/README.zh-tw.md index 4634ff5..a79c1fd 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -11,6 +11,7 @@ - [🔌 連線設定](#-連線設定) - [🛠️ 指令設定](#️-指令設定) - [🌐 代理設定](#-代理設定) + - [📤 輸出變數](#-輸出變數) - [⚡ 快速開始](#-快速開始) - [🔑 SSH 金鑰設定與 OpenSSH 相容性](#-ssh-金鑰設定與-openssh-相容性) - [設定 SSH 金鑰](#設定-ssh-金鑰) @@ -26,6 +27,7 @@ - [多主機不同埠號](#多主機不同埠號) - [多主機同步執行](#多主機同步執行) - [傳遞環境變數到 shell 腳本](#傳遞環境變數到-shell-腳本) + - [擷取指令輸出](#擷取指令輸出) - [🌐 代理與跳板機用法](#-代理與跳板機用法) - [🛡️ 安全最佳實踐](#️-安全最佳實踐) - [保護你的私鑰](#保護你的私鑰) @@ -46,6 +48,7 @@ ![ssh workflow](./images/ssh-workflow.png) [![testing main branch](https://github.com/appleboy/ssh-action/actions/workflows/main.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/main.yml) +[![Trivy Security Scan](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/appleboy/ssh-action/actions/workflows/trivy-scan.yml) --- @@ -84,7 +87,7 @@ | 參數 | 說明 | 預設值 | | --------------- | ----------------------------------------------------- | ------ | | script | 遠端執行的指令 | | -| script_path | 包含要執行指令的檔案路徑 | | +| script_path | 儲存庫中包含要遠端執行指令的檔案路徑 | | | envs | 傳遞給 shell 腳本的環境變數 | | | envs_format | 環境變數傳遞的彈性設定 | | | allenvs | 傳遞所有帶 `GITHUB_` 和 `INPUT_` 前綴的環境變數到腳本 | false | @@ -92,6 +95,7 @@ | debug | 啟用除錯模式 | false | | request_pty | 向伺服器請求偽終端 | false | | curl_insecure | 允許 curl 連線無憑證的 SSL 網站 | false | +| capture_stdout | 擷取指令的標準輸出作為 Action 輸出 | false | | version | drone-ssh 執行檔版本,未指定時使用最新版本 | | --- @@ -119,6 +123,16 @@ --- +## 📤 輸出變數 + +本 Action 提供以下輸出,可在後續步驟中使用: + +| 輸出 | 說明 | +| ------ | ----------------------------------------------------- | +| stdout | 執行指令的標準輸出(需設定 `capture_stdout: true`) | + +--- + ## ⚡ 快速開始 只需簡單設定,即可在工作流程中執行遠端 SSH 指令: @@ -135,7 +149,7 @@ jobs: uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} - username: linuxserver.io + username: ${{ secrets.USERNAME }} password: ${{ secrets.PASSWORD }} port: ${{ secrets.PORT }} script: whoami @@ -147,7 +161,7 @@ jobs: ======CMD====== whoami ======END====== -linuxserver.io +out: your_username =============================================== ✅ Successfully executed commands to all hosts. =============================================== @@ -221,7 +235,7 @@ ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publ 在 Ubuntu 20.04+,你可能需明確允許 `ssh-rsa` 演算法。請於 OpenSSH 設定檔(`/etc/ssh/sshd_config` 或 `/etc/ssh/sshd_config.d/` 下的 drop-in 檔案)加入: -```bash +```text CASignatureAlgorithms +ssh-rsa ``` @@ -365,6 +379,28 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com" > _`env` 物件中的所有環境變數必須為字串。傳遞整數或其他型別可能導致非預期結果。_ +### 擷取指令輸出 + +你可以擷取遠端指令的標準輸出,並在後續步驟中使用: + +```yaml +- name: 執行並擷取輸出 + id: ssh + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + port: ${{ secrets.PORT }} + capture_stdout: true + script: | + echo "Hello World" + hostname + +- name: 使用擷取的輸出 + run: echo "SSH 輸出為 ${{ steps.ssh.outputs.stdout }}" +``` + --- ## 🌐 代理與跳板機用法 @@ -379,7 +415,7 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com" 範例 `~/.ssh/config`: -```bash +```text Host Jumphost HostName Jumphost User ubuntu diff --git a/entrypoint.sh b/entrypoint.sh index 1eb7733..d5d56b6 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,7 +6,14 @@ export GITHUB="true" GITHUB_ACTION_PATH="${GITHUB_ACTION_PATH%/}" DRONE_SSH_RELEASE_URL="${DRONE_SSH_RELEASE_URL:-https://github.com/appleboy/drone-ssh/releases/download}" -DRONE_SSH_VERSION="${DRONE_SSH_VERSION:-1.8.1}" +DRONE_SSH_VERSION="${DRONE_SSH_VERSION:-1.8.2}" + +# Error codes +readonly ERR_UNKNOWN_PLATFORM=2 +readonly ERR_UNKNOWN_ARCH=3 +readonly ERR_DOWNLOAD_FAILED=4 +readonly ERR_INVALID_BINARY=5 +readonly ERR_VERSION_CHECK_FAILED=6 function log_error() { echo "$1" >&2 @@ -19,13 +26,13 @@ function detect_client_info() { case "${CLIENT_PLATFORM}" in darwin | linux | windows) ;; - *) log_error "Unknown or unsupported platform: ${CLIENT_PLATFORM}. Supported platforms are Linux, Darwin, and Windows." 2 ;; + *) log_error "Unknown or unsupported platform: ${CLIENT_PLATFORM}. Supported platforms are Linux, Darwin, and Windows." "${ERR_UNKNOWN_PLATFORM}" ;; esac case "${CLIENT_ARCH}" in x86_64* | i?86_64* | amd64*) CLIENT_ARCH="amd64" ;; aarch64* | arm64*) CLIENT_ARCH="arm64" ;; - *) log_error "Unknown or unsupported architecture: ${CLIENT_ARCH}. Supported architectures are x86_64, i686, and arm64." 3 ;; + *) log_error "Unknown or unsupported architecture: ${CLIENT_ARCH}. Supported architectures are x86_64, i686, and arm64." "${ERR_UNKNOWN_ARCH}" ;; esac } @@ -33,17 +40,35 @@ detect_client_info DOWNLOAD_URL_PREFIX="${DRONE_SSH_RELEASE_URL}/v${DRONE_SSH_VERSION}" CLIENT_BINARY="drone-ssh-${DRONE_SSH_VERSION}-${CLIENT_PLATFORM}-${CLIENT_ARCH}" TARGET="${GITHUB_ACTION_PATH}/${CLIENT_BINARY}" -echo "Downloading ${CLIENT_BINARY} from ${DOWNLOAD_URL_PREFIX}" -INSECURE_OPTION="" -if [[ "${INPUT_CURL_INSECURE}" == 'true' ]]; then - INSECURE_OPTION="--insecure" + +# Check if binary already exists and is executable (caching) +if [[ -f "${TARGET}" ]] && [[ -x "${TARGET}" ]]; then + echo "Binary ${CLIENT_BINARY} already exists, skipping download" +else + echo "Downloading ${CLIENT_BINARY} from ${DOWNLOAD_URL_PREFIX}" + INSECURE_OPTION="" + if [[ "${INPUT_CURL_INSECURE}" == 'true' ]]; then + INSECURE_OPTION="--insecure" + fi + + # Download with better error handling + if ! curl -fsSL --retry 5 --keepalive-time 2 --location ${INSECURE_OPTION} \ + "${DOWNLOAD_URL_PREFIX}/${CLIENT_BINARY}" -o "${TARGET}"; then + log_error "Failed to download ${CLIENT_BINARY} from ${DOWNLOAD_URL_PREFIX}. Please check the URL and your network connection." "${ERR_DOWNLOAD_FAILED}" + fi + + # Validate downloaded file + if [[ ! -f "${TARGET}" ]] || [[ ! -s "${TARGET}" ]]; then + log_error "Downloaded file is missing or empty: ${TARGET}" "${ERR_INVALID_BINARY}" + fi + + chmod +x "${TARGET}" fi -curl -fsSL --retry 5 --keepalive-time 2 ${INSECURE_OPTION} "${DOWNLOAD_URL_PREFIX}/${CLIENT_BINARY}" -o "${TARGET}" -chmod +x "${TARGET}" - echo "======= CLI Version Information =======" -"${TARGET}" --version +if ! "${TARGET}" --version; then + log_error "Failed to execute ${TARGET} --version. The binary may be corrupted." "${ERR_VERSION_CHECK_FAILED}" +fi echo "=======================================" if [[ "${INPUT_CAPTURE_STDOUT}" == 'true' ]]; then {