ENG-11157: add --min-instances/--max-instances to `reflex deploy`
What
Adds first-class autoscaling controls to reflex deploy, backed by the instance bounds route merged server side in flexgen #4744.
--min-instances N/--max-instances N(new). After the app is resolved and validated but before the deployment is submitted, the CLI posts toPOST /api/v1/apps/{app_id}/instance_boundswith{"min_instances": N, "max_instances": N}. The deployment picks the bounds up when it is created.- Only what the user passed is sent. An omitted flag means the key is absent from the body, so apps keep their platform defaults unless explicitly overridden. Passing neither flag skips the call entirely.
- No client-side validation. Server details for
400(min > max, out of 0..1000),400 "platform does not support instance bounds"(non-GCP apps), and409(scale already running) are surfaced verbatim.
Why
The platform now supports these controls but there was no way to reach them from the CLI — bounds could only be changed through the dashboard.
Notes for reviewers
--vmtype was already wired end to end; no fix was needed. The ticket lists it as dropped before the deployment POST, but it flows reflex.py → cli.deploy(vmtype=...) → create_deployment(vmtype=...) → payload["vm_type"], and has since the package landed (4710b252a). This PR adds the regression tests to pin that path instead. reflex cloud apps scale --vmtype also already exists (v2/apps.py), so it is untouched.
⚠️ One open question: the ticket names the submit body field vmtype, but the CLI sends vm_type. I did not rename it — it's the long-standing wire format and a rename would break every deploy unless flexgen #4744 changed it too. Note both spellings are already in play: validate_cli takes a vmtype query param, the submit takes a vm_type form field. Please confirm which the server expects.
Other deliberate calls:
- Bounds are not read from
cloud.yml/pyproject.toml, unlikevmtype/regions/hostname. Keeps to "no defaults injected by the CLI" and avoids a new public config schema key. Easy to add if wanted. - No way to reset a bound to the platform default — omitting a flag means "leave unchanged", so there's no sentinel for an explicit null.
- The call sits inside
_restore_provider_on_failure, so a rejected bound on a deploy that also switched provider rolls the provider back and prints the restore warning. Correct failure semantics, but one user error yields two messages. - Bounds are GCP-only, the inverse of
--vmtype, whichdeploy()silently nulls for GCP targets. No symmetric client-side notice was added — the server's 400 is the source of truth, which does mean a Reflex Cloud user gets a failed deploy rather than a warning-and-continue.
Test plan
-
uv run pytest tests/units— 7188 passed, 17 skipped; coverage 73.92% (threshold 72%) -
uv run ruff check ./uv run ruff format --check .clean -
uv run pyright reflex tests— no new errors (5 pre-existing in unrelated lucide/recharts stub tests) -
reflex deploy --helprenders both new flags - New unit tests: body carries only the passed bounds (min-only / max-only / both); 400-validation, 400-unsupported-platform and 409-conflict details surface; bounds call ordered strictly before submit; call skipped with no flags; deploy aborts before submit on rejection;
--vmtypepassthrough from flag tovm_typesubmit field
No component changes, so no .pyi regeneration.
🤖 Generated with Claude Code