Skip to content

Commit 6b74009

Browse files
authored
feat: add template background build helpers (#1011)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Add background template build APIs and build status retrieval to JS/Python SDKs, change build methods to return BuildInfo, and add tests. > > - **JS SDK (templates)**: > - Add `Template.buildInBackground` to start builds without waiting and return `BuildInfo`. > - Add `Template.getBuildStatus` with optional `logsOffset`; expose `GetBuildStatusResponse`. > - Change `Template.build` to return `BuildInfo` and then wait for completion; refactor internal `build(...)` to accept `ApiClient` and return identifiers. > - Add types `AuthOptions`, `BuildInfo`, `GetBuildStatusOptions`; make `logsOffset` optional in `getBuildStatus` input; update tests with `backgroundBuild.test.ts`. > - **Python SDK (templates)**: > - Introduce `BuildInfo` dataclass; `Template.build`/`AsyncTemplate.build` now return `BuildInfo`. > - Add `Template.build_in_background` / `AsyncTemplate.build_in_background` and `Template.get_build_status` / `AsyncTemplate.get_build_status`; factor shared `_build`. > - Export `BuildInfo` in `__init__.py`; add sync/async background build tests. > - **Meta**: > - Add changeset for patch releases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a47f70e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 978fbed commit 6b74009

File tree

13 files changed

+763
-264
lines changed

13 files changed

+763
-264
lines changed

.changeset/happy-bobcats-unite.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@e2b/python-sdk': patch
3+
'e2b': patch
4+
---
5+
6+
add background template build options

packages/js-sdk/src/template/buildApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type TriggerBuildInput = {
2424
type GetBuildStatusInput = {
2525
templateID: string
2626
buildID: string
27-
logsOffset: number
27+
logsOffset?: number
2828
}
2929

3030
export type GetBuildStatusResponse =
@@ -154,7 +154,7 @@ export async function triggerBuild(
154154
export async function getBuildStatus(
155155
client: ApiClient,
156156
{ templateID, buildID, logsOffset }: GetBuildStatusInput
157-
) {
157+
): Promise<GetBuildStatusResponse> {
158158
const buildStatusRes = await client.api.GET(
159159
'/templates/{templateID}/builds/{buildID}/status',
160160
{

packages/js-sdk/src/template/index.ts

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ConnectionConfig } from '../connectionConfig'
44
import { BuildError } from '../errors'
55
import { runtime } from '../utils'
66
import {
7+
getBuildStatus,
8+
GetBuildStatusResponse,
79
getFileUploadLink,
810
requestBuild,
911
triggerBuild,
@@ -16,8 +18,10 @@ import { parseDockerfile } from './dockerfileParser'
1618
import { LogEntry, LogEntryEnd, LogEntryStart } from './logger'
1719
import { ReadyCmd, waitForFile } from './readycmd'
1820
import {
21+
BuildInfo,
1922
BuildOptions,
2023
CopyItem,
24+
GetBuildStatusOptions,
2125
Instruction,
2226
InstructionType,
2327
McpServerName,
@@ -113,15 +117,94 @@ export class TemplateBase
113117
static async build(
114118
template: TemplateClass,
115119
options: BuildOptions
116-
): Promise<void> {
120+
): Promise<BuildInfo> {
117121
try {
118122
options.onBuildLogs?.(new LogEntryStart(new Date(), 'Build started'))
119-
return await (template as TemplateBase).build(options)
123+
const baseTemplate = template as TemplateBase
124+
125+
const config = new ConnectionConfig({
126+
domain: options.domain,
127+
apiKey: options.apiKey,
128+
})
129+
const client = new ApiClient(config)
130+
131+
const data = await baseTemplate.build(client, options)
132+
133+
options.onBuildLogs?.(
134+
new LogEntry(new Date(), 'info', 'Waiting for logs...')
135+
)
136+
137+
await waitForBuildFinish(client, {
138+
templateID: data.templateId,
139+
buildID: data.buildId,
140+
onBuildLogs: options.onBuildLogs,
141+
logsRefreshFrequency: baseTemplate.logsRefreshFrequency,
142+
stackTraces: baseTemplate.stackTraces,
143+
})
144+
145+
return data
120146
} finally {
121147
options.onBuildLogs?.(new LogEntryEnd(new Date(), 'Build finished'))
122148
}
123149
}
124150

151+
/**
152+
* Build and deploy a template to E2B infrastructure.
153+
*
154+
* @param template The template to build
155+
* @param options Build configuration options
156+
*
157+
* @example
158+
* ```ts
159+
* const template = Template().fromPythonImage('3')
160+
* const data = await Template.buildInBackground(template, {
161+
* alias: 'my-python-env',
162+
* cpuCount: 2,
163+
* memoryMB: 1024
164+
* })
165+
* ```
166+
*/
167+
static async buildInBackground(
168+
template: TemplateClass,
169+
options: BuildOptions
170+
): Promise<BuildInfo> {
171+
const config = new ConnectionConfig({
172+
domain: options.domain,
173+
apiKey: options.apiKey,
174+
})
175+
const client = new ApiClient(config)
176+
177+
return await (template as TemplateBase).build(client, options)
178+
}
179+
180+
/**
181+
* Get the status of a build.
182+
*
183+
* @param data Build identifiers
184+
* @param options Authentication options
185+
*
186+
* @example
187+
* ```ts
188+
* const status = await Template.getBuildStatus(data, { logsOffset: 0 })
189+
* ```
190+
*/
191+
static async getBuildStatus(
192+
data: Pick<BuildInfo, 'templateId' | 'buildId'>,
193+
options?: GetBuildStatusOptions
194+
): Promise<GetBuildStatusResponse> {
195+
const config = new ConnectionConfig({
196+
domain: options?.domain,
197+
apiKey: options?.apiKey,
198+
})
199+
const client = new ApiClient(config)
200+
201+
return await getBuildStatus(client, {
202+
templateID: data.templateId,
203+
buildID: data.buildId,
204+
logsOffset: options?.logsOffset,
205+
})
206+
}
207+
125208
fromDebianImage(variant: string = 'stable'): TemplateBuilder {
126209
return this.fromImage(`debian:${variant}`)
127210
}
@@ -768,16 +851,14 @@ export class TemplateBase
768851
/**
769852
* Internal implementation of the template build process.
770853
*
854+
* @param client API client for communicating with E2B backend
771855
* @param options Build configuration options
772856
* @throws BuildError if the build fails
773857
*/
774-
private async build(options: BuildOptions): Promise<void> {
775-
const config = new ConnectionConfig({
776-
domain: options.domain,
777-
apiKey: options.apiKey,
778-
})
779-
const client = new ApiClient(config)
780-
858+
private async build(
859+
client: ApiClient,
860+
options: BuildOptions
861+
): Promise<BuildInfo> {
781862
if (options.skipCache) {
782863
this.force = true
783864
}
@@ -880,17 +961,11 @@ export class TemplateBase
880961
template: this.serialize(instructionsWithHashes),
881962
})
882963

883-
options.onBuildLogs?.(
884-
new LogEntry(new Date(), 'info', 'Waiting for logs...')
885-
)
886-
887-
await waitForBuildFinish(client, {
888-
templateID,
889-
buildID,
890-
onBuildLogs: options.onBuildLogs,
891-
logsRefreshFrequency: this.logsRefreshFrequency,
892-
stackTraces: this.stackTraces,
893-
})
964+
return {
965+
alias: options.alias,
966+
templateId: templateID,
967+
buildId: buildID,
968+
}
894969
}
895970

896971
/**
@@ -989,12 +1064,16 @@ export function Template(options?: TemplateOptions): TemplateFromImage {
9891064
}
9901065

9911066
Template.build = TemplateBase.build
1067+
Template.buildInBackground = TemplateBase.buildInBackground
1068+
Template.getBuildStatus = TemplateBase.getBuildStatus
9921069
Template.toJSON = TemplateBase.toJSON
9931070
Template.toDockerfile = TemplateBase.toDockerfile
9941071

9951072
export type {
1073+
BuildInfo,
9961074
BuildOptions,
9971075
CopyItem,
1076+
GetBuildStatusOptions,
9981077
McpServerName,
9991078
TemplateBuilder,
10001079
TemplateClass,

packages/js-sdk/src/template/types.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export type BasicBuildOptions = {
4848
}
4949

5050
/**
51-
* Options for building a template with authentication.
51+
* Authentication options for E2B API.
5252
*/
53-
export type BuildOptions = BasicBuildOptions & {
53+
export type AuthOptions = {
5454
/**
5555
* E2B API key for authentication.
5656
*/
@@ -61,6 +61,25 @@ export type BuildOptions = BasicBuildOptions & {
6161
domain?: string
6262
}
6363

64+
/**
65+
* Options for building a template with authentication.
66+
*/
67+
export type BuildOptions = AuthOptions & BasicBuildOptions
68+
69+
/**
70+
* Information about a built template.
71+
*/
72+
export type BuildInfo = {
73+
alias: string
74+
templateId: string
75+
buildId: string
76+
}
77+
78+
/**
79+
* Response from getting build status.
80+
*/
81+
export type GetBuildStatusOptions = AuthOptions & { logsOffset?: number }
82+
6483
/**
6584
* Types of instructions that can be used in a template.
6685
*/

packages/js-sdk/tests/setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'node:crypto'
22
import { test as base } from 'vitest'
3-
import { LogEntry, Sandbox, Template, TemplateClass } from '../src'
3+
import { BuildInfo, LogEntry, Sandbox, Template, TemplateClass } from '../src'
44
import { template } from './template'
55

66
interface SandboxFixture {
@@ -14,7 +14,7 @@ interface BuildTemplateFixture {
1414
template: TemplateClass,
1515
options?: { skipCache?: boolean },
1616
onBuildLogs?: (logEntry: LogEntry) => void
17-
) => Promise<void>
17+
) => Promise<BuildInfo>
1818
}
1919

2020
function buildTemplate(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { randomUUID } from 'node:crypto'
2+
import { expect, test } from 'vitest'
3+
import { Template, waitForTimeout } from '../../src'
4+
5+
test('build template in background', async () => {
6+
const template = Template()
7+
.fromImage('ubuntu:22.04')
8+
.skipCache()
9+
.runCmd('sleep 5') // Add a delay to ensure build takes time
10+
.setStartCmd('echo "Hello"', waitForTimeout(10_000))
11+
12+
const alias = `e2b-test-${randomUUID()}`
13+
14+
const buildInfo = await Template.buildInBackground(template, {
15+
alias,
16+
cpuCount: 1,
17+
memoryMB: 1024,
18+
})
19+
20+
// Should return quickly (within a few seconds), not wait for the full build
21+
expect(buildInfo).toBeDefined()
22+
23+
// Verify the build is actually running
24+
const status = await Template.getBuildStatus(buildInfo)
25+
expect(status.status).toEqual('building')
26+
}, 10_000)

packages/python-sdk/e2b/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
wait_for_process,
9393
wait_for_timeout,
9494
)
95-
from .template.types import CopyItem
95+
from .template.types import CopyItem, BuildInfo
9696
from .template_async.main import AsyncTemplate
9797
from .template_sync.main import Template
9898

@@ -150,6 +150,7 @@
150150
"TemplateBase",
151151
"TemplateClass",
152152
"CopyItem",
153+
"BuildInfo",
153154
"ReadyCmd",
154155
"wait_for_file",
155156
"wait_for_url",

packages/python-sdk/e2b/template/types.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
from dataclasses import dataclass
12
from enum import Enum
23
from pathlib import Path
3-
from typing import List, Optional, TypedDict, Union
4-
from typing import Literal
4+
from typing import List, Literal, Optional, TypedDict, Union
55

66
from typing_extensions import NotRequired
77

@@ -92,3 +92,14 @@ class TemplateType(TypedDict):
9292
readyCmd: NotRequired[str]
9393
steps: List[Instruction]
9494
force: bool
95+
96+
97+
@dataclass
98+
class BuildInfo:
99+
"""
100+
Information about a built template.
101+
"""
102+
103+
alias: str
104+
template_id: str
105+
build_id: str

0 commit comments

Comments
 (0)