diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py index dbde778e..5f7b10cc 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -44,7 +44,9 @@ JIG_VOLUMES_HELP_EXAMPLES, EVALS_CREATE_HELP_EXAMPLES, FILES_UPLOAD_HELP_EXAMPLES, + JIG_ROLLBACK_HELP_EXAMPLES, BETA_CLUSTERS_HELP_EXAMPLES, + JIG_REVISIONS_HELP_EXAMPLES, MODELS_UPLOAD_HELP_EXAMPLES, JIG_JOB_STATUS_HELP_EXAMPLES, JIG_SECRETS_SET_HELP_EXAMPLES, @@ -582,6 +584,34 @@ async def run_command() -> None: (f"{_CLI}.beta.jig.jig:queue_status_cli"), name="queue-status", help="Get queue metrics for the deployment" ) jig_app.command((f"{_CLI}.beta.jig.jig:list_deployments_cli"), name="list", alias="ls", help="List all deployments") +jig_app.command( + (f"{_CLI}.beta.jig.jig:rollback_cli"), + name="rollback", + help="Roll back the deployment to a previous revision", + help_epilogue=JIG_ROLLBACK_HELP_EXAMPLES, +) + +revisions_app = jig_app.command( + App( + name="revisions", + help="Inspect a deployment's revision history", + group="Subcommands", + help_epilogue=JIG_REVISIONS_HELP_EXAMPLES, + ) +) +revisions_app.command( + (f"{_CLI}.beta.jig.jig:revisions_list_cli"), + name="list", + alias="ls", + help="List revision history (newest first)", + help_epilogue=JIG_REVISIONS_HELP_EXAMPLES, +) +revisions_app.command( + (f"{_CLI}.beta.jig.jig:revisions_get_cli"), + name="get", + alias=("retrieve", "describe"), + help="Get the full configuration of a specific revision", +) secrets_app = jig_app.command( App(name="secrets", help="Manage deployment secrets", group="Subcommands", help_epilogue=JIG_SECRETS_HELP_EXAMPLES) diff --git a/src/together/lib/cli/api/beta/jig/jig.py b/src/together/lib/cli/api/beta/jig/jig.py index 002ccefd..363f512c 100644 --- a/src/together/lib/cli/api/beta/jig/jig.py +++ b/src/together/lib/cli/api/beta/jig/jig.py @@ -1224,6 +1224,22 @@ def logs( return jig.follow_logs(replica_id, revision, version) if follow else jig.logs(replica_id, revision, version) +def rollback(jig: Jig, revision_identifier: str, detach: bool) -> Any: + """Roll back the deployment to a previous revision, then track it back to ready""" + res = jig.together.post( + f"/deployments/{jig.name}/rollback", + body={"revision_identifier": revision_identifier}, + cast_to=httpx.Response, + ) + console.print( + f"\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS} Rolling back {jig.name} to revision {revision_identifier}" + ) + if detach: + return res + jig.track(jig.api.retrieve(jig.name)) + return None + + def destroy(jig: Jig) -> str: """Destroy deployment""" jig.api.destroy(jig.name) @@ -1250,6 +1266,74 @@ def list_deployments(jig: Jig) -> Any: return jig.api.with_raw_response.list() +def get_revisions(jig: Jig, revision_id: str) -> Any: + """Get the full configuration of a specific revision""" + return jig.together.get( + f"/deployments/{jig.name}/revisions/{revision_id}", + cast_to=httpx.Response, + ) + + +def _fetch_revisions(jig: Jig, limit: int | None, before: int | None) -> httpx.Response: + params: dict[str, Any] = {} + if limit is not None: + params["limit"] = limit + if before: + params["before"] = before + return jig.together.get( + f"/deployments/{jig.name}/revisions", + options={"params": params}, + cast_to=httpx.Response, + ) + + +def _to_revision_list(jig: Jig, revisions: httpx.Response) -> None: + """Render an already-fetched revision history response as a table (newest first)""" + body = revisions.json() + events: list[dict[str, Any]] = body.get("data") or [] + + table = ListTable( + title=f"Revisions for {jig.name}", + empty_message=f"No revisions found for deployment {jig.name}", + ) + table.add_column("Event", ratio=None) + table.add_primary_column("Revision") + table.add_column("Action", ratio=None) + table.add_column("Activated", ratio=None) + table.add_column("Image", ratio=2) + table.add_column("Revision ID", ratio=2) + + last_event = events[-1] if events else None + last_event_number = last_event.get("event_number") if last_event else None + has_more_events = last_event is not None and last_event.get("action") != "create" + + for event in events: + event_number = event.get("event_number") + revision_number = event.get("revision_number") + image = event.get("image") + activated_at = event.get("activated_at") + time_since_activated = _age(activated_at) + table.add_row( + f"#{event_number}" if event_number is not None else "-", + f"#{revision_number}" if revision_number is not None else "-", + event.get("action") or "-", + f"{activated_at} [primary]({time_since_activated} ago)[/primary]", + jig.short_image(image) if image else "-", + event.get("revision_id") or "-", + ) + + console.print(table) + + console.print("\n[dim]To see view the spec of a revision, run: [dim]") + console.print(f"[dim]-[/dim] [primary]tg beta jig revisions get [/primary]") + + if has_more_events: + console.print("\n[dim]To see older revisions for this deployment, run:[dim]") + console.print(f"[dim]-[/dim] [primary]tg beta jig revisions list --before {last_event_number}[/primary]\n") + else: + console.print("\n[muted]You've reached the end of the revision history for this deployment.[/muted]\n") + + def secrets_set(jig: Jig, name: str, value: str, description: str) -> None: """Set a secret (create or update)""" jig.set_secret(name, value, description) @@ -1571,6 +1655,54 @@ def list_deployments_cli( _run_jig_cmd(config, toml_config, list_deployments) +def revisions_list_cli( + limit: Annotated[ + Optional[int], Parameter(name="--limit", help="Maximum number of events to return (default 20, max 100)") + ] = None, + before: Annotated[ + Optional[int], + Parameter(name="--before", help="Return events before this event number (exclusive, for pagination)"), + ] = None, + *, + config: CLIConfigParameter, + toml_config: TomlConfigParameter = None, +) -> None: + """List the deployment's revision history (newest first).""" + + def inner(jig: Jig) -> Any: + revisions = _fetch_revisions(jig, limit, before) + if config.json: + return revisions + _to_revision_list(jig, revisions) + return None + + _run_jig_cmd(config, toml_config, inner) + + +def revisions_get_cli( + revision_id: Annotated[str, Parameter(name="--revision-id", help="Revision UUID")], + *, + config: CLIConfigParameter, + toml_config: TomlConfigParameter = None, +) -> None: + """Get the full configuration of a specific revision.""" + _run_jig_cmd(config, toml_config, lambda jig: get_revisions(jig, revision_id)) + + +def rollback_cli( + revision: Annotated[str, Parameter(name="--revision", help="Revision number or UUID to roll back to")], + detach: Annotated[ + bool, + Parameter(help="Return immediately without waiting for the rollback to complete", negative=()), + ] = False, + *, + config: CLIConfigParameter, + toml_config: TomlConfigParameter = None, +) -> None: + """Roll back the deployment to a previous revision.""" + _run_jig_cmd(config, toml_config, lambda jig: rollback(jig, revision, detach)) + + def secrets_set_cli( name: Annotated[str, Parameter(name="--name", help="Secret name")], value: Annotated[str, Parameter(name="--value", help="Secret value")], diff --git a/src/together/lib/cli/utils/_console.py b/src/together/lib/cli/utils/_console.py index ad896db9..2e47a249 100644 --- a/src/together/lib/cli/utils/_console.py +++ b/src/together/lib/cli/utils/_console.py @@ -18,7 +18,7 @@ "prompt.choices": "#caaef5", # Purple 300 ⭐ "prompt.default": "dim #98a0b3", # Grey 400 ⭐ # Table styles - "table.header": "#414858", # Purple 300 ⭐ (lighter when bold) + "table.header": "#caaef5", # Purple 300 ⭐ (lighter when bold) "table.border": "#626b84", # Grey 600 ⭐ "table.row": "#c4c9d4", # Grey 300 ⭐ # Progress/Loading diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py index 2bbe64ea..41ac7c30 100644 --- a/src/together/lib/cli/utils/_help_examples.py +++ b/src/together/lib/cli/utils/_help_examples.py @@ -404,6 +404,9 @@ [dim]-[/dim] List deployments or tear one down: [primary]tg beta jig list[/primary] [primary]tg beta jig destroy[/primary] + +[dim]-[/dim] List revision history: + [primary]tg beta jig revisions list[/primary] """ JIG_SECRETS_HELP_EXAMPLES = """[dim]Examples:[/dim] @@ -466,6 +469,28 @@ [primary]tg beta jig destroy[/primary] """ +JIG_REVISIONS_HELP_EXAMPLES = """[dim]Examples:[/dim] +[dim]-[/dim] Show revision history (newest first, up to 10 events): + [primary]tg beta jig revisions list[/primary] + +[dim]-[/dim] View a page of 5 revision events, event numbers 5 to 10 (if present): + [primary]tg beta jig revisions list --limit 5 --before 11 [/primary] + +[dim]-[/dim] Inspect the full configuration of one revision (by revision number or revision ID): + [primary]tg beta jig revisions get 4 [/primary] +""" + +JIG_ROLLBACK_HELP_EXAMPLES = """[dim]Examples:[/dim] +[dim]-[/dim] Roll back to a revision by number (from [primary]revisions list[/primary]): + [primary]tg beta jig rollback 7[/primary] + +[dim]-[/dim] Roll back to a revision by UUID: + [primary]tg beta jig rollback 2accdd2a-9094-4682-9016-06e4f14daaff[/primary] + +[dim]-[/dim] Start the rollback and return immediately, without tracking: + [primary]tg beta jig rollback 7 --detach[/primary] +""" + JIG_LOGS_HELP_EXAMPLES = """[dim]Examples:[/dim] [dim]-[/dim] Print recent logs once: [primary]tg beta jig logs[/primary] diff --git a/uv.lock b/uv.lock index f248d558..0c102532 100644 --- a/uv.lock +++ b/uv.lock @@ -1559,7 +1559,7 @@ wheels = [ [[package]] name = "together" -version = "2.16.0" +version = "2.16.1" source = { editable = "." } dependencies = [ { name = "anyio" },