Skip to content

ENG-11433 Move reflex deploy out of the framework into the Cloud CLI - #6924

Open
FarhanAliRaza wants to merge 8 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/eng-11433-move-reflex-deploy-command-out-of-the-reflex-package-into
Open

ENG-11433 Move reflex deploy out of the framework into the Cloud CLI#6924
FarhanAliRaza wants to merge 8 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/eng-11433-move-reflex-deploy-command-out-of-the-reflex-package-into

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes ENG-11433.

reflex deploy lived in reflex/reflex.py, so cloud-specific code shipped inside the open-source framework. The implementation now lives in reflex-hosting-cli.

What changed

The command moved. reflex/reflex.py loses 245 lines; reflex_cli/v2/deploy.py gains 236. The framework contains no cloud code.

The import is guarded. reflex/reflex.py imports the moved command and falls back when it cannot:

try:
    from reflex_cli.v2.deploy import deploy
    from reflex_cli.v2.deployments import hosting_cli
except ImportError:
    cli.add_command(_missing_command("deploy"), name="deploy")
    cli.add_command(_missing_command("cloud"), name="cloud")
else:
    ...
    cli.add_command(deploy, name="deploy")
    cli.add_command(hosting_cli, name="cloud")

The typer conversion for cloud is unchanged from before; it just moved inside the else.

A missing package no longer fails opaquely. The stand-in is a real command of the same name. ignore_unknown_options plus a click.UNPROCESSED argument make it swallow every flag, so reflex deploy --app-name demo reports pip install reflex-hosting-cli instead of dying on "No such option". login and logout use the same helper on ImportError.

Standalone importability, fixed. deploy.py imported reflex.utils.cli_options, but reflex is not a dependency of reflex-hosting-cli, so importing the package on its own raised ModuleNotFoundError. The shared click options moved to reflex_base.utils.cli_options (adding click >=8.2 to reflex-base); reflex/utils/cli_options.py re-exports them for its existing callers.

The framework surface is one supported module (from review: the command body necessitated six reflex internals — constants, config, environment, reflex.reflex._init, utils.export, utils.prerequisites — that reflex would have to maintain forever). A new reflex/hosting.py is now the explicit interface the framework supports for the hosting CLI:

  • prepare_deploy(ssr=...) — sets the DEPLOY compile context, reconciles --ssr with REFLEX_SSR, ensures the cwd is an initialized app, and returns DeployPrep(app_name, loglevel, ssr).
  • export_for_deploy(...) — thin wrapper over export() that fills loglevel from config. The export_fn callback shape stays owned by the hosting CLI.

The command body imports nothing else from reflex, and test_deploy_uses_only_the_supported_framework_interface fails if any other reflex.* import sneaks back in. The --ssr/REFLEX_SSR arbitration that was duplicated between deploy and reflex export is now shared as exec.arbitrate_ssr().

Answers to the issue's open questions

How is it wired up? A guarded import, with reflex-hosting-cli still a required dependency. pip install reflex gives a working reflex deploy and nothing changes for existing users. Dropping the dependency would break them, which the "no breaking changes" bullet forbids.

An earlier revision of this PR used a reflex.cli_commands entry point group instead. It was removed: the only thing it bought over a direct import was that the framework did not write the name reflex_cli in its source, which did not justify 173 lines of discovery machinery and an indirection that hides where reflex deploy comes from. The cloud code is out of the framework either way, which is what the issue asks for.

What if the Cloud CLI is not installed? The command name still exists and reports what to install, as above.

No UX change

Verified rather than assumed: the pre-move command is reconstructed from the merge-base copy of reflex/reflex.py and diffed against the live one. All 23 parameters match on name, opts, secondary opts, type, default, multiple and required, and the rendered --help is identical. A separate check runs the command against a real app and asserts the 13 arguments still reach reflex_cli.v2.cli.deploy, and that --no-ssr and --exclude-from-backend still reach export() through the export_fn closure.

Note for the release

reflex deploy now lives in reflex_cli.v2.deploy, which older hosting CLI releases do not carry, so the floor must exclude them. That release is unpublished, so the pin is the workspace development version 0.1.70.post18.dev0 — the same convention reflex-hosting-cli already uses for its reflex-base pin. scripts/check_min_deps.py --check-dev-pins reflex fails while it stands, so it must be re-pinned to the published version before release.

One side effect: check_min_deps.py reflex cannot build the root package while the dev pin stands. [tool.hatch.build.hooks.custom] sets require-runtime-dependencies = true, so runtime deps resolve inside the isolated build environment, which the script's local editable overrides never reach. The other packages still check.

Hitchhiker: repair of #6918 against post-#6939 main

#6918 (reflex cloud whoami/token) merged with day-old green checks, after #6939 had made reflex-base an optional dependency of the hosting CLI, so main is red. Since this branch merges main anyway, the repair rides along:

  • auth.py imported reflex_base.utils.log at module scope, breaking test_cli_imports_without_reflex_base; it now goes through the reflex_cli.utils.log shim.
  • test_auth.py tripped pyright after the typer upgrade (typer now vendors click, so get_command()'s annotations are incompatible with click.testing.CliRunner); the unwrapped group is cast to click.Group, which is what _patch_typer's get_command actually returns at runtime.

Tests

reflex deploy had no dedicated tests before this branch.

  • tests/units/reflex_cli/v2/test_deploy.py — registration, the flag surface, --help, a sys.meta_path probe that imports the module with every reflex import blocked (covers the standalone-import bug above), and the framework-interface boundary guard.
  • tests/units/test_hosting.pyprepare_deploy (compile context, SSR arbitration, reinit path, returned config values) and export_for_deploy pass-through.
  • tests/units/utils/test_exec.pyarbitrate_ssr both branches.
  • tests/units/test_reflex.py — the cloud commands resolve into reflex_cli, and the stand-in reports the package rather than a usage error, including when the real command's flags are passed.

Full suite: 7763 passed, 17 skipped. Ruff clean. Pyright: 0 errors.

@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner August 21, 2026 17:48
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…sting CLI

The managed-platform deploy command (options and body) now lives in
reflex_cli.v2.deploy; the reflex CLI registers it via cli.add_command.
Flags and behavior are unchanged. The module lazily imports reflex
internals in the command body since it only runs through the reflex CLI.
@FarhanAliRaza
FarhanAliRaza force-pushed the farhan/eng-11433-move-reflex-deploy-command-out-of-the-reflex-package-into branch from d82f9ea to b822d53 Compare August 21, 2026 17:51
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves the managed reflex deploy implementation into reflex-hosting-cli while preserving the framework’s command surface and deployment/export handoff.

  • Registers deploy and cloud through guarded direct imports, with informative fallback commands when the hosting CLI is unavailable.
  • Introduces a supported reflex.hosting boundary for deployment preparation and artifact export.
  • Moves shared CLI options into the hosting package and adds coverage for registration, import isolation, flags, SSR arbitration, and fallback behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/reflex-hosting-cli/src/reflex_cli/v2/deploy.py Relocates the deploy command into the hosting CLI while retaining its options and forwarding deployment preparation and export through the framework interface.
reflex/hosting.py Adds the framework-facing preparation and export boundary consumed by the relocated deploy command.
reflex/reflex.py Removes the embedded deploy implementation and directly registers hosting commands with missing-package fallbacks; the previously reported plugin collision path is gone.
reflex/utils/exec.py Extracts the existing SSR environment arbitration into a shared helper used by export and deploy.
pyproject.toml Raises the hosting CLI dependency floor to the workspace version containing the relocated command.
tests/units/reflex_cli/v2/test_deploy.py Verifies command registration, standalone importability, option preservation, and the supported framework import boundary.

Reviews (7): Last reviewed commit: "fix(hosting-cli): repair the reflex-base..." | Re-trigger Greptile

Comment thread reflex/utils/cli_plugins.py Outdated
@codspeed-hq

codspeed-hq Bot commented Aug 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing FarhanAliRaza:farhan/eng-11433-move-reflex-deploy-command-out-of-the-reflex-package-into (2f256b3) with main (dfb4ef0)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/units/utils/test_cli_plugins.py">

<violation number="1" location="tests/units/utils/test_cli_plugins.py:282">
P3: This reflex-side test hardcodes `reflex_cli.v2.deploy`, a private module path of the separate reflex-hosting-cli package, to assert eager loading. If the hosting CLI renames or reorganizes that module, this framework test fails spuriously even though the decoupling behavior is unchanged. Derive the expected module from the discovered entry points instead (e.g., check that every module referenced by the entry-point value is present in sys.modules) so the test stays coupled to the entry-point contract rather than to the hosting CLI's internal layout.</violation>
</file>

<file name="reflex/utils/cli_plugins.py">

<violation number="1" location="reflex/utils/cli_plugins.py:75">
P2: When a Typer entry point cannot be converted, `typer.main.get_command` aborts construction of the entire `reflex` CLI. Catch conversion errors, log the plugin failure, and continue so the remaining commands and fallback placeholders remain available.</violation>

<violation number="2" location="reflex/utils/cli_plugins.py:163">
P2: When a contributed entry point exists but fails to load (`entry_point.load()` raises) or does not resolve to a click command, the code still registers the "install/upgrade reflex-hosting-cli" placeholder. That message is wrong for an installed, current package, so the user is told to reinstall or upgrade an already-correct package instead of the real cause. Track names that already had an entry point separately from truly absent ones, and give those a diagnostic that reflects the actual failure (the load error already logged, or the non-click object type).</violation>

<violation number="3" location="reflex/utils/cli_plugins.py:173">
P2: Guard duplicate names before registering the command. Click stores commands by name, so this call overwrites an existing built-in or earlier contributed command; reject or skip collisions instead.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread pyproject.toml
Comment thread reflex/utils/cli_plugins.py Outdated
Comment thread reflex/utils/cli_plugins.py Outdated
Comment thread reflex/utils/cli_plugins.py Outdated
Comment thread tests/units/utils/test_cli_plugins.py Outdated
Comment thread reflex/reflex.py
The command body now lives in reflex_cli.v2.deploy, so reflex/reflex.py
imports it instead of defining it. That import is guarded: when the
hosting CLI is absent, a stand-in of the same name is registered. It
accepts any flags, so the user is told which package to install rather
than getting a usage error about an option the real command understands.
`login` and `logout` report the same way.

deploy.py imported log_options from `reflex`, which is not a dependency
of reflex-hosting-cli, so the package failed to import on its own. The
shared click options move to reflex_base.utils.cli_options, which both
packages already depend on; reflex/utils/cli_options.py re-exports them.

The hosting CLI floor moves to the release carrying the moved module,
held at the workspace development version until that ships.
@FarhanAliRaza
FarhanAliRaza force-pushed the farhan/eng-11433-move-reflex-deploy-command-out-of-the-reflex-package-into branch from b822d53 to 03a451c Compare August 21, 2026 18:07
…base

Reverts the reflex_base.utils.cli_options move (and the click dependency it
added to reflex-base). reflex/utils/cli_options.py holds the implementation
again, and the hosting CLI carries its own copy in
reflex_cli.utils.cli_options so deploy.py no longer imports reflex_base at
module scope; the framework bits it needs at runtime moved into the lazy
import block in the command body.
…ve-reflex-deploy-command-out-of-the-reflex-package-into

# Conflicts:
#	reflex/reflex.py
Comment thread packages/reflex-hosting-cli/src/reflex_cli/v2/deploy.py Outdated
…eploy interface

The deploy command body needed six reflex internals (constants, config,
environment, _init, export, prerequisites). Fold them into two supported
functions in a new reflex.hosting module -- prepare_deploy() and
export_for_deploy() -- so the framework can reshuffle its internals without
breaking the hosting CLI. The shared --ssr/REFLEX_SSR arbitration moves to
exec.arbitrate_ssr(), deduplicating the copy in the export command. A new
test guards that deploy.py imports the framework only through reflex.hosting.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-hosting-cli/src/reflex_cli/v2/deploy.py
…ve-reflex-deploy-command-out-of-the-reflex-package-into
…ged into main

auth.py imported reflex_base.utils.log directly, which breaks the hosting
CLI on reflex versions that predate reflex-base (guarded by
test_cli_imports_without_reflex_base, added in reflex-dev#6939 after reflex-dev#6918's last CI
run). Route it through the reflex_cli.utils.log shim instead.

test_auth.py tripped pyright after the typer upgrade: typer now vendors
click, so get_command()'s annotations are incompatible with
click.testing.CliRunner. Cast the unwrapped group to click.Group, which is
what get_command actually returns at runtime via _patch_typer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants