From 5441d464d1086d1f5e9fbcaa3874825d9b7cbc46 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:37:30 -0500 Subject: [PATCH 1/2] fix(retry): enforce timeout_minutes without coreutils timeout Co-Authored-By: Claude Sonnet 5.5 --- retry/action.yml | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/retry/action.yml b/retry/action.yml index 681363e..1ea6270 100644 --- a/retry/action.yml +++ b/retry/action.yml @@ -15,8 +15,8 @@ description: Run a command, retrying it on failure or timeout # The retry loop always runs in bash (Git Bash on Windows runners), so set # `shell: pwsh` (PowerShell 7.3 or newer) to have each attempt run `command` as # PowerShell instead. -# Each attempt is killed after `timeout_minutes`, which needs `timeout` or -# `gtimeout` on the PATH (macOS runners without coreutils run without one). +# Each attempt is killed after `timeout_minutes`, using `timeout` or `gtimeout` +# when installed and a built-in watchdog otherwise (hosted macOS runners). inputs: command: @@ -74,19 +74,33 @@ runs: exit 1 fi - limit=() - if command -v timeout >/dev/null 2>&1; then - limit=(timeout --kill-after=30 "$((RETRY_TIMEOUT_MINUTES * 60))") - elif command -v gtimeout >/dev/null 2>&1; then - limit=(gtimeout --kill-after=30 "$((RETRY_TIMEOUT_MINUTES * 60))") - else - echo "::warning::timeout is not available; attempts will not be time limited" - fi + # Run "$@", killing it after the timeout. Prefer coreutils timeout, and + # fall back to a watchdog where it is missing (hosted macOS runners). + seconds=$((RETRY_TIMEOUT_MINUTES * 60)) + run_limited() { + if [ "$seconds" -eq 0 ]; then + "$@" + elif command -v timeout >/dev/null 2>&1; then + timeout --kill-after=30 "$seconds" "$@" + elif command -v gtimeout >/dev/null 2>&1; then + gtimeout --kill-after=30 "$seconds" "$@" + else + "$@" & + local pid=$! + (sleep "$seconds"; kill -TERM "$pid"; sleep 30; kill -KILL "$pid") >/dev/null 2>&1 & + local watchdog=$! + local rc=0 + wait "$pid" || rc=$? + kill "$watchdog" >/dev/null 2>&1 || true + wait "$watchdog" >/dev/null 2>&1 || true + return "$rc" + fi + } attempt=1 while true; do status=0 - "${limit[@]}" "${run[@]}" || status=$? + run_limited "${run[@]}" || status=$? if [ "$status" -eq 0 ]; then exit 0 fi From 6a3ad2fe21679495ba476ab6e8741b1f1419aff1 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:43:47 -0500 Subject: [PATCH 2/2] fix(retry): kill the attempt's process group on watchdog timeout Co-Authored-By: Claude Sonnet 5.5 --- retry/action.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/retry/action.yml b/retry/action.yml index 1ea6270..a1dc84e 100644 --- a/retry/action.yml +++ b/retry/action.yml @@ -85,9 +85,13 @@ runs: elif command -v gtimeout >/dev/null 2>&1; then gtimeout --kill-after=30 "$seconds" "$@" else + # Job control gives the attempt its own process group, so the + # watchdog can signal its descendants like timeout does. + set -m "$@" & local pid=$! - (sleep "$seconds"; kill -TERM "$pid"; sleep 30; kill -KILL "$pid") >/dev/null 2>&1 & + set +m + (sleep "$seconds"; kill -TERM -- "-$pid"; sleep 30; kill -KILL -- "-$pid") >/dev/null 2>&1 & local watchdog=$! local rc=0 wait "$pid" || rc=$?