From 7e2e74aa41031863bf8812fa63a3f0d551d742f4 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 5 Mar 2026 13:06:45 +0100 Subject: [PATCH 1/4] feat: add toggle for preview/production timelines to "create revision" API --- src/deno_sandbox/revisions.py | 18 ++++++++++++++++++ tests/test_revisions.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/deno_sandbox/revisions.py b/src/deno_sandbox/revisions.py index 00b89f4..bdf60a7 100644 --- a/src/deno_sandbox/revisions.py +++ b/src/deno_sandbox/revisions.py @@ -182,6 +182,8 @@ async def deploy( layers: Optional[List[str]] = None, env_vars: Optional[List[EnvVarInputForDeploy]] = None, labels: Optional[Dict[str, str]] = None, + production: Optional[bool] = None, + preview: Optional[bool] = None, ) -> Revision: """Deploy a revision by uploading source files as assets. @@ -192,6 +194,10 @@ async def deploy( layers: Layer IDs or slugs to reference for this revision. env_vars: Optional environment variables for this revision. labels: Optional labels (e.g., git metadata). + production: Whether to deploy to the production timeline. + Defaults to true on the server. + preview: Whether to deploy as a preview deployment. + Defaults to false on the server. Returns: The created Revision (build is async; poll for status). @@ -205,6 +211,10 @@ async def deploy( body["env_vars"] = env_vars if labels is not None: body["labels"] = labels + if production is not None: + body["production"] = production + if preview is not None: + body["preview"] = preview result = await self._client.post(f"/api/v2/apps/{app}/deploy", body) return cast(Revision, convert_to_snake_case(result)) @@ -267,6 +277,8 @@ def deploy( layers: Optional[List[str]] = None, env_vars: Optional[List[EnvVarInputForDeploy]] = None, labels: Optional[Dict[str, str]] = None, + production: Optional[bool] = None, + preview: Optional[bool] = None, ) -> Revision: """Deploy a revision by uploading source files as assets. @@ -277,6 +289,10 @@ def deploy( layers: Layer IDs or slugs to reference for this revision. env_vars: Optional environment variables for this revision. labels: Optional labels (e.g., git metadata). + production: Whether to deploy to the production timeline. + Defaults to true on the server. + preview: Whether to deploy as a preview deployment. + Defaults to false on the server. Returns: The created Revision (build is async; poll for status). @@ -289,5 +305,7 @@ def deploy( layers=layers, env_vars=env_vars, labels=labels, + production=production, + preview=preview, ) ) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index 4cf5ba2..0d95bd1 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -193,3 +193,31 @@ def test_revisions_deploy_sync(): assert revision["status"] == "succeeded", revision.get("failure_reason") finally: sdk.apps.delete(app["id"]) + + +@pytest.mark.timeout(60) +@pytest.mark.asyncio(loop_scope="session") +async def test_revisions_deploy_preview_only_async(): + sdk = AsyncDenoDeploy() + app = await sdk.apps.create() + try: + revision = await sdk.revisions.deploy( + app["id"], + assets={ + "main.ts": { + "kind": "file", + "encoding": "utf-8", + "content": 'Deno.serve(() => new Response("Hello"))', + } + }, + production=False, + preview=True, + ) + assert revision["id"] is not None + while revision["status"] in ("queued", "building"): + await asyncio.sleep(1) + revision = await sdk.revisions.get(revision["id"]) + assert revision is not None + assert revision["status"] == "succeeded", revision.get("failure_reason") + finally: + await sdk.apps.delete(app["id"]) From 2c8cf6054082255e253f105dfa9f91794cb48a7c Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 5 Mar 2026 13:13:33 +0100 Subject: [PATCH 2/4] improve test --- tests/test_revisions.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index 0d95bd1..ec70ca0 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -195,9 +195,10 @@ def test_revisions_deploy_sync(): sdk.apps.delete(app["id"]) -@pytest.mark.timeout(60) +@pytest.mark.timeout(120) @pytest.mark.asyncio(loop_scope="session") async def test_revisions_deploy_preview_only_async(): + """Deploy with production=False, preview=True and verify timeline assignment.""" sdk = AsyncDenoDeploy() app = await sdk.apps.create() try: @@ -219,5 +220,22 @@ async def test_revisions_deploy_preview_only_async(): revision = await sdk.revisions.get(revision["id"]) assert revision is not None assert revision["status"] == "succeeded", revision.get("failure_reason") + + # Verify timeline assignment via the revision timelines API + timelines = await sdk.revisions._client.get( + f"/api/v2/revisions/{revision['id']}/timelines" + ) + production = [ + t for t in timelines + if t["context"]["slug"] == "production" + and t.get("active_revision", {}).get("id") == revision["id"] + ] + preview = [ + t for t in timelines + if t["context"]["slug"] == "preview" + and t.get("active_revision", {}).get("id") == revision["id"] + ] + assert len(production) == 0, "should not be on production timeline" + assert len(preview) > 0, "should be on preview timeline" finally: await sdk.apps.delete(app["id"]) From e33c5e0b080fe0a530b7bd00ff912d686f86e7db Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 5 Mar 2026 14:33:01 +0100 Subject: [PATCH 3/4] fix test --- tests/test_revisions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index ec70ca0..398c425 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -227,13 +227,13 @@ async def test_revisions_deploy_preview_only_async(): ) production = [ t for t in timelines - if t["context"]["slug"] == "production" - and t.get("active_revision", {}).get("id") == revision["id"] + if t["slug"] == "production" + and not t.get("partition", {}).get("deno.revision.id") ] preview = [ t for t in timelines - if t["context"]["slug"] == "preview" - and t.get("active_revision", {}).get("id") == revision["id"] + if t["slug"] == "preview" + and t.get("partition", {}).get("deno.revision.id") == revision["id"] ] assert len(production) == 0, "should not be on production timeline" assert len(preview) > 0, "should be on preview timeline" From bf60047634e1078554b25d7f8380581b1da0ebd1 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 5 Mar 2026 15:15:32 +0100 Subject: [PATCH 4/4] fix: format test_revisions.py Co-Authored-By: Claude Opus 4.6 --- tests/test_revisions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_revisions.py b/tests/test_revisions.py index 398c425..9d5e6aa 100644 --- a/tests/test_revisions.py +++ b/tests/test_revisions.py @@ -226,12 +226,14 @@ async def test_revisions_deploy_preview_only_async(): f"/api/v2/revisions/{revision['id']}/timelines" ) production = [ - t for t in timelines + t + for t in timelines if t["slug"] == "production" and not t.get("partition", {}).get("deno.revision.id") ] preview = [ - t for t in timelines + t + for t in timelines if t["slug"] == "preview" and t.get("partition", {}).get("deno.revision.id") == revision["id"] ]