mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
feat: upgrade default server sizes, fix Fly.io agent installs, improve E2E tests (#1428)
- Upgrade default VM sizes across clouds for better agent performance: - Hetzner: cpx11 → cx23 (with cx22 fallback support for deprecated types) - DigitalOcean: s-2vcpu-2gb → s-2vcpu-4gb - Daytona: 2048MB → 4096MB memory - Oracle: VM.Standard.E2.1.Micro → VM.Standard.A1.Flex - OVH: d2-2 → d2-4 - Fix Fly.io agent failures: - Add Node.js + build-essential to wait_for_cloud_init (fixes npm-based agents) - Prepend PATH in interactive_session (fixes "source not found" errors) - Fix openclaw installs across clouds: use explicit PATH export instead of source - Fix DigitalOcean token validation (check "uuid" not "id") - Fix AWS cloud-init: chown .bashrc/.zshrc to ubuntu user - Improve Hetzner fallback: add "cheapest available" as last-resort fallback - Upgrade E2E tests: per-combo auto-fix, credential collection, robustness fixes Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
963144ecbd
commit
633ce8eaac
15 changed files with 290 additions and 136 deletions
|
|
@ -120,6 +120,7 @@ su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash'
|
|||
# Configure PATH
|
||||
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc
|
||||
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc
|
||||
chown ubuntu:ubuntu /home/ubuntu/.bashrc /home/ubuntu/.zshrc
|
||||
touch /home/ubuntu/.cloud-init-complete
|
||||
chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete
|
||||
CLOUD_INIT_EOF
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ echo ""
|
|||
AGENT_MODEL_PROMPT=1
|
||||
AGENT_MODEL_DEFAULT="openrouter/auto"
|
||||
|
||||
agent_install() { install_agent "openclaw" "source ~/.bashrc && bun install -g openclaw" cloud_run; }
|
||||
agent_install() { install_agent "openclaw" "export PATH=\$HOME/.bun/bin:\$PATH && bun install -g openclaw" cloud_run; }
|
||||
agent_env_vars() {
|
||||
generate_env_config \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
|
|
@ -25,7 +25,7 @@ agent_env_vars() {
|
|||
}
|
||||
agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; }
|
||||
agent_pre_launch() {
|
||||
cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
|
||||
cloud_run "export PATH=\$HOME/.bun/bin:\$PATH && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
|
||||
wait_for_openclaw_gateway cloud_run
|
||||
}
|
||||
agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; }
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ _is_snapshot_conflict() {
|
|||
_daytona_create_with_resources() {
|
||||
local name="${1}"
|
||||
local cpu="${DAYTONA_CPU:-2}"
|
||||
local memory="${DAYTONA_MEMORY:-2048}"
|
||||
local memory="${DAYTONA_MEMORY:-4096}"
|
||||
local disk="${DAYTONA_DISK:-5}"
|
||||
|
||||
# Validate numeric env vars to prevent command injection
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ do_api() {
|
|||
test_do_token() {
|
||||
local response
|
||||
response=$(do_api GET "/account")
|
||||
if [[ "$response" == *'"id"'* ]]; then
|
||||
if [[ "$response" == *'"uuid"'* ]]; then
|
||||
log_info "API token validated"
|
||||
return 0
|
||||
else
|
||||
|
|
@ -165,7 +165,7 @@ _do_check_create_error() {
|
|||
# Create a DigitalOcean droplet with cloud-init
|
||||
create_server() {
|
||||
local name="$1"
|
||||
local size="${DO_DROPLET_SIZE:-s-2vcpu-2gb}"
|
||||
local size="${DO_DROPLET_SIZE:-s-2vcpu-4gb}"
|
||||
local region="${DO_REGION:-nyc3}"
|
||||
local image="ubuntu-24-04-x64"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ echo ""
|
|||
AGENT_MODEL_PROMPT=1
|
||||
AGENT_MODEL_DEFAULT="openrouter/auto"
|
||||
|
||||
agent_install() { install_agent "openclaw" "source ~/.bashrc && bun install -g openclaw" cloud_run; }
|
||||
agent_install() { install_agent "openclaw" "export PATH=\$HOME/.bun/bin:\$PATH && bun install -g openclaw" cloud_run; }
|
||||
agent_env_vars() {
|
||||
generate_env_config \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
|
|
@ -25,7 +25,7 @@ agent_env_vars() {
|
|||
}
|
||||
agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; }
|
||||
agent_pre_launch() {
|
||||
cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
|
||||
cloud_run "source ~/.spawnrc 2>/dev/null; export PATH=\$HOME/.bun/bin:\$PATH && nohup openclaw gateway </dev/null > /tmp/openclaw-gateway.log 2>&1 & disown"
|
||||
wait_for_openclaw_gateway cloud_run
|
||||
}
|
||||
agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; }
|
||||
|
|
|
|||
|
|
@ -338,9 +338,13 @@ wait_for_cloud_init() {
|
|||
_fly_wait_for_ssh || return 1
|
||||
|
||||
log_step "Installing packages (this may take 1-2 minutes)..."
|
||||
run_server "apt-get update -y && apt-get install -y curl unzip git zsh python3 pip" 600 || {
|
||||
run_server "apt-get update -y && apt-get install -y curl unzip git zsh python3 python3-pip build-essential" 600 || {
|
||||
log_warn "Package install timed out or failed, retrying..."
|
||||
run_server "apt-get install -y curl unzip git zsh python3 pip" 300 || true
|
||||
run_server "apt-get install -y curl unzip git zsh python3 python3-pip build-essential" 300 || true
|
||||
}
|
||||
log_step "Installing Node.js..."
|
||||
run_server "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs" 120 || {
|
||||
log_warn "Node.js install failed, npm-based agents may not work"
|
||||
}
|
||||
log_step "Installing bun..."
|
||||
run_server "curl -fsSL https://bun.sh/install | bash" 120 || true
|
||||
|
|
@ -401,9 +405,12 @@ upload_file() {
|
|||
# Start an interactive SSH session on the Fly.io machine
|
||||
interactive_session() {
|
||||
local cmd="$1"
|
||||
# Wrap in bash -c with PATH prepended (same as run_server) so shell builtins
|
||||
# like "source" work — fly ssh console -C execs directly, not via a shell.
|
||||
local full_cmd="export PATH=\"\$HOME/.local/bin:\$HOME/.bun/bin:\$PATH\" && $cmd"
|
||||
# SECURITY: Properly escape command to prevent injection
|
||||
local escaped_cmd
|
||||
escaped_cmd=$(printf '%q' "$cmd")
|
||||
escaped_cmd=$(printf '%q' "$full_cmd")
|
||||
local session_exit=0
|
||||
"$(_get_fly_cmd)" ssh console -a "$FLY_APP_NAME" -C "bash -c \"$escaped_cmd\"" || session_exit=$?
|
||||
SERVER_NAME="${FLY_APP_NAME:-}" SPAWN_RECONNECT_CMD="fly ssh console -a ${FLY_APP_NAME:-}" \
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ AGENT_MODEL_PROMPT=1
|
|||
AGENT_MODEL_DEFAULT="openrouter/auto"
|
||||
|
||||
agent_install() {
|
||||
cloud_run "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs"
|
||||
install_agent "openclaw" "npm install -g --ignore-scripts openclaw@latest" cloud_run
|
||||
# Node.js is installed in wait_for_cloud_init; bun install -g fails on Fly
|
||||
install_agent "openclaw" "npm install -g openclaw@latest" cloud_run
|
||||
}
|
||||
|
||||
agent_env_vars() {
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ _pick_server_type() {
|
|||
local location="$1"
|
||||
# Wrap the location-specific list function for interactive_pick
|
||||
_list_server_types_for_current_location() { _list_server_types_for_location "$location"; }
|
||||
interactive_pick "HETZNER_SERVER_TYPE" "cpx11" "server types" _list_server_types_for_current_location "cpx11"
|
||||
interactive_pick "HETZNER_SERVER_TYPE" "cx23" "server types" _list_server_types_for_current_location "cx23"
|
||||
unset -f _list_server_types_for_current_location
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ _hetzner_get_available_ids() {
|
|||
}
|
||||
|
||||
# Search for a compatible fallback server type when the requested one is unavailable
|
||||
# Tries same CPU family first, then any family with >= specs
|
||||
# Tries: 1) same CPU family with >= specs, 2) any family with >= specs, 3) cheapest available
|
||||
# Prints the fallback type name on success; emits FALLBACK: info on stderr
|
||||
_hetzner_find_fallback_type() {
|
||||
local server_type="$1" types_response="$2" location="$3" available_ids="$4"
|
||||
|
|
@ -269,12 +269,15 @@ _hetzner_find_fallback_type() {
|
|||
ids_json=$(printf '%s\n' "$available_ids" | jq -Rn '[inputs | tonumber]')
|
||||
|
||||
local family candidates replacement
|
||||
for family in "same" "any"; do
|
||||
for family in "same" "any" "cheapest"; do
|
||||
local filter
|
||||
if [[ "$family" == "same" ]]; then
|
||||
filter="select(.cpu_type == \"${wanted_cpu}\" and .cores >= ${wanted_cores} and .memory >= ${wanted_memory})"
|
||||
else
|
||||
elif [[ "$family" == "any" ]]; then
|
||||
filter="select(.cores >= ${wanted_cores} and .memory >= ${wanted_memory})"
|
||||
else
|
||||
# Last resort: any available non-deprecated type (cheapest)
|
||||
filter="."
|
||||
fi
|
||||
|
||||
candidates=$(_hetzner_find_candidates "$types_response" "$location" "$ids_json" "$filter")
|
||||
|
|
@ -282,6 +285,7 @@ _hetzner_find_fallback_type() {
|
|||
replacement=$(printf '%s\n' "$candidates" | head -1 | cut -d'|' -f2)
|
||||
local label="${wanted_cpu}"
|
||||
[[ "$family" == "any" ]] && label="any"
|
||||
[[ "$family" == "cheapest" ]] && label="cheapest"
|
||||
printf 'FALLBACK:%s:%s:%s:%s\n' "$server_type" "$replacement" "$location" "$label" >&2
|
||||
printf '%s' "$replacement"
|
||||
return 0
|
||||
|
|
@ -308,22 +312,30 @@ _validate_server_type_for_location() {
|
|||
local types_response
|
||||
types_response=$(hetzner_api GET "/server_types?per_page=50")
|
||||
|
||||
# Check if the requested type exists and is directly available
|
||||
# Check if the requested type exists (including deprecated) for spec lookup
|
||||
local wanted_id
|
||||
wanted_id=$(printf '%s' "$types_response" | jq -r \
|
||||
--arg name "$server_type" \
|
||||
'.server_types[] | select(.name == $name and .deprecation == null) | .id')
|
||||
|
||||
if [[ -z "$wanted_id" ]]; then
|
||||
printf 'ERROR:unknown_type\n' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$available_ids" | grep -qx "$wanted_id"; then
|
||||
if [[ -n "$wanted_id" ]] && printf '%s\n' "$available_ids" | grep -qx "$wanted_id"; then
|
||||
printf '%s' "$server_type"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Type is unavailable (not in available list or deprecated) — find a fallback.
|
||||
# If the type exists but is deprecated, we can still read its specs for fallback matching.
|
||||
if [[ -z "$wanted_id" ]]; then
|
||||
# Check if it exists at all (even deprecated) so we can read specs for fallback
|
||||
wanted_id=$(printf '%s' "$types_response" | jq -r \
|
||||
--arg name "$server_type" \
|
||||
'.server_types[] | select(.name == $name) | .id')
|
||||
if [[ -z "$wanted_id" ]]; then
|
||||
printf 'ERROR:unknown_type\n' >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_hetzner_find_fallback_type "$server_type" "$types_response" "$location" "$available_ids"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ _launch_oci_instance() {
|
|||
|
||||
create_server() {
|
||||
local name="${1}"
|
||||
local shape="${OCI_SHAPE:-VM.Standard.E2.1.Micro}"
|
||||
local shape="${OCI_SHAPE:-VM.Standard.A1.Flex}"
|
||||
|
||||
log_step "Creating OCI instance '${name}' (shape: ${shape})..."
|
||||
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ print(json.dumps(body))
|
|||
# Create an OVH Public Cloud instance
|
||||
create_ovh_instance() {
|
||||
local name="$1"
|
||||
local flavor="${OVH_FLAVOR:-d2-2}"
|
||||
local flavor="${OVH_FLAVOR:-d2-4}"
|
||||
local region="${OVH_REGION:-GRA7}"
|
||||
|
||||
# Validate env var inputs to prevent injection into Python code
|
||||
|
|
|
|||
|
|
@ -3120,10 +3120,10 @@ EOF
|
|||
# DEFAULT_VALUE - Default value if env var unset and list is empty or choice invalid
|
||||
# PROMPT_TEXT - Label shown above the menu (e.g., "locations", "server types")
|
||||
# LIST_CALLBACK - Function that outputs pipe-delimited lines (first field = ID)
|
||||
# DEFAULT_ID - Optional: ID to pre-select as default (e.g., "cpx11")
|
||||
# DEFAULT_ID - Optional: ID to pre-select as default (e.g., "cx23")
|
||||
#
|
||||
# LIST_CALLBACK must output pipe-delimited lines where the first field is the selectable ID.
|
||||
# Example output: "fsn1|Falkenstein|DE" or "cpx11|2 vCPU|4 GB RAM|40 GB disk"
|
||||
# Example output: "fsn1|Falkenstein|DE" or "cx23|2 vCPU|4 GB RAM|40 GB disk"
|
||||
#
|
||||
# Display a numbered list and read user selection
|
||||
# Pipe-delimited items: "id|label". Returns selected id via stdout.
|
||||
|
|
|
|||
267
test/e2e.sh
267
test/e2e.sh
|
|
@ -81,6 +81,111 @@ _get_token_env_var() {
|
|||
esac
|
||||
}
|
||||
|
||||
# --- Credential helpers ---
|
||||
|
||||
# Try to load a token from the spawn config file into the env var.
|
||||
# Returns 0 if token was loaded, 1 if not.
|
||||
_load_token_from_config() {
|
||||
local cloud="$1"
|
||||
local token_var
|
||||
token_var=$(_get_token_env_var "$cloud")
|
||||
[[ -z "$token_var" ]] && return 1
|
||||
|
||||
# Already set — nothing to do
|
||||
local current="${!token_var:-}"
|
||||
[[ -n "$current" ]] && return 0
|
||||
|
||||
local config_file="${HOME}/.config/spawn/${cloud}.json"
|
||||
[[ -f "$config_file" ]] || return 1
|
||||
|
||||
local saved
|
||||
saved=$(python3 -c "import json, sys; data=json.load(open(sys.argv[1])); print(data.get('api_key','') or data.get('token',''))" "$config_file" 2>/dev/null)
|
||||
if [[ -n "$saved" ]]; then
|
||||
export "$token_var=$saved"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Interactive credential collection — runs BEFORE non-interactive tests.
|
||||
# For each token-based cloud, ensures the env var is set by:
|
||||
# 1. Checking the env var
|
||||
# 2. Loading from ~/.config/spawn/{cloud}.json
|
||||
# 3. Prompting the user (Enter to skip)
|
||||
_collect_credentials() {
|
||||
local clouds="$1"
|
||||
local collected=""
|
||||
local skipped=""
|
||||
|
||||
for cloud in $clouds; do
|
||||
local token_var
|
||||
token_var=$(_get_token_env_var "$cloud")
|
||||
|
||||
# CLI-auth clouds (aws, gcp, oracle, sprite) — no token to collect
|
||||
[[ -z "$token_var" ]] && continue
|
||||
|
||||
# Already in env?
|
||||
if [[ -n "${!token_var:-}" ]]; then
|
||||
collected="${collected} ${cloud}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Try config file
|
||||
if _load_token_from_config "$cloud"; then
|
||||
_e2e_log "Loaded ${token_var} from ~/.config/spawn/${cloud}.json"
|
||||
collected="${collected} ${cloud}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Fly: try CLI auth (fly auth token)
|
||||
if [[ "$cloud" == "fly" ]] && _try_fly_cli_token; then
|
||||
_e2e_log "Loaded FLY_API_TOKEN from fly CLI auth"
|
||||
collected="${collected} ${cloud}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# No TTY? Can't prompt — skip
|
||||
if ! echo -n "" > /dev/tty 2>/dev/null; then
|
||||
skipped="${skipped} ${cloud}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Interactive prompt
|
||||
printf ' %s: paste %s (Enter to skip): ' "$cloud" "$token_var"
|
||||
local token=""
|
||||
read -r token </dev/tty
|
||||
if [[ -n "$token" ]]; then
|
||||
export "$token_var=$token"
|
||||
collected="${collected} ${cloud}"
|
||||
else
|
||||
skipped="${skipped} ${cloud}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$skipped" ]]; then
|
||||
_e2e_log "Skipped (no credentials):${skipped}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Try to get FLY_API_TOKEN from the flyctl CLI (fly auth token)
|
||||
_try_fly_cli_token() {
|
||||
local fly_cmd=""
|
||||
if command -v fly &>/dev/null; then
|
||||
fly_cmd="fly"
|
||||
elif command -v flyctl &>/dev/null; then
|
||||
fly_cmd="flyctl"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
local token
|
||||
token=$("$fly_cmd" auth token 2>/dev/null) || return 1
|
||||
if [[ -n "$token" ]]; then
|
||||
export FLY_API_TOKEN="$token"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Credential check ---
|
||||
|
||||
# Check if a cloud has credentials available (non-interactive)
|
||||
|
|
@ -98,7 +203,7 @@ _cloud_has_credentials() {
|
|||
local) return 0 ;;
|
||||
esac
|
||||
|
||||
# Token-based clouds: check env var, then spawn config file
|
||||
# Token-based clouds: check env var, then spawn config file, then CLI
|
||||
if [[ -n "$token_var" ]]; then
|
||||
local token_val="${!token_var:-}"
|
||||
if [[ -n "$token_val" ]]; then
|
||||
|
|
@ -109,6 +214,10 @@ _cloud_has_credentials() {
|
|||
if [[ -f "$config_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Fly: also check CLI auth
|
||||
if [[ "$cloud" == "fly" ]]; then
|
||||
_try_fly_cli_token &>/dev/null && return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
|
@ -314,7 +423,7 @@ _setup_noninteractive_env() {
|
|||
case "$cloud" in
|
||||
hetzner)
|
||||
export HETZNER_LOCATION="${HETZNER_LOCATION:-fsn1}"
|
||||
export HETZNER_SERVER_TYPE="${HETZNER_SERVER_TYPE:-cpx11}"
|
||||
export HETZNER_SERVER_TYPE="${HETZNER_SERVER_TYPE:-cx23}"
|
||||
;;
|
||||
fly)
|
||||
export FLY_REGION="${FLY_REGION:-iad}"
|
||||
|
|
@ -491,60 +600,46 @@ _build_failure_context() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Spawn one Claude agent per cloud to fix ALL failures on that cloud at once
|
||||
auto_fix_cloud() {
|
||||
local cloud="$1"
|
||||
shift
|
||||
local agents="$*"
|
||||
# Spawn one Claude agent to fix a single failing combo
|
||||
auto_fix_combo() {
|
||||
local cloud="$1" agent="$2"
|
||||
|
||||
if ! command -v claude &>/dev/null; then
|
||||
_e2e_log "claude CLI not found — skipping auto-fix for ${cloud}"
|
||||
_e2e_log "claude CLI not found — skipping auto-fix for ${cloud}/${agent}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local agent_count=0
|
||||
local prompt=""
|
||||
local files_list=""
|
||||
|
||||
# Build combined prompt with all failures for this cloud
|
||||
for agent in $agents; do
|
||||
prompt="${prompt}$(_build_failure_context "$cloud" "$agent")"
|
||||
prompt="${prompt}---
|
||||
|
||||
"
|
||||
files_list="${files_list} ${cloud}/${agent}.sh"
|
||||
agent_count=$((agent_count + 1))
|
||||
done
|
||||
local prompt
|
||||
prompt=$(_build_failure_context "$cloud" "$agent")
|
||||
|
||||
local cloud_lib=""
|
||||
if [[ -f "${REPO_ROOT}/${cloud}/lib/common.sh" ]]; then
|
||||
cloud_lib=$(cat "${REPO_ROOT}/${cloud}/lib/common.sh")
|
||||
fi
|
||||
|
||||
_e2e_log "Spawning Claude agent for ${cloud} (${agent_count} failure(s): ${agents})..."
|
||||
_e2e_log "Spawning Claude agent for ${cloud}/${agent}..."
|
||||
|
||||
claude -p "You are fixing E2E test failures for the **${cloud}** cloud provider.
|
||||
claude -p "You are fixing an E2E test failure for **${cloud}/${agent}**.
|
||||
|
||||
## Cloud Library (${cloud}/lib/common.sh)
|
||||
\`\`\`bash
|
||||
${cloud_lib}
|
||||
\`\`\`
|
||||
|
||||
## Failures to Fix
|
||||
## Failure
|
||||
|
||||
${prompt}
|
||||
|
||||
## Instructions
|
||||
|
||||
Fix ALL ${agent_count} failing script(s):${files_list}
|
||||
Fix the failing script: ${cloud}/${agent}.sh
|
||||
|
||||
For each script:
|
||||
1. Read the error output to understand what went wrong
|
||||
2. Compare with the reference script (working on another cloud) if available
|
||||
3. Fix the issue — common problems: wrong install command, missing PATH, timeout in non-TTY
|
||||
4. Run \`bash -n\` on every modified file
|
||||
|
||||
Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/autofix_${cloud}.log" || true
|
||||
Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/autofix_${cloud}_${agent}.log" || true
|
||||
}
|
||||
|
||||
# --- Timing history ---
|
||||
|
|
@ -810,68 +905,48 @@ _build_slow_context() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Spawn one Claude agent per cloud to optimize ALL slow combos on that cloud
|
||||
optimize_slow_cloud() {
|
||||
local cloud="$1"
|
||||
shift
|
||||
# Remaining args: "agent:elapsed:reasons" entries
|
||||
local entries="$*"
|
||||
# Spawn one Claude agent to optimize a single slow combo
|
||||
optimize_slow_combo() {
|
||||
local cloud="$1" agent="$2" elapsed="$3" reasons="$4"
|
||||
|
||||
if ! command -v claude &>/dev/null; then
|
||||
_e2e_log "claude CLI not found — skipping optimization for ${cloud}"
|
||||
_e2e_log "claude CLI not found — skipping optimization for ${cloud}/${agent}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local agent_count=0
|
||||
local prompt=""
|
||||
local files_list=""
|
||||
|
||||
for entry in $entries; do
|
||||
local agent="${entry%%:*}"
|
||||
local rest="${entry#*:}"
|
||||
local elapsed="${rest%%:*}"
|
||||
local reasons
|
||||
reasons=$(printf '%s' "${rest#*:}" | tr '|' '\n')
|
||||
|
||||
prompt="${prompt}$(_build_slow_context "$cloud" "$agent" "$elapsed" "$reasons")"
|
||||
prompt="${prompt}---
|
||||
|
||||
"
|
||||
files_list="${files_list} ${cloud}/${agent}.sh"
|
||||
agent_count=$((agent_count + 1))
|
||||
done
|
||||
local prompt
|
||||
prompt=$(_build_slow_context "$cloud" "$agent" "$elapsed" "$reasons")
|
||||
|
||||
local cloud_lib=""
|
||||
if [[ -f "${REPO_ROOT}/${cloud}/lib/common.sh" ]]; then
|
||||
cloud_lib=$(cat "${REPO_ROOT}/${cloud}/lib/common.sh")
|
||||
fi
|
||||
|
||||
_e2e_log "Spawning Claude agent for ${cloud} (${agent_count} slow combo(s))..."
|
||||
_e2e_log "Spawning Claude agent for ${cloud}/${agent} (${elapsed}s)..."
|
||||
|
||||
claude -p "You are optimizing slow E2E tests for the **${cloud}** cloud provider.
|
||||
All these scripts PASS but are too slow.
|
||||
claude -p "You are optimizing a slow E2E test for **${cloud}/${agent}**.
|
||||
The script PASSES but is too slow.
|
||||
|
||||
## Cloud Library (${cloud}/lib/common.sh)
|
||||
\`\`\`bash
|
||||
${cloud_lib}
|
||||
\`\`\`
|
||||
|
||||
## Slow Scripts to Optimize
|
||||
## Slow Script
|
||||
|
||||
${prompt}
|
||||
|
||||
## Instructions
|
||||
|
||||
Optimize ALL ${agent_count} slow script(s):${files_list}
|
||||
Optimize the script: ${cloud}/${agent}.sh
|
||||
|
||||
For each script:
|
||||
1. Compare timings with the fastest peer cloud for the same agent
|
||||
2. Identify what makes it slow (heavy installer, compiling native deps, unnecessary steps)
|
||||
3. Make it faster — use lighter install methods, skip unnecessary setup, parallelize where possible
|
||||
4. Run \`bash -n\` on every modified file
|
||||
5. Don't break anything — the scripts must still pass E2E
|
||||
5. Don't break anything — the script must still pass E2E
|
||||
|
||||
Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/optimize_${cloud}.log" || true
|
||||
Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/optimize_${cloud}_${agent}.log" || true
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
|
@ -975,6 +1050,16 @@ main() {
|
|||
# Testable clouds (excludes local, sprite which don't provision real servers the same way)
|
||||
local testable_clouds="fly hetzner digitalocean ovh aws daytona gcp oracle"
|
||||
|
||||
# --- Credential collection (interactive) ---
|
||||
# Load tokens from config files and prompt for any missing ones
|
||||
# BEFORE we go non-interactive. This lets the user provide tokens
|
||||
# that aren't in env vars or config files.
|
||||
echo ""
|
||||
_e2e_log "━━━ Credential Collection ━━━"
|
||||
echo ""
|
||||
_collect_credentials "$testable_clouds"
|
||||
echo ""
|
||||
|
||||
# Discover clouds with available credentials
|
||||
local available_clouds=""
|
||||
if [[ -n "$filter_cloud" ]]; then
|
||||
|
|
@ -1200,34 +1285,19 @@ main() {
|
|||
done
|
||||
echo ""
|
||||
|
||||
# Group slow combos by cloud and spawn one agent per cloud in parallel
|
||||
local opt_clouds=""
|
||||
# Spawn one Claude agent per slow combo, all in parallel
|
||||
local opt_pids=""
|
||||
for entry in $slow_combos; do
|
||||
local combo="${entry%%:*}"
|
||||
local rest="${entry#*:}"
|
||||
local elapsed="${rest%%:*}"
|
||||
local reasons
|
||||
reasons=$(printf '%s' "${rest#*:}" | tr '|' '\n')
|
||||
local cloud="${combo%%/*}"
|
||||
case " $opt_clouds " in
|
||||
*" $cloud "*) ;; # already listed
|
||||
*) opt_clouds="${opt_clouds} ${cloud}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local opt_pids=""
|
||||
for cloud in $opt_clouds; do
|
||||
# Collect all slow entries for this cloud: "agent:elapsed:reasons ..."
|
||||
local cloud_entries=""
|
||||
for entry in $slow_combos; do
|
||||
local combo="${entry%%:*}"
|
||||
local entry_cloud="${combo%%/*}"
|
||||
local agent="${combo##*/}"
|
||||
if [[ "$entry_cloud" == "$cloud" ]]; then
|
||||
local rest="${entry#*:}"
|
||||
cloud_entries="${cloud_entries} ${agent}:${rest}"
|
||||
fi
|
||||
done
|
||||
cloud_entries=$(printf '%s' "$cloud_entries" | sed 's/^ //')
|
||||
local agent="${combo##*/}"
|
||||
|
||||
(
|
||||
optimize_slow_cloud "$cloud" $cloud_entries
|
||||
optimize_slow_combo "$cloud" "$agent" "$elapsed" "$reasons"
|
||||
) &
|
||||
opt_pids="${opt_pids} $!"
|
||||
done
|
||||
|
|
@ -1249,7 +1319,7 @@ main() {
|
|||
local cloud="${combo%%/*}"
|
||||
local agent="${combo##*/}"
|
||||
|
||||
run_e2e_test "$cloud" "$agent"
|
||||
run_e2e_test "$cloud" "$agent" || true
|
||||
|
||||
local result_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.result"
|
||||
local timing_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.timing"
|
||||
|
|
@ -1267,37 +1337,20 @@ main() {
|
|||
done
|
||||
fi
|
||||
|
||||
# Auto-fix failures — one Claude agent per cloud, all in parallel
|
||||
# Auto-fix failures — one Claude agent per combo, all in parallel
|
||||
if [[ "$total_fail" -gt 0 ]] && [[ "$E2E_AUTO_FIX" == "1" ]]; then
|
||||
echo ""
|
||||
_e2e_log "━━━ Auto-Fix Phase ━━━"
|
||||
echo ""
|
||||
|
||||
# Group failures by cloud
|
||||
local fix_clouds=""
|
||||
# Spawn one agent per failing combo in parallel
|
||||
local fix_pids=""
|
||||
for combo in $failed_combos; do
|
||||
local cloud="${combo%%/*}"
|
||||
case " $fix_clouds " in
|
||||
*" $cloud "*) ;;
|
||||
*) fix_clouds="${fix_clouds} ${cloud}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Spawn one agent per cloud in parallel
|
||||
local fix_pids=""
|
||||
for cloud in $fix_clouds; do
|
||||
local cloud_agents=""
|
||||
for combo in $failed_combos; do
|
||||
local c="${combo%%/*}"
|
||||
local a="${combo##*/}"
|
||||
if [[ "$c" == "$cloud" ]]; then
|
||||
cloud_agents="${cloud_agents} ${a}"
|
||||
fi
|
||||
done
|
||||
cloud_agents=$(printf '%s' "$cloud_agents" | sed 's/^ //')
|
||||
local agent="${combo##*/}"
|
||||
|
||||
(
|
||||
auto_fix_cloud "$cloud" $cloud_agents
|
||||
auto_fix_combo "$cloud" "$agent"
|
||||
) &
|
||||
fix_pids="${fix_pids} $!"
|
||||
done
|
||||
|
|
@ -1319,7 +1372,7 @@ main() {
|
|||
local cloud="${combo%%/*}"
|
||||
local agent="${combo##*/}"
|
||||
|
||||
run_e2e_test "$cloud" "$agent"
|
||||
run_e2e_test "$cloud" "$agent" || true
|
||||
|
||||
local result_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.result"
|
||||
local timing_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.timing"
|
||||
|
|
|
|||
2
test/fixtures/hetzner/_env.sh
vendored
2
test/fixtures/hetzner/_env.sh
vendored
|
|
@ -1,4 +1,4 @@
|
|||
export HCLOUD_TOKEN="test-token-hetzner"
|
||||
export HETZNER_SERVER_NAME="test-srv"
|
||||
export HETZNER_SERVER_TYPE="cpx11"
|
||||
export HETZNER_SERVER_TYPE="cx22"
|
||||
export HETZNER_LOCATION="fsn1"
|
||||
|
|
|
|||
2
test/fixtures/hetzner/datacenters.json
vendored
2
test/fixtures/hetzner/datacenters.json
vendored
|
|
@ -12,7 +12,7 @@
|
|||
"city": "Falkenstein"
|
||||
},
|
||||
"server_types": {
|
||||
"available": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97],
|
||||
"available": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 120],
|
||||
"available_for_migration": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36],
|
||||
"supported": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97]
|
||||
}
|
||||
|
|
|
|||
81
test/fixtures/hetzner/server_types.json
vendored
81
test/fixtures/hetzner/server_types.json
vendored
|
|
@ -2591,6 +2591,87 @@
|
|||
],
|
||||
"storage_type": "local"
|
||||
},
|
||||
{
|
||||
"architecture": "x86",
|
||||
"category": "cost_optimized",
|
||||
"cores": 2,
|
||||
"cpu_type": "shared",
|
||||
"deprecated": false,
|
||||
"deprecation": null,
|
||||
"description": "CX 22",
|
||||
"disk": 40,
|
||||
"id": 120,
|
||||
"locations": [
|
||||
{
|
||||
"deprecation": null,
|
||||
"id": 1,
|
||||
"name": "fsn1"
|
||||
},
|
||||
{
|
||||
"deprecation": null,
|
||||
"id": 2,
|
||||
"name": "nbg1"
|
||||
},
|
||||
{
|
||||
"deprecation": null,
|
||||
"id": 3,
|
||||
"name": "hel1"
|
||||
}
|
||||
],
|
||||
"memory": 2,
|
||||
"name": "cx22",
|
||||
"prices": [
|
||||
{
|
||||
"included_traffic": 21990232555520,
|
||||
"location": "fsn1",
|
||||
"price_hourly": {
|
||||
"gross": "0.0048000000000000",
|
||||
"net": "0.0048000000"
|
||||
},
|
||||
"price_monthly": {
|
||||
"gross": "2.9900000000000000",
|
||||
"net": "2.9900000000"
|
||||
},
|
||||
"price_per_tb_traffic": {
|
||||
"gross": "1.2000000000000000",
|
||||
"net": "1.2000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"included_traffic": 21990232555520,
|
||||
"location": "hel1",
|
||||
"price_hourly": {
|
||||
"gross": "0.0048000000000000",
|
||||
"net": "0.0048000000"
|
||||
},
|
||||
"price_monthly": {
|
||||
"gross": "2.9900000000000000",
|
||||
"net": "2.9900000000"
|
||||
},
|
||||
"price_per_tb_traffic": {
|
||||
"gross": "1.2000000000000000",
|
||||
"net": "1.2000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"included_traffic": 21990232555520,
|
||||
"location": "nbg1",
|
||||
"price_hourly": {
|
||||
"gross": "0.0048000000000000",
|
||||
"net": "0.0048000000"
|
||||
},
|
||||
"price_monthly": {
|
||||
"gross": "2.9900000000000000",
|
||||
"net": "2.9900000000"
|
||||
},
|
||||
"price_per_tb_traffic": {
|
||||
"gross": "1.2000000000000000",
|
||||
"net": "1.2000000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"storage_type": "local"
|
||||
},
|
||||
{
|
||||
"architecture": "x86",
|
||||
"category": "cost_optimized",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue