WIP: chore(cli): support DR for deploy command (#7098)

- [ ] **Add tests and docs**: If you're adding a new integration, you
must include:
1. A test for the integration, preferably unit tests that do not rely on
network access,
2. An example notebook showing its use. It lives in
`docs/docs/integrations` directory.

- [ ] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. We will not consider a
PR unless these three are passing in CI. See [contribution
guidelines](https://docs.langchain.com/oss/python/contributing/overview)
for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
This commit is contained in:
Quanzheng Long
2026-03-10 10:03:32 -07:00
committed by GitHub
parent 4b4b91c7e7
commit db40389fbd
4 changed files with 103 additions and 6 deletions
+19 -2
View File
@@ -13,6 +13,7 @@ import tempfile
import time
from collections.abc import Callable, Sequence
from contextlib import contextmanager
from typing import Any
import click
import click.exceptions
@@ -715,6 +716,13 @@ def deploy(
secrets = _secrets_from_env(env_vars)
# Determine language and runtime mode from config for host backend
is_python = bool(config_json.get("python_version")) or not config_json.get(
"node_version"
)
deploy_engine_runtime_mode = "distributed" if is_python else "combined_queue_server"
deploy_api_version = api_version or config_json.get("api_version")
# Use buildx to cross-compile for amd64 when running on a non-x86_64 host
# (e.g. Apple Silicon). On amd64 hosts, plain docker build is sufficient.
needs_buildx = platform.machine() != "x86_64"
@@ -858,13 +866,16 @@ def deploy(
if needs_creation:
log_step(f"{step}. Creating deployment '{name}'")
payload = {
payload: dict[str, Any] = {
"name": name,
"source": "internal_docker",
"source_config": {"deployment_type": deployment_type},
"source_revision_config": {},
"secrets": secrets,
"engine_runtime_mode": deploy_engine_runtime_mode,
}
if deploy_api_version:
payload["deployed_api_version"] = deploy_api_version
created = client.create_deployment(payload)
created_id = created.get("id") if isinstance(created, dict) else None
if not isinstance(created_id, str) or not created_id:
@@ -973,7 +984,13 @@ def deploy(
# -- Step: Update deployment --
log_step(f"{step}. Updating deployment {deployment_id}")
updated = client.update_deployment(deployment_id, remote_image, secrets=secrets)
updated = client.update_deployment(
deployment_id,
remote_image,
secrets=secrets,
engine_runtime_mode=deploy_engine_runtime_mode,
deployed_api_version=deploy_api_version,
)
tenant_id = updated.get("tenant_id") if isinstance(updated, dict) else None
if tenant_id:
status_url = (
+6
View File
@@ -82,12 +82,18 @@ class HostBackendClient:
deployment_id: str,
image_uri: str,
secrets: list[dict[str, str]] | None = None,
engine_runtime_mode: str | None = None,
deployed_api_version: str | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"source_revision_config": {"image_uri": image_uri},
}
if secrets is not None:
payload["secrets"] = secrets
if engine_runtime_mode is not None:
payload["engine_runtime_mode"] = engine_runtime_mode
if deployed_api_version is not None:
payload["deployed_api_version"] = deployed_api_version
return self._request(
"PATCH",
f"/v2/deployments/{deployment_id}",
+5 -4
View File
@@ -946,18 +946,19 @@ def test_prepare_args_and_stdin_distributed_mode() -> None:
api_version="0.7.67",
)
# API service should use langgraph-api base image
assert "FROM langchain/langgraph-api:" in actual_stdin
# API service should use langgraph-api base image with pinned version
assert "FROM langchain/langgraph-api:0.7.67-py3.11" in actual_stdin
# Distributed mode sets N_JOBS_PER_WORKER=0 on the API service
assert 'N_JOBS_PER_WORKER: "0"' in actual_stdin
# Orchestrator service present
# Orchestrator service present with pinned version
assert "langgraph-orchestrator:" in actual_stdin
assert "langchain/langgraph-orchestrator-licensed:0.7.67" in actual_stdin
# Executor service present with correct base image
assert "langgraph-executor:" in actual_stdin
assert "FROM langchain/langgraph-executor:" in actual_stdin
assert "FROM langchain/langgraph-executor:0.7.67-py3.11" in actual_stdin
assert "executor_entrypoint.sh" in actual_stdin
@@ -152,6 +152,79 @@ def test_update_deployment_no_secrets(client):
assert result == {"ok": True}
def test_update_deployment_with_engine_runtime_mode():
"""Verify engine_runtime_mode is included in the PATCH payload."""
import json
def handler(req: httpx.Request) -> httpx.Response:
body = json.loads(req.content)
assert body["engine_runtime_mode"] == "distributed"
assert body["source_revision_config"]["image_uri"] == "img:v1"
return httpx.Response(200, json={"id": "dep-1"})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
result = c.update_deployment(
"dep-1", "img:v1", engine_runtime_mode="distributed"
)
assert result == {"id": "dep-1"}
def test_update_deployment_with_deployed_api_version():
"""Verify deployed_api_version is included in the PATCH payload."""
import json
def handler(req: httpx.Request) -> httpx.Response:
body = json.loads(req.content)
assert body["deployed_api_version"] == "0.3.5"
assert body["engine_runtime_mode"] == "distributed"
assert body["source_revision_config"]["image_uri"] == "img:v2"
assert body["secrets"] == [{"name": "K", "value": "V"}]
return httpx.Response(200, json={"id": "dep-2"})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
result = c.update_deployment(
"dep-2",
"img:v2",
secrets=[{"name": "K", "value": "V"}],
engine_runtime_mode="distributed",
deployed_api_version="0.3.5",
)
assert result == {"id": "dep-2"}
def test_update_deployment_omits_none_fields():
"""Verify None values for optional fields are not sent in payload."""
import json
def handler(req: httpx.Request) -> httpx.Response:
body = json.loads(req.content)
assert "engine_runtime_mode" not in body
assert "deployed_api_version" not in body
assert "secrets" not in body
return httpx.Response(200, json={"ok": True})
c = HostBackendClient("https://api.example.com", "test-key")
c._client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
timeout=30,
)
c.update_deployment("dep-3", "img:v1")
def test_list_revisions(client):
result = client.list_revisions("dep-123", limit=5)
assert result == {"ok": True}