feat(cli): Add deploy revisions list command (#7142)

### Summary
```
% langgraph deploy revisions list f5bb6e58-e2ab-40b9-9855-d6dd6d1051b6
No LangSmith API key found. Create one at Settings > API Keys in LangSmith.
Enter LangSmith API key: 
Revision ID                           Status    Created At                 
------------------------------------  --------  ---------------------------
fe05f163-58cf-4783-a918-0c78a8fab0c7  BUILDING  2026-03-12T20:47:14.626436Z
03c32fcf-544f-4517-a016-c7a7243b898d  DEPLOYED  2026-03-12T20:30:09.199275Z
29605810-29cc-4110-ac05-d43e564827d0  DEPLOYED  2026-03-12T19:56:36.232427Z
```
This commit is contained in:
Andrew Nguonly
2026-03-19 16:55:19 -07:00
committed by GitHub
parent 54c92757f2
commit cc9acc1ba9
4 changed files with 283 additions and 22 deletions
+83 -22
View File
@@ -31,7 +31,11 @@ from langgraph_cli.helpers import format_log_entry, level_fg, resolve_deployment
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
from langgraph_cli.progress import Progress
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
from langgraph_cli.util import format_deployments_table, warn_non_wolfi_distro
from langgraph_cli.util import (
format_deployments_table,
format_revisions_table,
warn_non_wolfi_distro,
)
from langgraph_cli.version import __version__
RESERVED_ENV_VARS = frozenset(
@@ -331,25 +335,25 @@ class NestedHelpGroup(click.Group):
self, ctx: click.Context, formatter: click.HelpFormatter
) -> None:
command_entries: list[tuple[str, click.Command]] = []
# Collect the top-level commands first, then append one level of nested
# subcommands using names like "deploy list" so they show up in the
# top-level help output.
for command_name in self.list_commands(ctx):
command = self.get_command(ctx, command_name)
if command is None or command.hidden:
continue
command_entries.append((command_name, command))
if isinstance(command, click.Group):
# Build a child context so Click resolves the subcommands the same
# way it would for the nested group itself.
sub_ctx = click.Context(command, info_name=command_name, parent=ctx)
for subcommand_name in command.list_commands(sub_ctx):
subcommand = command.get_command(sub_ctx, subcommand_name)
if subcommand is None or subcommand.hidden:
continue
command_entries.append(
(f"{command_name} {subcommand_name}", subcommand)
def collect_commands(
group: click.Group, parent_ctx: click.Context, prefix: str = ""
) -> None:
for command_name in group.list_commands(parent_ctx):
command = group.get_command(parent_ctx, command_name)
if command is None or command.hidden:
continue
qualified_name = f"{prefix} {command_name}" if prefix else command_name
command_entries.append((qualified_name, command))
if isinstance(command, click.Group):
sub_ctx = click.Context(
command,
info_name=qualified_name,
parent=parent_ctx,
)
collect_commands(command, sub_ctx, qualified_name)
collect_commands(self, ctx)
# Compute the available width for help text up front so we can truncate
# descriptions before handing them to Click. That keeps each command on
@@ -373,13 +377,30 @@ class DeployGroup(NestedHelpGroup):
"""Group that treats leading '-' args as passthrough docker flags."""
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
"""Treat leading option-like subcommand tokens as passthrough args.
Click stores the unresolved nested command token on the context after
``Group.parse_args()`` runs, but the backing attribute changed across
supported Click versions. Click 8.1.x stores the value directly on
``protected_args``, while Click 8.2+ stores it on ``_protected_args``
and exposes ``protected_args`` as a deprecated compatibility property.
Since this package allows ``click>=8.1.7``, we need to check both
names to support the full version range without relying on one
version-specific internal detail.
"""
result = super().parse_args(ctx, args)
if ctx._protected_args and ctx._protected_args[0].startswith("-"):
protected_args = ctx.__dict__.get("protected_args")
if protected_args is None:
protected_args = ctx.__dict__.get("_protected_args", [])
if protected_args and protected_args[0].startswith("-"):
# Click stores the would-be subcommand in _protected_args; if it looks
# like an option (e.g. --build-arg) treat it as passthrough docker
# args instead of insisting on a nested command.
ctx.args = [*ctx._protected_args, *ctx.args]
ctx._protected_args = []
ctx.args = [*protected_args, *ctx.args]
if "protected_args" in ctx.__dict__:
ctx.protected_args = []
elif "_protected_args" in ctx.__dict__:
ctx._protected_args = []
return ctx.args
return result
@@ -788,6 +809,13 @@ def deploy(ctx: click.Context, **_: object):
return ctx.forward(_deploy, docker_build_args=docker_build_args)
@deploy.group(
"revisions", cls=NestedHelpGroup, help="[Beta] Manage deployment revisions."
)
def deploy_revisions() -> None:
pass
@_deploy_base_options()
@click.command(context_settings=dict(ignore_unknown_options=True))
def _deploy(
@@ -1225,6 +1253,39 @@ def deploy_list(api_key: str | None, host_url: str | None, name_contains: str) -
click.echo(format_deployments_table(deployments))
@OPT_HOST_API_KEY
@OPT_HOST_URL
@click.option(
"--limit",
type=int,
default=10,
show_default=True,
help="Maximum number of revisions to return.",
)
@click.argument("deployment_id")
@deploy_revisions.command(
"list",
help=(
"[Beta] List revisions for a LangSmith Deployment.\n\n"
"Use the `deploy list` command to list deployment IDs."
),
)
def deploy_revisions_list(
api_key: str | None, host_url: str | None, limit: int, deployment_id: str
) -> None:
client = _create_host_backend_client(host_url, api_key)
response = _call_host_backend_with_optional_tenant(
client,
lambda c: c.list_revisions(deployment_id, limit=limit),
)
resources = response.get("resources", []) if isinstance(response, dict) else []
revisions = [item for item in resources if isinstance(item, dict)]
if not revisions:
click.echo(f"No revisions found for deployment {deployment_id}.")
return
click.echo(format_revisions_table(revisions))
@OPT_HOST_API_KEY
@OPT_HOST_URL
@click.option(