From b6351f2e23616e4f6db73bb9cb388a0d59f5b32b Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 3 Aug 2026 19:51:36 -0700 Subject: [PATCH 1/2] feat(blueprints): add direct upload registration --- src/index.ts | 6 +++ src/resources/blueprints.ts | 62 ++++++++++++++++++++++++++ src/resources/index.ts | 3 ++ tests/api-resources/blueprints.test.ts | 17 +++++++ 4 files changed, 88 insertions(+) diff --git a/src/index.ts b/src/index.ts index b4ac078fd..67712671a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,9 @@ import { BlueprintListView, BlueprintPreviewParams, BlueprintPreviewView, + BlueprintRegisterParams, + BlueprintUploadParameters, + BlueprintUploadView, BlueprintView, BlueprintViewsBlueprintsCursorIDPage, Blueprints, @@ -662,6 +665,8 @@ export declare namespace Runloop { type BlueprintBuildParameters as BlueprintBuildParameters, type BlueprintListView as BlueprintListView, type BlueprintPreviewView as BlueprintPreviewView, + type BlueprintUploadParameters as BlueprintUploadParameters, + type BlueprintUploadView as BlueprintUploadView, type BlueprintView as BlueprintView, type BlueprintDeleteResponse as BlueprintDeleteResponse, BlueprintViewsBlueprintsCursorIDPage as BlueprintViewsBlueprintsCursorIDPage, @@ -669,6 +674,7 @@ export declare namespace Runloop { type BlueprintListParams as BlueprintListParams, type BlueprintListPublicParams as BlueprintListPublicParams, type BlueprintPreviewParams as BlueprintPreviewParams, + type BlueprintRegisterParams as BlueprintRegisterParams, }; export { diff --git a/src/resources/blueprints.ts b/src/resources/blueprints.ts index f6ed91e77..9ea25e6c3 100644 --- a/src/resources/blueprints.ts +++ b/src/resources/blueprints.ts @@ -88,6 +88,19 @@ export class Blueprints extends APIResource { return this._client.post('/v1/blueprints', { body, ...options }); } + /** + * Create a private Blueprint awaiting its image to be pushed out-of-band via + * docker push. Bypasses the build pipeline entirely: no Dockerfile is composed + * and no image is built. The Blueprint stays in the 'awaiting_upload' step until + * the push completes. + */ + register( + body: BlueprintRegisterParams, + options?: Core.RequestOptions, + ): Core.APIPromise { + return this._client.post('/v1/blueprints/register', { body, ...options }); + } + /** * Get the details of a previously created Blueprint including the build status. */ @@ -413,6 +426,35 @@ export interface BlueprintPreviewView { dockerfile: string; } +export interface BlueprintUploadParameters { + /** + * Name of the Blueprint. + */ + name: string; + + /** + * Parameters to configure your Devbox at launch time. + */ + launch_parameters?: Shared.LaunchParameters | null; + + /** + * (Optional) User defined metadata for the Blueprint. + */ + metadata?: { [key: string]: string } | null; +} + +export interface BlueprintUploadView { + /** + * The created Blueprint, awaiting its image upload. + */ + blueprint: BlueprintView; + + /** + * The reference to push the image to, e.g. via docker push. + */ + push_reference: string; +} + /** * Blueprints are ways to create customized starting points for Devboxes. They * allow you to define custom starting points for Devboxes such that environment @@ -826,6 +868,23 @@ export interface BlueprintPreviewParams { system_setup_commands?: Array | null; } +export interface BlueprintRegisterParams { + /** + * Name of the Blueprint. + */ + name: string; + + /** + * Parameters to configure your Devbox at launch time. + */ + launch_parameters?: Shared.LaunchParameters | null; + + /** + * (Optional) User defined metadata for the Blueprint. + */ + metadata?: { [key: string]: string } | null; +} + export namespace BlueprintPreviewParams { /** * A build context backed by an Object. @@ -899,6 +958,8 @@ export declare namespace Blueprints { type BlueprintBuildParameters as BlueprintBuildParameters, type BlueprintListView as BlueprintListView, type BlueprintPreviewView as BlueprintPreviewView, + type BlueprintUploadParameters as BlueprintUploadParameters, + type BlueprintUploadView as BlueprintUploadView, type BlueprintView as BlueprintView, type BlueprintDeleteResponse as BlueprintDeleteResponse, BlueprintViewsBlueprintsCursorIDPage as BlueprintViewsBlueprintsCursorIDPage, @@ -906,5 +967,6 @@ export declare namespace Blueprints { type BlueprintListParams as BlueprintListParams, type BlueprintListPublicParams as BlueprintListPublicParams, type BlueprintPreviewParams as BlueprintPreviewParams, + type BlueprintRegisterParams as BlueprintRegisterParams, }; } diff --git a/src/resources/index.ts b/src/resources/index.ts index fe0f43691..be59bce05 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -74,12 +74,15 @@ export { type BlueprintBuildParameters, type BlueprintListView, type BlueprintPreviewView, + type BlueprintUploadParameters, + type BlueprintUploadView, type BlueprintView, type BlueprintDeleteResponse, type BlueprintCreateParams, type BlueprintListParams, type BlueprintListPublicParams, type BlueprintPreviewParams, + type BlueprintRegisterParams, } from './blueprints'; export { DevboxSnapshotViewsDiskSnapshotsCursorIDPage, diff --git a/tests/api-resources/blueprints.test.ts b/tests/api-resources/blueprints.test.ts index 3de333517..c076d71a8 100644 --- a/tests/api-resources/blueprints.test.ts +++ b/tests/api-resources/blueprints.test.ts @@ -9,6 +9,23 @@ const client = new Runloop({ }); describe('resource blueprints', () => { + test('register: only required params', async () => { + const responsePromise = client.blueprints.register({ name: 'name' }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + expect(response.push_reference).toBeDefined(); + }); + + test('register: required and optional params', async () => { + await client.blueprints.register({ + name: 'name', + launch_parameters: { architecture: 'x86_64' }, + metadata: { source: 'upload' }, + }); + }); + test('create: only required params', async () => { const responsePromise = client.blueprints.create({ name: 'name' }); const rawResponse = await responsePromise.asResponse(); From 1b6d7bbd73d5762bc39c093a46faf251917b4295 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 3 Aug 2026 20:03:12 -0700 Subject: [PATCH 2/2] fix(blueprints): handle awaiting upload status --- src/resources/blueprints.ts | 8 ++--- tests/api-resources/blueprints.test.ts | 49 ++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/resources/blueprints.ts b/src/resources/blueprints.ts index 9ea25e6c3..d4cfc3c91 100644 --- a/src/resources/blueprints.ts +++ b/src/resources/blueprints.ts @@ -126,7 +126,7 @@ export class Blueprints extends APIResource { signal, ...(pollTimeoutMs !== undefined ? { timeoutMs: pollTimeoutMs } : {}), shouldStop: (result) => { - return !['queued', 'provisioning', 'building'].includes(result.status); + return !['queued', 'provisioning', 'building', 'awaiting_upload'].includes(result.status); }, }, ); @@ -491,7 +491,7 @@ export interface BlueprintView { /** * The status of the Blueprint build. */ - status: 'queued' | 'provisioning' | 'building' | 'failed' | 'build_complete'; + status: 'queued' | 'provisioning' | 'building' | 'awaiting_upload' | 'failed' | 'build_complete'; /** * The ID of the base Blueprint. @@ -758,7 +758,7 @@ export interface BlueprintListParams extends BlueprintsCursorIDPageParams { name?: string; /** - * Filter by build status (queued, provisioning, building, failed, build_complete) + * Filter by build status (queued, provisioning, building, awaiting_upload, failed, build_complete) */ status?: string; } @@ -776,7 +776,7 @@ export interface BlueprintListPublicParams extends BlueprintsCursorIDPageParams name?: string; /** - * Filter by build status (queued, provisioning, building, failed, build_complete) + * Filter by build status (queued, provisioning, building, awaiting_upload, failed, build_complete) */ status?: string; } diff --git a/tests/api-resources/blueprints.test.ts b/tests/api-resources/blueprints.test.ts index c076d71a8..dac1b4af0 100644 --- a/tests/api-resources/blueprints.test.ts +++ b/tests/api-resources/blueprints.test.ts @@ -1,29 +1,74 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Runloop } from '@runloop/api-client'; -import { Response } from 'node-fetch'; +import { Response, type RequestInfo, type RequestInit } from 'node-fetch'; const client = new Runloop({ bearerToken: 'My Bearer Token', baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); +const createRegisterClient = () => { + const fetch = jest.fn( + async (_url: RequestInfo, _init?: RequestInit) => + new Response( + JSON.stringify({ + blueprint: { + id: 'bpt_123', + name: 'name', + status: 'awaiting_upload', + state: 'created', + create_time_ms: 0, + parameters: { name: 'name' }, + }, + push_reference: 'converter.runloop.ai/blueprints:bpt_123', + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + ); + + return { + client: new Runloop({ + bearerToken: 'My Bearer Token', + baseURL: 'http://localhost:5000', + fetch, + }), + fetch, + }; +}; + describe('resource blueprints', () => { test('register: only required params', async () => { + const { client, fetch } = createRegisterClient(); const responsePromise = client.blueprints.register({ name: 'name' }); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; expect(response).not.toBeInstanceOf(Response); - expect(response.push_reference).toBeDefined(); + expect(response.push_reference).toBe('converter.runloop.ai/blueprints:bpt_123'); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:5000/v1/blueprints/register', + expect.objectContaining({ method: 'POST' }), + ); + expect(JSON.parse(fetch.mock.calls[0]![1]!.body as string)).toEqual({ name: 'name' }); }); test('register: required and optional params', async () => { + const { client, fetch } = createRegisterClient(); await client.blueprints.register({ name: 'name', launch_parameters: { architecture: 'x86_64' }, metadata: { source: 'upload' }, }); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:5000/v1/blueprints/register', + expect.objectContaining({ method: 'POST' }), + ); + expect(JSON.parse(fetch.mock.calls[0]![1]!.body as string)).toEqual({ + name: 'name', + launch_parameters: { architecture: 'x86_64' }, + metadata: { source: 'upload' }, + }); }); test('create: only required params', async () => {