diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b93db4b9016..ff84b87884b 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -150,7 +150,7 @@ jobs: - name: Install reflex-docs dependencies working-directory: ./docs/app - run: sfw uv sync + run: sfw uv sync --frozen - name: Init Website for reflex-docs working-directory: ./docs/app diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py index e1a1819aa60..755b3410831 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py @@ -6,6 +6,7 @@ referenced with ``gcloud builds submit --config=...``) — the user's project tree is never modified. The script reads its parameters from environment variables (GCP_PROJECT, GCP_REGION, SERVICE_NAME, AR_REPO, VERSION, +CLOUD_RUN_CPU, CLOUD_RUN_MEMORY, CLOUD_RUN_MIN_INSTANCES, REFLEX_CLOUDBUILD_YAML). """ @@ -37,6 +38,9 @@ ENV_SERVICE_NAME = "SERVICE_NAME" ENV_AR_REPO = "AR_REPO" ENV_VERSION = "VERSION" +ENV_CPU = "CLOUD_RUN_CPU" +ENV_MEMORY = "CLOUD_RUN_MEMORY" +ENV_MIN_INSTANCES = "CLOUD_RUN_MIN_INSTANCES" # Path to the Cloud Build config file written by the CLI. The rewritten # deploy script references it as ``--config="${REFLEX_CLOUDBUILD_YAML}"``. ENV_REFLEX_CLOUDBUILD_YAML = "REFLEX_CLOUDBUILD_YAML" @@ -136,6 +140,28 @@ default=None, help="The image version tag (sets VERSION). Defaults to a UTC timestamp.", ) +@click.option( + "--cpu", + "cpu", + default="1", + show_default=True, + help="Cloud Run CPU allocation, e.g. '1', '2', '4' (sets CLOUD_RUN_CPU).", +) +@click.option( + "--memory", + "memory", + default="1Gi", + show_default=True, + help="Cloud Run memory allocation, e.g. '512Mi', '1Gi', '2Gi' (sets CLOUD_RUN_MEMORY).", +) +@click.option( + "--min-instances", + "min_instances", + default=1, + show_default=True, + type=click.IntRange(min=0), + help="Minimum number of Cloud Run instances to keep warm (sets CLOUD_RUN_MIN_INSTANCES). Set to 0 to scale to zero.", +) @click.option( "--source", "source_dir", @@ -170,6 +196,9 @@ def deploy_command( service_name: str, ar_repo: str, version_tag: str | None, + cpu: str, + memory: str, + min_instances: int, source_dir: str, token: str | None, interactive: bool, @@ -252,6 +281,9 @@ def deploy_command( ENV_SERVICE_NAME: service_name, ENV_AR_REPO: ar_repo, ENV_VERSION: version_value, + ENV_CPU: cpu, + ENV_MEMORY: memory, + ENV_MIN_INSTANCES: str(min_instances), } console.info("Received deploy manifest from Reflex.") diff --git a/tests/units/reflex_cli/v2/test_gcp.py b/tests/units/reflex_cli/v2/test_gcp.py index c7ba4a3bb7a..8109504133f 100644 --- a/tests/units/reflex_cli/v2/test_gcp.py +++ b/tests/units/reflex_cli/v2/test_gcp.py @@ -157,6 +157,79 @@ def capture(**kwargs): assert get_mock.call_args.kwargs["headers"] == {"X-API-TOKEN": "fake-token"} +def test_gcp_deploy_forwards_resource_flags(mocker: MockFixture, tmp_path: Path): + """--cpu / --memory / --min-instances flow through to the deploy script env.""" + run_mock = _patch_environment(mocker) + _mock_manifest_response(mocker) + + result = runner.invoke( + hosting_cli, + [ + "deploy", + "--gcp", + "--gcp-project", + "p", + "--source", + str(tmp_path), + "--cpu", + "4", + "--memory", + "2Gi", + "--min-instances", + "0", + ], + input="y\n", + ) + + assert result.exit_code == 0, result.output + env_overrides = run_mock.call_args.kwargs["env_overrides"] + assert env_overrides["CLOUD_RUN_CPU"] == "4" + assert env_overrides["CLOUD_RUN_MEMORY"] == "2Gi" + assert env_overrides["CLOUD_RUN_MIN_INSTANCES"] == "0" + + +def test_gcp_deploy_resource_flags_have_defaults(mocker: MockFixture, tmp_path: Path): + """When the user omits the new flags, defaults reach the deploy script env.""" + run_mock = _patch_environment(mocker) + _mock_manifest_response(mocker) + + result = runner.invoke( + hosting_cli, + ["deploy", "--gcp", "--gcp-project", "p", "--source", str(tmp_path)], + input="y\n", + ) + + assert result.exit_code == 0, result.output + env_overrides = run_mock.call_args.kwargs["env_overrides"] + assert env_overrides["CLOUD_RUN_CPU"] == "1" + assert env_overrides["CLOUD_RUN_MEMORY"] == "1Gi" + assert env_overrides["CLOUD_RUN_MIN_INSTANCES"] == "1" + + +def test_gcp_deploy_rejects_negative_min_instances(mocker: MockFixture, tmp_path: Path): + """--min-instances is IntRange(min=0); negative values fail at the CLI layer.""" + run_mock = _patch_environment(mocker) + _mock_manifest_response(mocker) + + result = runner.invoke( + hosting_cli, + [ + "deploy", + "--gcp", + "--gcp-project", + "p", + "--source", + str(tmp_path), + "--min-instances", + "-1", + ], + ) + + assert result.exit_code == 2 + assert "min-instances" in result.output.lower() + assert run_mock.call_count == 0 + + def test_gcp_deploy_aborts_on_no(mocker: MockFixture, tmp_path: Path): """Declining the run prompt aborts before any staging.""" run_mock = _patch_environment(mocker)