From 48051304a90233ca052a8833cfeeb45076814585 Mon Sep 17 00:00:00 2001 From: Rafid Saad Date: Sun, 13 Sep 2026 14:02:55 -0700 Subject: [PATCH] fix(cli): keep workspace alias changes minimal --- libs/cli/README.md | 21 --- libs/cli/langgraph_cli/deploy.py | 16 +-- .../tests/unit_tests/test_deploy_helpers.py | 122 +++++------------- 3 files changed, 36 insertions(+), 123 deletions(-) diff --git a/libs/cli/README.md b/libs/cli/README.md index f26e6795b..cac7808bb 100644 --- a/libs/cli/README.md +++ b/libs/cli/README.md @@ -86,27 +86,6 @@ langgraph dockerfile SAVE_PATH [OPTIONS] -c, --config FILE Config file path ``` -### `langgraph deploy` - -Build and deploy your app to LangSmith Deployment: - -```bash -langgraph deploy -``` - -Set `LANGSMITH_WORKSPACE_ID` to select a workspace. The legacy name -`LANGSMITH_TENANT_ID` remains supported. Resolution uses the first nonempty value -in this order: - -1. `LANGSMITH_WORKSPACE_ID` in the configured env file/inline env, or default `.env`. -2. `LANGSMITH_WORKSPACE_ID` in the shell environment. -3. `LANGSMITH_TENANT_ID` in the configured env file/inline env, or default `.env`. -4. `LANGSMITH_TENANT_ID` in the shell environment. - -This applies to `deploy` and its subcommands. The selected workspace is sent in -the existing `X-Tenant-ID` request header; neither name is uploaded as a deployment -secret. - ## Configuration The CLI uses a `langgraph.json` configuration file with these key settings: diff --git a/libs/cli/langgraph_cli/deploy.py b/libs/cli/langgraph_cli/deploy.py index 6a2b43ad7..a4ecfd974 100644 --- a/libs/cli/langgraph_cli/deploy.py +++ b/libs/cli/langgraph_cli/deploy.py @@ -51,6 +51,7 @@ RESERVED_ENV_VARS = frozenset( "LANGGRAPH_AUTH_TYPE", "LANGSMITH_AUTH_ENDPOINT", "LANGSMITH_TENANT_ID", + "LANGSMITH_WORKSPACE_ID", "LANGSMITH_AUTH_VERIFY_TENANT_ID", "LANGSMITH_HOST_PROJECT_ID", "LANGSMITH_HOST_PROJECT_NAME", @@ -63,8 +64,6 @@ RESERVED_ENV_VARS = frozenset( "DD_TRACE_REDIS_ENABLED", "LANGSMITH_DEPLOYMENT_NAME", "LANGGRAPH_CLOUD_LICENSE_KEY", - # CLI workspace selection must not override the deployed runtime. - "LANGSMITH_WORKSPACE_ID", # ALLOWED_SELF_HOSTED_ENV_VARS (rejected for non-self-hosted) "LANGSMITH_API_KEY", "LANGSMITH_ENDPOINT", @@ -1260,13 +1259,11 @@ def _create_host_backend_client( fg="yellow", ) resolved_api_key = click.prompt("Enter LangSmith API key", hide_input=True) - # Prefer the public workspace name, retaining tenant as a legacy alias. - # As with API keys, config/.env takes precedence over the shell per name. tenant_id = ( - env_vars.get("LANGSMITH_WORKSPACE_ID") - or os.environ.get("LANGSMITH_WORKSPACE_ID") - or env_vars.get("LANGSMITH_TENANT_ID") + env_vars.get("LANGSMITH_TENANT_ID") or os.environ.get("LANGSMITH_TENANT_ID") + or env_vars.get("LANGSMITH_WORKSPACE_ID") + or os.environ.get("LANGSMITH_WORKSPACE_ID") ) return HostBackendClient(host_url, resolved_api_key, tenant_id=tenant_id) @@ -1296,8 +1293,7 @@ def _call_host_backend_with_optional_tenant( if _no_input: raise click.ClickException( "API key is org-scoped and requires a workspace ID. " - "Set LANGSMITH_WORKSPACE_ID (or LANGSMITH_TENANT_ID) " - "in your environment or .env file, or " + "Set LANGSMITH_TENANT_ID in your .env file or " "use a workspace-scoped API key." ) from None click.secho( @@ -1553,8 +1549,6 @@ def _deploy_base_options( "[Beta] Build and deploy a LangGraph image to LangSmith Deployment.\n\n" "This command is in beta and under active development. " "Expect frequent updates and improvements.\n\n" - "Set LANGSMITH_WORKSPACE_ID in your environment or .env file to select " - "a workspace. LANGSMITH_TENANT_ID is supported as a fallback.\n\n" "Run from the root of your LangGraph project (where langgraph.json " "is located). This command also accepts build flags (--base-image, " "--config, --pull, etc.). See 'langgraph build --help' for details." diff --git a/libs/cli/tests/unit_tests/test_deploy_helpers.py b/libs/cli/tests/unit_tests/test_deploy_helpers.py index f789189de..b3911d0bd 100644 --- a/libs/cli/tests/unit_tests/test_deploy_helpers.py +++ b/libs/cli/tests/unit_tests/test_deploy_helpers.py @@ -355,7 +355,7 @@ class TestCallHostBackendWithOptionalTenant: client = self._make_client( lambda req: httpx.Response(403, text=requires_workspace) ) - with pytest.raises(click.ClickException, match="LANGSMITH_WORKSPACE_ID"): + with pytest.raises(click.ClickException, match="workspace"): _call_host_backend_with_optional_tenant( client, lambda c: c.list_deployments() ) @@ -541,99 +541,39 @@ class TestCreateHostBackendClientNoInput: assert client is not None -class TestWorkspaceSelection: - @pytest.mark.parametrize( - "configured_workspace,shell_workspace,configured_tenant,shell_tenant,expected", - [ - ("config-workspace", None, None, None, "config-workspace"), - (None, "shell-workspace", None, None, "shell-workspace"), - (None, None, "config-tenant", None, "config-tenant"), - (None, None, None, "shell-tenant", "shell-tenant"), - ( - "config-workspace", - "shell-workspace", - "config-tenant", - "shell-tenant", - "config-workspace", - ), - ( - None, - "shell-workspace", - "config-tenant", - "shell-tenant", - "shell-workspace", - ), - (None, None, "config-tenant", "shell-tenant", "config-tenant"), - ("", "shell-workspace", "config-tenant", None, "shell-workspace"), - ("", "", "config-tenant", "shell-tenant", "config-tenant"), - (None, None, "", "shell-tenant", "shell-tenant"), - (None, None, None, None, None), - ("", "", "", "", None), - ], +@pytest.mark.parametrize("source", ["config", "shell"]) +@pytest.mark.parametrize("existing_tenant", [None, "existing-tenant"]) +def test_workspace_id_alias(monkeypatch, source, existing_tenant): + monkeypatch.delenv("LANGSMITH_WORKSPACE_ID", raising=False) + monkeypatch.delenv("LANGSMITH_TENANT_ID", raising=False) + env_vars = {} + if source == "config": + env_vars["LANGSMITH_WORKSPACE_ID"] = "workspace" + else: + monkeypatch.setenv("LANGSMITH_WORKSPACE_ID", "workspace") + if existing_tenant: + monkeypatch.setenv("LANGSMITH_TENANT_ID", existing_tenant) + + def handler(request): + assert request.headers["X-Tenant-ID"] == (existing_tenant or "workspace") + return httpx.Response(200, json={"resources": []}) + + monkeypatch.setattr( + httpx, "HTTPTransport", lambda **kwargs: httpx.MockTransport(handler) ) - def test_request_workspace_header( - self, - monkeypatch, - configured_workspace, - shell_workspace, - configured_tenant, - shell_tenant, - expected, - ): - env_vars = {} - for name, configured, shell in [ - ("LANGSMITH_WORKSPACE_ID", configured_workspace, shell_workspace), - ("LANGSMITH_TENANT_ID", configured_tenant, shell_tenant), - ]: - monkeypatch.delenv(name, raising=False) - if configured is not None: - env_vars[name] = configured - if shell is not None: - monkeypatch.setenv(name, shell) + client = _create_host_backend_client( + "https://api.example.com", "test-key", env_vars + ) + try: + client.list_deployments() + finally: + client._client.close() - def handler(request): - assert request.headers.get("X-Tenant-ID") == expected - return httpx.Response(200, json={"deployments": []}) - monkeypatch.setattr( - httpx, "HTTPTransport", lambda **kwargs: httpx.MockTransport(handler) - ) - client = _create_host_backend_client( - "https://api.example.com", "test-key", env_vars - ) - try: - assert client.list_deployments() == {"deployments": []} - finally: - client._client.close() - - @pytest.mark.parametrize("name", ["LANGSMITH_WORKSPACE_ID", "LANGSMITH_TENANT_ID"]) - def test_loads_workspace_from_dotenv(self, monkeypatch, tmp_path, name): - monkeypatch.chdir(tmp_path) - monkeypatch.delenv("LANGSMITH_WORKSPACE_ID", raising=False) - monkeypatch.delenv("LANGSMITH_TENANT_ID", raising=False) - (tmp_path / ".env").write_text(f"{name}=file-workspace\n") - - def handler(request): - assert request.headers["X-Tenant-ID"] == "file-workspace" - return httpx.Response(200, json={"deployments": []}) - - monkeypatch.setattr( - httpx, "HTTPTransport", lambda **kwargs: httpx.MockTransport(handler) - ) - client = _create_host_backend_client("https://api.example.com", "test-key") - try: - assert client.list_deployments() == {"deployments": []} - finally: - client._client.close() - - def test_workspace_selection_is_not_uploaded_as_secrets(self): - assert _secrets_from_env( - { - "LANGSMITH_WORKSPACE_ID": "workspace-id", - "LANGSMITH_TENANT_ID": "legacy-workspace-id", - "APP_SETTING": "value", - } - ) == [{"name": "APP_SETTING", "value": "value"}] +def test_workspace_id_is_not_uploaded_as_secret(): + assert _secrets_from_env( + {"LANGSMITH_WORKSPACE_ID": "workspace", "APP_SETTING": "value"} + ) == [{"name": "APP_SETTING", "value": "value"}] class TestSmithDashboardBaseUrl: