Files
actions/retry/action.yml

189 lines
6.7 KiB
YAML

# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Example usage:
#
# Basic usage (will retry failed step 3 times):
# steps:
# - uses: ultralytics/actions/retry@main
# with:
# run: python train.py
#
# Advanced usage:
# steps:
# - uses: ultralytics/actions/retry@main
# with:
# run: |
# python setup.py install
# pytest tests/
# retries: 2 # Retry twice after initial attempt (3 total runs)
# timeout_minutes: 30 # Total timeout for all attempts combined
# retry_delay_seconds: 10 # Base delay between retries in seconds
# backoff: exponential # exponential (10s, 20s, 40s, ...) or fixed
# jitter: true # Randomize delay to 80-120% to avoid thundering herd
# shell: bash # Use python or bash shell
name: "Step-Level Retry"
description: "Retries a step while preserving its full context"
inputs:
timeout_minutes:
description: "Maximum total time in minutes for all attempts (checked between attempts; does not interrupt a running attempt)"
required: false
default: "360"
retries:
description: "Number of retry attempts after initial run"
required: false
default: "3"
retry_delay_seconds:
description: "Base delay between retries in seconds"
required: false
default: "10"
backoff:
description: "Backoff strategy: exponential (base * 2^n) or fixed. Delays are bounded by timeout_minutes."
required: false
default: "exponential"
jitter:
description: "Randomize delay to 80-120% of computed value to spread retries and avoid thundering herd"
required: false
default: "true"
run:
description: "Command to run"
required: true
shell:
description: "Shell to use (bash or python)"
required: false
default: "bash"
runs:
using: "composite"
steps:
- name: Execute with retry
shell: bash
env:
# Note: Workflow env vars are automatically inherited. Only RETRY_COMMANDS is added here.
RETRY_COMMANDS: ${{ inputs.run }}
run: |
set +e # Don't exit on error - we handle errors manually
start_time=$(date +%s)
timeout_seconds=$(( ${{ inputs.timeout_minutes }} * 60 ))
attempt=1
max_attempts=$(( 1 + ${{ inputs.retries }} )) # Initial run + retries
retries=${{ inputs.retries }}
retry_delay=${{ inputs.retry_delay_seconds }}
backoff="${{ inputs.backoff }}"
jitter="${{ inputs.jitter }}"
timeout_mins=${{ inputs.timeout_minutes }}
shell_type="${{ inputs.shell }}"
exit_code=0
# Validate backoff strategy
if [ "$backoff" != "exponential" ] && [ "$backoff" != "fixed" ]; then
echo "::error::Invalid backoff '$backoff'; must be 'exponential' or 'fixed'"
exit 1
fi
# Validate numeric inputs are non-negative integers
case "$retries$retry_delay$timeout_mins" in
''|*[!0-9]*) echo "::error::retries, retry_delay_seconds, timeout_minutes must be non-negative integers"; exit 1 ;;
esac
# Create temporary script file with appropriate extension
if [ "$shell_type" = "python" ]; then
if ! TEMP_SCRIPT=$(mktemp --suffix=.py 2>/dev/null); then
# Fallback: create temp file then rename it
if ! TEMP_SCRIPT=$(mktemp 2>/dev/null); then
echo "::error::Failed to create temporary file"
exit 1
fi
if ! mv "$TEMP_SCRIPT" "${TEMP_SCRIPT}.py"; then
rm -f "$TEMP_SCRIPT"
echo "::error::Failed to rename temporary file"
exit 1
fi
TEMP_SCRIPT="${TEMP_SCRIPT}.py"
fi
else
if ! TEMP_SCRIPT=$(mktemp 2>/dev/null); then
echo "::error::Failed to create temporary file"
exit 1
fi
fi
# Ensure cleanup on exit
trap 'rm -f "$TEMP_SCRIPT"' EXIT
# Write the user's commands to the temp script
printf '%s\n' "$RETRY_COMMANDS" > "$TEMP_SCRIPT"
# Validate script was written
if [ ! -s "$TEMP_SCRIPT" ]; then
echo "::error::No commands provided to execute"
exit 1
fi
while [ "$attempt" -le "$max_attempts" ]; do
if [ "$attempt" -gt 1 ]; then
echo "::group::Retry $((attempt - 1)) of $retries"
fi
current_time=$(date +%s)
elapsed=$((current_time - start_time))
if [ "$elapsed" -gt "$timeout_seconds" ]; then
echo "::error::Step timed out after $timeout_mins minutes"
[ "$attempt" -gt 1 ] && echo "::endgroup::"
exit 1
fi
# Execute the script with appropriate interpreter
if [ "$shell_type" = "python" ]; then
python3 "$TEMP_SCRIPT"
else
bash -e "$TEMP_SCRIPT"
fi
exit_code=$?
if [ "$exit_code" -eq 0 ]; then
[ "$attempt" -gt 1 ] && echo "::endgroup::"
exit 0
fi
# Handle failure
if [ "$attempt" -gt 1 ]; then
echo "Retry $((attempt - 1)) failed with exit code $exit_code"
echo "::endgroup::"
else
echo "Initial attempt failed with exit code $exit_code"
fi
if [ "$attempt" -eq "$max_attempts" ]; then
echo "::error::Step failed after initial attempt and $((attempt - 1)) retries"
exit "$exit_code"
fi
# Compute delay for the upcoming retry
if [ "$backoff" = "exponential" ]; then
delay=$((retry_delay * (2 ** (attempt - 1))))
else
delay=$retry_delay
fi
# Apply jitter: random 80-120% of computed delay (±20%)
if [ "$jitter" = "true" ] && [ "$delay" -gt 0 ]; then
low=$((delay * 8 / 10))
span=$((delay * 4 / 10))
rand=$((RANDOM % (span + 1)))
delay=$((low + rand))
[ "$delay" -lt 1 ] && delay=1 # integer division floor; never collapse to 0
fi
# Cap sleep at remaining timeout budget; no point sleeping past deadline.
# Refresh timestamp — the attempt may have taken significant time.
remaining=$((timeout_seconds - ($(date +%s) - start_time)))
[ "$delay" -gt "$remaining" ] && delay=$remaining
[ "$delay" -lt 0 ] && delay=0
echo "Retrying in $delay seconds..."
sleep "$delay"
attempt=$((attempt + 1))
done