From 359d3e0eff4317f97c29df5240ece17286261fc5 Mon Sep 17 00:00:00 2001 From: Nicholas Polimeni Date: Fri, 12 Jun 2026 17:06:20 -0700 Subject: [PATCH 1/2] add jig commands for new rollback and revisions features --- src/together/lib/cli/__init__.py | 30 ++++ src/together/lib/cli/api/beta/jig/jig.py | 128 ++++++++++++++++ src/together/lib/cli/utils/_console.py | 2 +- src/together/lib/cli/utils/_help_examples.py | 22 +++ tests/cli/test_beta_jig.py | 145 +++++++++++++++++++ uv.lock | 2 +- 6 files changed, 327 insertions(+), 2 deletions(-) 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..b895f3fc 100644 --- a/src/together/lib/cli/api/beta/jig/jig.py +++ b/src/together/lib/cli/api/beta/jig/jig.py @@ -1250,6 +1250,86 @@ def list_deployments(jig: Jig) -> Any: return jig.api.with_raw_response.list() +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_primary_column("Revision") + table.add_column("Event") + table.add_column("Action") + table.add_column("Image", ratio=2) + table.add_column("Activated", 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") + 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"#{number}" if number is not None else "-", + f"#{event_number}" if event_number is not None else "-", + event.get("action") or "-", + jig.short_image(image) if image else "-", + f"{activated_at} [primary]({time_since_activated} ago)[/primary]", + event.get("revision_id") or "-", + ) + + console.print(table) + + 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 revisions_get(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 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 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 +1651,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: revisions_get(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..d6c796c2 100644 --- a/src/together/lib/cli/utils/_help_examples.py +++ b/src/together/lib/cli/utils/_help_examples.py @@ -466,6 +466,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] Page through the 10 revision events, event numbers 90 to 100: + [primary]tg beta jig revisions list --limit 10 --before 101 [/primary] + +[dim]-[/dim] Inspect the full configuration of one revision: + [primary]tg beta jig revisions get or [/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 [/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/tests/cli/test_beta_jig.py b/tests/cli/test_beta_jig.py index 35a89402..8a135e51 100644 --- a/tests/cli/test_beta_jig.py +++ b/tests/cli/test_beta_jig.py @@ -429,3 +429,148 @@ async def upload_files(self, source: Path, prefix: str) -> None: patch_body = json.loads(cast(Call, patch_r.calls[0]).request.content.decode()) assert patch_body["content"] == {"type": "files", "source_prefix": "shared/4"} assert result.exit_code == 0 + + +_REVISION_ID = "11111111-1111-1111-1111-111111111111" + + +def _ready_deployment_body(revision_id: str) -> dict[str, object]: + """Minimal deployment body that jig.track() treats as up and ready.""" + return { + "name": _DEPLOY_NAME, + "object": "deployment", + "status": "Ready", + "environment_variables": [{"name": "TOGETHER_DEPLOYMENT_REVISION_ID", "value": revision_id}], + "replica_events": { + "replica-1": { + "revision_id": revision_id, + "replica_status": "Running", + "replica_ready_since": "2024-01-01T00:00:00Z", + } + }, + } + + +class TestBetaJigRevisions: + @pytest.mark.respx(base_url=base_url) + def test_list_json(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: + _write_jig_project(tmp_path) + payload = { + "object": "list", + "data": [{"object": "revision_event", "revision_number": 2, "revision_id": _REVISION_ID}], + "next_cursor": "1", + } + respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock(return_value=httpx.Response(200, json=payload)) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "revisions", "list", "--json"]) + + assert json.loads(result.output) == payload + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_list_table(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: + _write_jig_project(tmp_path) + payload = { + "object": "list", + "data": [ + { + "object": "revision_event", + "revision_number": 4, + "event_number": 7, + "revision_id": _REVISION_ID, + "action": "rollback", + "activated_at": "2024-01-01T00:00:00Z", + } + ], + } + respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock(return_value=httpx.Response(200, json=payload)) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "revisions", "list"]) + + assert "Revisions for" in result.output + assert "#4" in result.output + assert "#7" in result.output + # pagination hint uses the last event's event_number + assert "--before 7" in result.output + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_list_forwards_limit_and_before( + self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner + ) -> None: + _write_jig_project(tmp_path) + route = respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock( + return_value=httpx.Response(200, json={"object": "list", "data": []}) + ) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "revisions", "list", "--limit", "50", "--before", "3"]) + + request = cast(Call, route.calls[0]).request + assert request.url.params["limit"] == "50" + assert request.url.params["before"] == "3" + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_get_by_revision_id(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: + _write_jig_project(tmp_path) + payload = {"object": "revision", "revision_id": _REVISION_ID, "image": "registry/img:abc", "min_replicas": 1} + respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions/{_REVISION_ID}").mock( + return_value=httpx.Response(200, json=payload) + ) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "revisions", "get", "--revision-id", _REVISION_ID]) + + assert json.loads(result.output) == payload + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_get_not_found(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: + _write_jig_project(tmp_path) + respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions/missing").mock( + return_value=httpx.Response(404, json={"error": {"message": "revision not found"}}) + ) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "revisions", "get", "missing"]) + + assert "not found" in result.output.lower() + assert result.exit_code == 1 + + +class TestBetaJigRollback: + @pytest.mark.respx(base_url=base_url) + def test_rollback_detach(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: + _write_jig_project(tmp_path) + route = respx_mock.post(f"/deployments/{_DEPLOY_NAME}/rollback").mock( + return_value=httpx.Response(200, json={"name": _DEPLOY_NAME, "object": "deployment", "status": "Updating"}) + ) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "rollback", "7", "--detach"]) + + body = json.loads(cast(Call, route.calls[0]).request.content.decode()) + assert body == {"revision_identifier": "7"} + assert "Rolling back" in result.output + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_rollback_tracks_until_ready( + self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner + ) -> None: + _write_jig_project(tmp_path) + respx_mock.post(f"/deployments/{_DEPLOY_NAME}/rollback").mock( + return_value=httpx.Response(200, json=_ready_deployment_body(_REVISION_ID)) + ) + respx_mock.get(f"/deployments/{_DEPLOY_NAME}").mock( + return_value=httpx.Response(200, json=_ready_deployment_body(_REVISION_ID)) + ) + + with _chdir(tmp_path): + result = cli_runner.invoke(["beta", "jig", "rollback", "--revision", _REVISION_ID]) + + assert "Deployment successful" in result.output + assert result.exit_code == 0 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" }, From b34b72aaae716e9fd0e7d330583c8451b3777b45 Mon Sep 17 00:00:00 2001 From: Nicholas Polimeni Date: Mon, 15 Jun 2026 15:30:50 -0700 Subject: [PATCH 2/2] improve help section, remove unneeded tests --- src/together/lib/cli/api/beta/jig/jig.py | 66 +++++---- src/together/lib/cli/utils/_help_examples.py | 13 +- tests/cli/test_beta_jig.py | 145 ------------------- 3 files changed, 43 insertions(+), 181 deletions(-) diff --git a/src/together/lib/cli/api/beta/jig/jig.py b/src/together/lib/cli/api/beta/jig/jig.py index b895f3fc..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,14 @@ 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: @@ -1272,11 +1296,11 @@ def _to_revision_list(jig: Jig, revisions: httpx.Response) -> None: 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("Event") - table.add_column("Action") + table.add_column("Action", ratio=None) + table.add_column("Activated", ratio=None) table.add_column("Image", ratio=2) - table.add_column("Activated", ratio=2) table.add_column("Revision ID", ratio=2) last_event = events[-1] if events else None @@ -1285,50 +1309,30 @@ def _to_revision_list(jig: Jig, revisions: httpx.Response) -> None: for event in events: event_number = event.get("event_number") - number = event.get("revision_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"#{number}" if number is not None else "-", f"#{event_number}" if event_number is not None else "-", + f"#{revision_number}" if revision_number is not None else "-", event.get("action") or "-", - jig.short_image(image) if image else "-", 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") + 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 revisions_get(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 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 secrets_set(jig: Jig, name: str, value: str, description: str) -> None: """Set a secret (create or update)""" @@ -1682,7 +1686,7 @@ def revisions_get_cli( toml_config: TomlConfigParameter = None, ) -> None: """Get the full configuration of a specific revision.""" - _run_jig_cmd(config, toml_config, lambda jig: revisions_get(jig, revision_id)) + _run_jig_cmd(config, toml_config, lambda jig: get_revisions(jig, revision_id)) def rollback_cli( diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py index d6c796c2..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] @@ -470,11 +473,11 @@ [dim]-[/dim] Show revision history (newest first, up to 10 events): [primary]tg beta jig revisions list[/primary] -[dim]-[/dim] Page through the 10 revision events, event numbers 90 to 100: - [primary]tg beta jig revisions list --limit 10 --before 101 [/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: - [primary]tg beta jig revisions get or [/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] @@ -482,7 +485,7 @@ [primary]tg beta jig rollback 7[/primary] [dim]-[/dim] Roll back to a revision by UUID: - [primary]tg beta jig rollback [/primary] + [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] diff --git a/tests/cli/test_beta_jig.py b/tests/cli/test_beta_jig.py index 8a135e51..35a89402 100644 --- a/tests/cli/test_beta_jig.py +++ b/tests/cli/test_beta_jig.py @@ -429,148 +429,3 @@ async def upload_files(self, source: Path, prefix: str) -> None: patch_body = json.loads(cast(Call, patch_r.calls[0]).request.content.decode()) assert patch_body["content"] == {"type": "files", "source_prefix": "shared/4"} assert result.exit_code == 0 - - -_REVISION_ID = "11111111-1111-1111-1111-111111111111" - - -def _ready_deployment_body(revision_id: str) -> dict[str, object]: - """Minimal deployment body that jig.track() treats as up and ready.""" - return { - "name": _DEPLOY_NAME, - "object": "deployment", - "status": "Ready", - "environment_variables": [{"name": "TOGETHER_DEPLOYMENT_REVISION_ID", "value": revision_id}], - "replica_events": { - "replica-1": { - "revision_id": revision_id, - "replica_status": "Running", - "replica_ready_since": "2024-01-01T00:00:00Z", - } - }, - } - - -class TestBetaJigRevisions: - @pytest.mark.respx(base_url=base_url) - def test_list_json(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: - _write_jig_project(tmp_path) - payload = { - "object": "list", - "data": [{"object": "revision_event", "revision_number": 2, "revision_id": _REVISION_ID}], - "next_cursor": "1", - } - respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock(return_value=httpx.Response(200, json=payload)) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "revisions", "list", "--json"]) - - assert json.loads(result.output) == payload - assert result.exit_code == 0 - - @pytest.mark.respx(base_url=base_url) - def test_list_table(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: - _write_jig_project(tmp_path) - payload = { - "object": "list", - "data": [ - { - "object": "revision_event", - "revision_number": 4, - "event_number": 7, - "revision_id": _REVISION_ID, - "action": "rollback", - "activated_at": "2024-01-01T00:00:00Z", - } - ], - } - respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock(return_value=httpx.Response(200, json=payload)) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "revisions", "list"]) - - assert "Revisions for" in result.output - assert "#4" in result.output - assert "#7" in result.output - # pagination hint uses the last event's event_number - assert "--before 7" in result.output - assert result.exit_code == 0 - - @pytest.mark.respx(base_url=base_url) - def test_list_forwards_limit_and_before( - self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner - ) -> None: - _write_jig_project(tmp_path) - route = respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions").mock( - return_value=httpx.Response(200, json={"object": "list", "data": []}) - ) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "revisions", "list", "--limit", "50", "--before", "3"]) - - request = cast(Call, route.calls[0]).request - assert request.url.params["limit"] == "50" - assert request.url.params["before"] == "3" - assert result.exit_code == 0 - - @pytest.mark.respx(base_url=base_url) - def test_get_by_revision_id(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: - _write_jig_project(tmp_path) - payload = {"object": "revision", "revision_id": _REVISION_ID, "image": "registry/img:abc", "min_replicas": 1} - respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions/{_REVISION_ID}").mock( - return_value=httpx.Response(200, json=payload) - ) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "revisions", "get", "--revision-id", _REVISION_ID]) - - assert json.loads(result.output) == payload - assert result.exit_code == 0 - - @pytest.mark.respx(base_url=base_url) - def test_get_not_found(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: - _write_jig_project(tmp_path) - respx_mock.get(f"/deployments/{_DEPLOY_NAME}/revisions/missing").mock( - return_value=httpx.Response(404, json={"error": {"message": "revision not found"}}) - ) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "revisions", "get", "missing"]) - - assert "not found" in result.output.lower() - assert result.exit_code == 1 - - -class TestBetaJigRollback: - @pytest.mark.respx(base_url=base_url) - def test_rollback_detach(self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner) -> None: - _write_jig_project(tmp_path) - route = respx_mock.post(f"/deployments/{_DEPLOY_NAME}/rollback").mock( - return_value=httpx.Response(200, json={"name": _DEPLOY_NAME, "object": "deployment", "status": "Updating"}) - ) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "rollback", "7", "--detach"]) - - body = json.loads(cast(Call, route.calls[0]).request.content.decode()) - assert body == {"revision_identifier": "7"} - assert "Rolling back" in result.output - assert result.exit_code == 0 - - @pytest.mark.respx(base_url=base_url) - def test_rollback_tracks_until_ready( - self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner - ) -> None: - _write_jig_project(tmp_path) - respx_mock.post(f"/deployments/{_DEPLOY_NAME}/rollback").mock( - return_value=httpx.Response(200, json=_ready_deployment_body(_REVISION_ID)) - ) - respx_mock.get(f"/deployments/{_DEPLOY_NAME}").mock( - return_value=httpx.Response(200, json=_ready_deployment_body(_REVISION_ID)) - ) - - with _chdir(tmp_path): - result = cli_runner.invoke(["beta", "jig", "rollback", "--revision", _REVISION_ID]) - - assert "Deployment successful" in result.output - assert result.exit_code == 0