From c68469eb60d1596680c01c404f51049ca32d391c Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 2 Mar 2026 17:40:55 -0800 Subject: [PATCH 1/5] blueprint build with context added to examples --- EXAMPLES.md | 31 ++++++ examples/blueprint_with_build_context.py | 127 +++++++++++++++++++++++ examples/registry.py | 8 ++ 3 files changed, 166 insertions(+) create mode 100644 examples/blueprint_with_build_context.py diff --git a/EXAMPLES.md b/EXAMPLES.md index 9377b08bb..9e3b233d1 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -7,9 +7,40 @@ Runnable examples live in [`examples/`](./examples). ## Table of Contents +- [Blueprint with Build Context](#blueprint-with-build-context) - [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle) - [MCP Hub + Claude Code + GitHub](#mcp-github-tools) + +## Blueprint with Build Context + +**Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. + +**Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup` + +### Workflow +- Create a temporary directory with sample application files +- Upload the directory to object storage as build context +- Create a blueprint with a Dockerfile that copies the context files +- Create a devbox from the blueprint +- Verify the files were copied into the image +- Shutdown devbox and delete blueprint and storage object + +### Prerequisites +- `RUNLOOP_API_KEY` + +### Run +```sh +uv run python -m examples.blueprint_with_build_context +``` + +### Test +```sh +uv run pytest -m smoketest tests/smoketests/examples/ +``` + +**Source:** [`examples/blueprint_with_build_context.py`](./examples/blueprint_with_build_context.py) + ## Devbox From Blueprint (Run Command, Shutdown) diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py new file mode 100644 index 000000000..e9740286d --- /dev/null +++ b/examples/blueprint_with_build_context.py @@ -0,0 +1,127 @@ +#!/usr/bin/env -S uv run python +""" +--- +title: Blueprint with Build Context +slug: blueprint-with-build-context +use_case: Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. +workflow: + - Create a temporary directory with sample application files + - Upload the directory to object storage as build context + - Create a blueprint with a Dockerfile that copies the context files + - Create a devbox from the blueprint + - Verify the files were copied into the image + - Shutdown devbox and delete blueprint and storage object +tags: + - blueprint + - object-store + - build-context + - devbox + - cleanup +prerequisites: + - RUNLOOP_API_KEY +run: uv run python -m examples.blueprint_with_build_context +test: uv run pytest -m smoketest tests/smoketests/examples/ +--- +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path +from datetime import timedelta + +from runloop_api_client import RunloopSDK +from runloop_api_client.lib.polling import PollingConfig + +from ._harness import run_as_cli, unique_name, wrap_recipe +from .example_types import ExampleCheck, RecipeOutput, RecipeContext + +# building can take time: make sure to set a long blueprint build timeout +BLUEPRINT_POLL_TIMEOUT_S = 10 * 60 +BLUEPRINT_POLL_MAX_ATTEMPTS = 600 +ONE_WEEK = timedelta(weeks=1) + + +def recipe(ctx: RecipeContext) -> RecipeOutput: + """Create a blueprint with build context from object storage, then verify files in a devbox.""" + cleanup = ctx.cleanup + + sdk = RunloopSDK() + + # setup: create a temporary directory with sample application files to use as build context + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) + (tmp_path / "app.py").write_text('print("Hello from app")') + (tmp_path / "config.txt").write_text("key=value") + + # upload the build context to object storage + storage_obj = sdk.storage_object.upload_from_dir( + tmp_path, + name=unique_name("example-build-context"), + ttl=ONE_WEEK, + ) + cleanup.add(f"storage_object:{storage_obj.id}", storage_obj.delete) + + # create a blueprint with the build context + blueprint = sdk.blueprint.create( + name=unique_name("example-blueprint-context"), + dockerfile="FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .", + build_context=storage_obj.as_build_context(), + polling_config=PollingConfig( + timeout_seconds=BLUEPRINT_POLL_TIMEOUT_S, + max_attempts=BLUEPRINT_POLL_MAX_ATTEMPTS, + ), + ) + cleanup.add(f"blueprint:{blueprint.id}", blueprint.delete) + + devbox = blueprint.create_devbox( + name=unique_name("example-devbox"), + launch_parameters={ + "resource_size_request": "X_SMALL", + "keep_alive_time_seconds": 60 * 5, + }, + ) + cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) + + app_result = devbox.cmd.exec("cat /app/app.py") + app_stdout = app_result.stdout() + + config_result = devbox.cmd.exec("cat /app/config.txt") + config_stdout = config_result.stdout() + + return RecipeOutput( + resources_created=[ + f"storage_object:{storage_obj.id}", + f"blueprint:{blueprint.id}", + f"devbox:{devbox.id}", + ], + checks=[ + ExampleCheck( + name="app.py exists and readable", + passed=app_result.exit_code == 0, + details=f"exitCode={app_result.exit_code}", + ), + ExampleCheck( + name="app.py contains expected content", + passed='print("Hello from app")' in app_stdout, + details=app_stdout.strip(), + ), + ExampleCheck( + name="config.txt exists and readable", + passed=config_result.exit_code == 0, + details=f"exitCode={config_result.exit_code}", + ), + ExampleCheck( + name="config.txt contains expected content", + passed="key=value" in config_stdout, + details=config_stdout.strip(), + ), + ], + ) + + +run_blueprint_with_build_context_example = wrap_recipe(recipe) + + +if __name__ == "__main__": + run_as_cli(run_blueprint_with_build_context_example) diff --git a/examples/registry.py b/examples/registry.py index 41a4b4b51..cb6b780a9 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -9,11 +9,19 @@ from .example_types import ExampleResult from .mcp_github_tools import run_mcp_github_tools_example +from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example ExampleRegistryEntry = dict[str, Any] example_registry: list[ExampleRegistryEntry] = [ + { + "slug": "blueprint-with-build-context", + "title": "Blueprint with Build Context", + "file_name": "blueprint_with_build_context.py", + "required_env": ["RUNLOOP_API_KEY"], + "run": run_blueprint_with_build_context_example, + }, { "slug": "devbox-from-blueprint-lifecycle", "title": "Devbox From Blueprint (Run Command, Shutdown)", From ae7d92c2541eb4f73c5da01f1cc9feec46382c24 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 14:43:42 -0800 Subject: [PATCH 2/5] switch to async sdk since that is perferred anyway --- EXAMPLES.md | 4 ++-- examples/blueprint_with_build_context.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 9e3b233d1..0d3ff62cc 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -14,9 +14,9 @@ Runnable examples live in [`examples/`](./examples). ## Blueprint with Build Context -**Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. +**Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. -**Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup` +**Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup`, `async` ### Workflow - Create a temporary directory with sample application files diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py index e9740286d..499850b18 100644 --- a/examples/blueprint_with_build_context.py +++ b/examples/blueprint_with_build_context.py @@ -3,7 +3,7 @@ --- title: Blueprint with Build Context slug: blueprint-with-build-context -use_case: Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. +use_case: Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. workflow: - Create a temporary directory with sample application files - Upload the directory to object storage as build context @@ -17,6 +17,7 @@ - build-context - devbox - cleanup + - async prerequisites: - RUNLOOP_API_KEY run: uv run python -m examples.blueprint_with_build_context @@ -30,7 +31,7 @@ from pathlib import Path from datetime import timedelta -from runloop_api_client import RunloopSDK +from runloop_api_client import AsyncRunloopSDK from runloop_api_client.lib.polling import PollingConfig from ._harness import run_as_cli, unique_name, wrap_recipe @@ -42,11 +43,11 @@ ONE_WEEK = timedelta(weeks=1) -def recipe(ctx: RecipeContext) -> RecipeOutput: +async def recipe(ctx: RecipeContext) -> RecipeOutput: """Create a blueprint with build context from object storage, then verify files in a devbox.""" cleanup = ctx.cleanup - sdk = RunloopSDK() + sdk = AsyncRunloopSDK() # setup: create a temporary directory with sample application files to use as build context with tempfile.TemporaryDirectory() as tmp_dir: @@ -55,7 +56,7 @@ def recipe(ctx: RecipeContext) -> RecipeOutput: (tmp_path / "config.txt").write_text("key=value") # upload the build context to object storage - storage_obj = sdk.storage_object.upload_from_dir( + storage_obj = await sdk.storage_object.upload_from_dir( tmp_path, name=unique_name("example-build-context"), ttl=ONE_WEEK, @@ -63,7 +64,7 @@ def recipe(ctx: RecipeContext) -> RecipeOutput: cleanup.add(f"storage_object:{storage_obj.id}", storage_obj.delete) # create a blueprint with the build context - blueprint = sdk.blueprint.create( + blueprint = await sdk.blueprint.create( name=unique_name("example-blueprint-context"), dockerfile="FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .", build_context=storage_obj.as_build_context(), @@ -74,7 +75,7 @@ def recipe(ctx: RecipeContext) -> RecipeOutput: ) cleanup.add(f"blueprint:{blueprint.id}", blueprint.delete) - devbox = blueprint.create_devbox( + devbox = await blueprint.create_devbox( name=unique_name("example-devbox"), launch_parameters={ "resource_size_request": "X_SMALL", @@ -83,11 +84,11 @@ def recipe(ctx: RecipeContext) -> RecipeOutput: ) cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) - app_result = devbox.cmd.exec("cat /app/app.py") - app_stdout = app_result.stdout() + app_result = await devbox.cmd.exec("cat /app/app.py") + app_stdout = await app_result.stdout() - config_result = devbox.cmd.exec("cat /app/config.txt") - config_stdout = config_result.stdout() + config_result = await devbox.cmd.exec("cat /app/config.txt") + config_stdout = await config_result.stdout() return RecipeOutput( resources_created=[ From 46439d24ef62de74849996d76ae7df1ee57301a2 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 14:47:56 -0800 Subject: [PATCH 3/5] add comment for object timeout --- examples/blueprint_with_build_context.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py index 499850b18..98e086f34 100644 --- a/examples/blueprint_with_build_context.py +++ b/examples/blueprint_with_build_context.py @@ -40,6 +40,8 @@ # building can take time: make sure to set a long blueprint build timeout BLUEPRINT_POLL_TIMEOUT_S = 10 * 60 BLUEPRINT_POLL_MAX_ATTEMPTS = 600 + +# make context available for a week (this is a demo value to show you can configure this value) ONE_WEEK = timedelta(weeks=1) From 72257159e3f1366f6ae68811a483571d69de938a Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 14:49:07 -0800 Subject: [PATCH 4/5] add comment for object timeout & change value to be more reasonable --- examples/blueprint_with_build_context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py index 98e086f34..b4ccbb83a 100644 --- a/examples/blueprint_with_build_context.py +++ b/examples/blueprint_with_build_context.py @@ -41,8 +41,8 @@ BLUEPRINT_POLL_TIMEOUT_S = 10 * 60 BLUEPRINT_POLL_MAX_ATTEMPTS = 600 -# make context available for a week (this is a demo value to show you can configure this value) -ONE_WEEK = timedelta(weeks=1) +# configure object storage ttl for the build context +BUILD_CONTEXT_TTL = timedelta(days=1) async def recipe(ctx: RecipeContext) -> RecipeOutput: @@ -61,7 +61,7 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: storage_obj = await sdk.storage_object.upload_from_dir( tmp_path, name=unique_name("example-build-context"), - ttl=ONE_WEEK, + ttl=BUILD_CONTEXT_TTL, ) cleanup.add(f"storage_object:{storage_obj.id}", storage_obj.delete) From c4751c6cd5a821d797554bd635a710a3c76320e7 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 15:27:40 -0800 Subject: [PATCH 5/5] shrink build context to 1h --- examples/blueprint_with_build_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py index b4ccbb83a..3bd9395d3 100644 --- a/examples/blueprint_with_build_context.py +++ b/examples/blueprint_with_build_context.py @@ -42,7 +42,7 @@ BLUEPRINT_POLL_MAX_ATTEMPTS = 600 # configure object storage ttl for the build context -BUILD_CONTEXT_TTL = timedelta(days=1) +BUILD_CONTEXT_TTL = timedelta(hours=1) async def recipe(ctx: RecipeContext) -> RecipeOutput: