diff --git a/packages/cli/package.json b/packages/cli/package.json index 563d703e2..9831bafae 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.27.4", + "version": "0.27.5", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index b0d79a154..638114b99 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -449,7 +449,9 @@ export async function checkAccountStatus(): Promise { if (existingDroplets.ok) { const currentCount = existingDroplets.data.length; if (currentCount >= dropletLimit) { - const msg = `DigitalOcean droplet limit reached: ${currentCount}/${dropletLimit} droplets in use. Delete existing droplets or request a limit increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase`; + // List existing droplet names to help operators identify which to delete + const dropletNames = existingDroplets.data.map((d) => (isString(d.name) ? d.name : "unknown")).join(", "); + const msg = `DigitalOcean droplet limit reached: ${currentCount}/${dropletLimit} droplets in use. Existing: [${dropletNames}]. Delete existing droplets at ${DO_DASHBOARD_URL} or request a limit increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase`; logWarn(msg); if (process.env.SPAWN_NON_INTERACTIVE === "1") { throw new Error(msg); diff --git a/sh/e2e/e2e.sh b/sh/e2e/e2e.sh index a788a2c2a..76bc8d5e7 100755 --- a/sh/e2e/e2e.sh +++ b/sh/e2e/e2e.sh @@ -379,6 +379,21 @@ run_agents_for_cloud() { fi fi + # Bail out early if the cloud reports zero capacity (e.g. droplet limit reached). + # All agents would fail anyway — skip with an actionable error instead of wasting + # time on retries that cannot succeed. (#3059) + if [ "${effective_parallel}" -eq 0 ] && [ "${SEQUENTIAL_MODE}" -eq 0 ]; then + log_err "No capacity available on ${cloud} — all ${cloud} agents will be marked as failed." + log_err "Delete existing instances or request a limit increase, then re-run." + for agent in ${AGENTS_TO_TEST}; do + printf 'fail' > "${log_dir}/${cloud}-${agent}.result" + if [ -z "${cloud_failed}" ]; then cloud_failed="${agent}"; else cloud_failed="${cloud_failed} ${agent}"; fi + done + printf '%s %s %s %s %s' "0" "$(printf '%s\n' "${AGENTS_TO_TEST}" | wc -w | tr -d ' ')" "0s" "" "|${cloud_failed}" \ + > "${log_dir}/${cloud}.summary" + return 1 + fi + if [ "${effective_parallel}" -gt 0 ] && [ "${SEQUENTIAL_MODE}" -eq 0 ]; then # Parallel mode: batch agents log_info "Running agents in parallel (batch size: ${effective_parallel})" diff --git a/sh/e2e/lib/clouds/digitalocean.sh b/sh/e2e/lib/clouds/digitalocean.sh index eba4cd073..0f0589dc3 100644 --- a/sh/e2e/lib/clouds/digitalocean.sh +++ b/sh/e2e/lib/clouds/digitalocean.sh @@ -366,6 +366,7 @@ EOF # Queries the DigitalOcean account to determine available droplet capacity. # Subtracts non-e2e droplets from the account limit so parallel test runs # don't fail due to pre-existing droplets consuming quota slots. +# Returns 0 when no capacity is available so the caller can skip the cloud. # Falls back to 3 if the API is unavailable. # --------------------------------------------------------------------------- _digitalocean_max_parallel() { @@ -375,7 +376,8 @@ _digitalocean_max_parallel() { _existing=$(_do_curl_auth -sf "${_DO_API}/droplets?per_page=200" 2>/dev/null | grep -o '"id":[0-9]*' | wc -l | tr -d ' ') || { printf '3'; return 0; } _available=$(( _limit - _existing )) if [ "${_available}" -lt 1 ]; then - printf '1' + log_warn "DigitalOcean droplet limit reached: ${_existing}/${_limit} droplets in use (0 available)" + printf '0' else printf '%d' "${_available}" fi