feat: add langsmith deployment subcommand (LangGraph CLI port)#32
feat: add langsmith deployment subcommand (LangGraph CLI port)#32William FH (hinthornw) wants to merge 6 commits intomainfrom
langsmith deployment subcommand (LangGraph CLI port)#32Conversation
Translate the LangGraph Python CLI into native Go as a subcommand of the langsmith CLI. This adds `langsmith deployment <cmd>` with the following commands: - `up` — launch LangGraph API server via Docker Compose (Redis, Postgres, API) - `build` — build a Docker image from langgraph.json config - `dev` — run dev server (delegates to local `langgraph` or `uvx`) - `deploy` — full build+push+deploy flow to LangSmith - `deploy list` — list existing deployments - `deploy delete` — delete a deployment - `deploy logs` — fetch build/deploy logs - `dockerfile` — generate Dockerfile (and optionally docker-compose.yml) - `new` — scaffold a new project from a template Includes internal/deployment package with config validation, Dockerfile generation, Docker Compose YAML generation, host backend API client, subprocess execution, progress spinner, and template management. 48 tests covering config validation, docker compose generation, API client, helper utilities, command tree structure, flag verification, and end-to-end deploy list integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Check error return values (errcheck) - Lowercase error strings (staticcheck ST1005) - Simplify loop to append (gosimple S1011) - Simplify bool return (gosimple S1008) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Zip Slip: validate extracted file paths stay within destination directory - Unsafe quoting: properly escape JSON in Dockerfile ENV directives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use filepath.Clean + HasPrefix("..") check directly on the name
before joining, which is the standard pattern CodeQL recognizes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeQL tracks taint from f.Name through to filepath.Join result. Guard the targetPath directly with strings.HasPrefix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docker.go: debugger depends_on langgraph-postgres only when using local DB (includeDB), not when external postgres-uri is provided - hostbackend.go: URL-encode query parameters using net/url.Values to prevent injection via special characters in user input - deployment.go: follow mode in logs command tracks last timestamp and updates start_time in payload to avoid re-printing old entries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| if err != nil { | ||
| exitErrorf("Creating temp dir: %v", err) | ||
| } | ||
| defer func() { _ = os.RemoveAll(tmpDir) }() |
There was a problem hiding this comment.
🔴 Temp directory with Docker registry auth token not cleaned up on error due to os.Exit skipping defer
In the deploy command, a temp directory is created at line 556 to store a Docker config containing a base64-encoded OAuth access token for the container registry. The cleanup is registered via defer func() { _ = os.RemoveAll(tmpDir) }() at line 560. However, all error paths after this point call exitErrorf (internal/cmd/root.go:111-113), which calls os.Exit(1). In Go, os.Exit does not run deferred functions, so the temp directory (containing config.json with the registry auth token) persists on disk in /tmp/langsmith-deploy-*/ after any failure in tagging (line 579), pushing (line 584), updating deployment (line 593), or timeout (line 614).
Prompt for agents
In internal/cmd/deployment.go, the defer on line 560 won't run because exitErrorf calls os.Exit(1), which skips deferred functions. To fix this, either:
1. Replace the defer with explicit cleanup calls before each exitErrorf after line 560 (lines 572, 579, 584, 593, 614, 641), e.g., add `_ = os.RemoveAll(tmpDir)` before each exitErrorf call.
2. Or refactor the deploy command's Run function to return errors instead of calling os.Exit directly, and handle cleanup in a wrapper that calls os.RemoveAll(tmpDir) before exiting.
Option 1 is simpler. You would add `_ = os.RemoveAll(tmpDir)` before each exitErrorf call on lines 572, 579, 584, 593, 614, and 641.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // Update deployment with secrets | ||
| secrets := deployment.SecretsFromEnv(cfg) | ||
| fmt.Fprintln(os.Stderr, "Updating deployment...") | ||
| secretMaps := append([]map[string]string{}, secrets...) |
There was a problem hiding this comment.
🟡 SecretsFromEnv nil return converted to non-nil empty slice, always sending empty secrets to API
On line 590, append([]map[string]string{}, secrets...) converts a nil return from SecretsFromEnv into a non-nil empty slice []map[string]string{}. This is then passed to UpdateDeployment (internal/deployment/hostbackend.go:154), which uses if secrets != nil (line 160) to decide whether to include "secrets" in the API payload. Because the append wrapper always produces a non-nil slice, the API always receives "secrets": [], even when the user has no env section in their config (the default is map[string]interface{}{} per internal/deployment/config.go:194). This could cause the API to clear any existing deployment secrets that were configured through other means (e.g., the LangSmith UI).
| secretMaps := append([]map[string]string{}, secrets...) | |
| _, err = client.UpdateDeployment(deploymentID, imageURI, secrets) |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
langsmith deployment <cmd>up,build,dev,deploy(withlist,delete,logs),dockerfile,newdevdelegates to locallanggraphif installed, otherwise usesuvx/uv(cache-first, no forced internet)internal/deploymentpackage: config validation, Dockerfile generation, Docker Compose YAML, host backend API client, subprocess execution, progress spinner, templatesCommands
langsmith deployment uplangsmith deployment build --tag imglangsmith deployment devlangsmith deployment deploylangsmith deployment deploy listlangsmith deployment deploy delete IDlangsmith deployment deploy logslangsmith deployment dockerfile PATHlangsmith deployment new PATH --template TTest plan
go test ./...)langsmith deployment deploy list --api-key ...langsmith deployment devwith local langgraph installlangsmith deployment upwith Docker🤖 Generated with Claude Code