mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix(digitalocean): detect droplet limit before creation, clear error on 422 (#2891)
checkAccountStatus() now queries the account's droplet_limit and current droplet count. When at capacity it warns interactively and throws immediately in headless/E2E mode with a clear message instead of attempting creation and getting a cryptic 422. Also adds specific detection of droplet limit 422 errors in createServer() with actionable guidance (limit increase URL). Bump CLI to 0.25.14. Fixes #2865 Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
83cd6bc6df
commit
0224b56a4d
2 changed files with 39 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.25.13",
|
||||
"version": "0.25.14",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -390,9 +390,10 @@ export async function promptSwitchAccount(): Promise<boolean> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check DigitalOcean account status for billing issues.
|
||||
* Check DigitalOcean account status for billing issues and droplet limits.
|
||||
* Uses the /v2/account endpoint which is already called during token validation.
|
||||
* Throws if the account is locked (billing issue). Warns on other statuses.
|
||||
* Throws if the account is locked (billing issue) or at the droplet limit (in headless mode).
|
||||
* Warns on other statuses.
|
||||
*/
|
||||
export async function checkAccountStatus(): Promise<void> {
|
||||
if (!_state.token) {
|
||||
|
|
@ -407,6 +408,7 @@ export async function checkAccountStatus(): Promise<void> {
|
|||
}
|
||||
const status = isString(rec.status) ? rec.status : "";
|
||||
const emailVerified = rec.email_verified;
|
||||
const dropletLimit = isNumber(rec.droplet_limit) ? rec.droplet_limit : 0;
|
||||
|
||||
if (status === "locked") {
|
||||
logWarn("Your DigitalOcean account is locked (usually a billing issue).");
|
||||
|
|
@ -440,10 +442,31 @@ export async function checkAccountStatus(): Promise<void> {
|
|||
if (emailVerified === false) {
|
||||
logWarn("Your DigitalOcean email is not verified. Verify it to avoid account restrictions.");
|
||||
}
|
||||
|
||||
// Check droplet limit — fail fast before attempting creation
|
||||
if (dropletLimit > 0) {
|
||||
const existingDroplets = await asyncTryCatch(() => doGetAll("/droplets", "droplets"));
|
||||
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`;
|
||||
logWarn(msg);
|
||||
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
|
||||
throw new Error(msg);
|
||||
}
|
||||
} else if (dropletLimit - currentCount <= 2) {
|
||||
logWarn(`DigitalOcean droplet quota almost full: ${currentCount}/${dropletLimit} droplets in use.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!r.ok) {
|
||||
// Only re-throw if it's our explicit lock error
|
||||
if (r.error instanceof Error && r.error.message === "DigitalOcean account is locked") {
|
||||
// Re-throw explicit errors (account locked, droplet limit in headless mode)
|
||||
if (
|
||||
r.error instanceof Error &&
|
||||
(r.error.message === "DigitalOcean account is locked" ||
|
||||
r.error.message.startsWith("DigitalOcean droplet limit reached"))
|
||||
) {
|
||||
throw r.error;
|
||||
}
|
||||
// Otherwise non-fatal — let createServer be the final check
|
||||
|
|
@ -1086,10 +1109,20 @@ export async function createServer(
|
|||
}
|
||||
logError(`Retry failed: ${String(retryData?.message || "Unknown error")}`);
|
||||
}
|
||||
} else if (/droplet.limit|limit.exceeded|error 422.*unprocessable/i.test(errMsg)) {
|
||||
logError(
|
||||
"Droplet limit exceeded. Delete existing droplets or request a limit increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase",
|
||||
);
|
||||
// Offer account switch — user might have another account with capacity
|
||||
const switched = await promptSwitchAccount();
|
||||
if (switched) {
|
||||
logStep("Retrying droplet creation with new account...");
|
||||
return createServer(name, tier, dropletSize, region, imageOverride);
|
||||
}
|
||||
} else {
|
||||
showNonBillingError(digitaloceanBilling, [
|
||||
"Region/size unavailable (try different DO_REGION or DO_DROPLET_SIZE)",
|
||||
"Droplet limit reached (check account limits)",
|
||||
"Droplet limit reached (check account limits at https://cloud.digitalocean.com/account/team/droplet_limit_increase)",
|
||||
]);
|
||||
// Offer account switch for non-billing errors too (e.g. quota on wrong account)
|
||||
const switched = await promptSwitchAccount();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue