Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ These parameters control the commands executed on the remote host and related be
| 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. | |
| retry_attempts | Number of retries after an SSH connection failure | 0 |
| retry_delay | Delay between retry attempts | 0s |

---

Expand Down Expand Up @@ -309,6 +311,22 @@ This section covers common and advanced usage patterns, including multi-host, pr
script_path: scripts/script.sh
```

### Retry SSH connection failures

Use `retry_attempts` to retry transient SSH connection failures, such as `dial tcp ...: i/o timeout`. Retries are disabled by default. Remote command failures are not retried.

```yaml
- name: Retry transient SSH connection failures
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
retry_attempts: 3
retry_delay: 5s
script: whoami
```

### Multiple hosts

```diff
Expand Down
18 changes: 18 additions & 0 deletions README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
| curl_insecure | 允许 curl 连接无证书的 SSL 站点 | false |
| capture_stdout | 捕获命令的标准输出作为 Action 输出 | false |
| version | drone-ssh 二进制版本,未指定时使用最新版本 | |
| retry_attempts | SSH 连接失败后的重试次数 | 0 |
| retry_delay | 每次重试之间的延迟 | 0s |

---

Expand Down Expand Up @@ -309,6 +311,22 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com"
script_path: scripts/script.sh
```

### 重试 SSH 连接失败

使用 `retry_attempts` 重试临时 SSH 连接失败,例如 `dial tcp ...: i/o timeout`。默认不重试。远程命令失败不会重试。

```yaml
- name: 重试临时 SSH 连接失败
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
retry_attempts: 3
retry_delay: 5s
script: whoami
```

### 多主机

```diff
Expand Down
18 changes: 18 additions & 0 deletions README.zh-tw.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
| curl_insecure | 允許 curl 連線無憑證的 SSL 網站 | false |
| capture_stdout | 擷取指令的標準輸出作為 Action 輸出 | false |
| version | drone-ssh 執行檔版本,未指定時使用最新版本 | |
| retry_attempts | SSH 連線失敗後的重試次數 | 0 |
| retry_delay | 每次重試之間的延遲 | 0s |

---

Expand Down Expand Up @@ -309,6 +311,22 @@ ssh-keygen -t ed25519 -a 200 -C "your_email@example.com"
script_path: scripts/script.sh
```

### 重試 SSH 連線失敗

使用 `retry_attempts` 重試暫時性 SSH 連線失敗,例如 `dial tcp ...: i/o timeout`。預設不重試。遠端指令失敗不會重試。

```yaml
- name: 重試暫時性 SSH 連線失敗
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
retry_attempts: 3
retry_delay: 5s
script: whoami
```

### 多主機

```diff
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ inputs:
version:
description: |
The version of drone-ssh to use.
retry_attempts:
description: "Number of retry attempts after the initial SSH command fails."
default: "0"
retry_delay:
description: "Delay between retry attempts"
default: "0s"

outputs:
stdout:
Expand Down Expand Up @@ -138,6 +144,8 @@ runs:
INPUT_SYNC: ${{ inputs.sync }}
INPUT_CAPTURE_STDOUT: ${{ inputs.capture_stdout }}
INPUT_CURL_INSECURE: ${{ inputs.curl_insecure }}
INPUT_RETRY_ATTEMPTS: ${{ inputs.retry_attempts }}
INPUT_RETRY_DELAY: ${{ inputs.retry_delay }}
DRONE_SSH_VERSION: ${{ inputs.version }}

branding:
Expand Down
106 changes: 99 additions & 7 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,104 @@ function log_error() {
exit "$2"
}

function validate_non_negative_integer() {
local name="$1"
local value="$2"

if [[ ! "${value}" =~ ^[0-9]+$ ]]; then
log_error "${name} must be a non-negative integer, got: ${value}" 1
fi
}

function is_retryable_connection_failure() {
local output="$1"

[[ "${output}" =~ dial[[:space:]]+tcp ]] || \
[[ "${output}" =~ i/o[[:space:]]+timeout ]] || \
[[ "${output}" =~ connection[[:space:]]+refused ]] || \
[[ "${output}" =~ connection[[:space:]]+reset ]] || \
[[ "${output}" =~ connection[[:space:]]+timed[[:space:]]+out ]] || \
[[ "${output}" =~ no[[:space:]]+route[[:space:]]+to[[:space:]]+host ]] || \
[[ "${output}" =~ network[[:space:]]+is[[:space:]]+unreachable ]] || \
[[ "${output}" =~ no[[:space:]]+such[[:space:]]+host ]] || \
[[ "${output}" =~ ssh:[[:space:]]+handshake[[:space:]]+failed ]]
}

function run_ssh_command() {
local status=0
local stderr=""

if [[ "${INPUT_CAPTURE_STDOUT}" == 'true' ]]; then
echo 'stdout<<EOF' >> "${GITHUB_OUTPUT}"
exec 3>&1
set +e
stderr="$(
{
"${TARGET}" "$@" 1> >(tee -a "${GITHUB_OUTPUT}" >&3)
} 2>&1 | tee /dev/stderr
)"
status="${PIPESTATUS[0]}"
set -e
exec 3>&-
echo 'EOF' >> "${GITHUB_OUTPUT}"
RUN_SSH_OUTPUT="${stderr}"
return "${status}"
else
exec 3>&1
set +e
stderr="$(
{
"${TARGET}" "$@" 1>&3
} 2>&1 | tee /dev/stderr
)"
status="${PIPESTATUS[0]}"
set -e
exec 3>&-
RUN_SSH_OUTPUT="${stderr}"
return "${status}"
fi
}

function run_ssh_command_with_retry() {
local retries="${INPUT_RETRY_ATTEMPTS:-0}"
local delay="${INPUT_RETRY_DELAY:-0}"
local max_attempts
local attempt=1
local status=0
RUN_SSH_OUTPUT=""

validate_non_negative_integer "retry_attempts" "${retries}"

max_attempts=$((retries + 1))

while true; do
if (( retries > 0 )); then
echo "SSH command attempt ${attempt}/${max_attempts}"
fi

run_ssh_command "$@"
status="$?"

if (( status == 0 )); then
return 0
fi

if (( attempt >= max_attempts )); then
return "${status}"
fi

if ! is_retryable_connection_failure "${RUN_SSH_OUTPUT}"; then
return "${status}"
fi

attempt=$((attempt + 1))

if [[ "${delay}" != "0" && "${delay}" != "0s" ]]; then
sleep "${delay}"
fi
done
}

function detect_client_info() {
CLIENT_PLATFORM="${SSH_CLIENT_OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
CLIENT_ARCH="${SSH_CLIENT_ARCH:-$(uname -m)}"
Expand Down Expand Up @@ -70,10 +168,4 @@ 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
echo 'stdout<<EOF' >> "${GITHUB_OUTPUT}"
"${TARGET}" "$@" | tee -a "${GITHUB_OUTPUT}"
echo 'EOF' >> "${GITHUB_OUTPUT}"
else
"${TARGET}" "$@"
fi
run_ssh_command_with_retry "$@"