@declaw/sdk 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/index.cjs +253 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -11
- package/dist/index.d.ts +110 -11
- package/dist/index.js +250 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ All notable changes to the Declaw TypeScript / JavaScript SDK are documented in
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0]
|
|
9
|
+
|
|
10
|
+
_2026-09 train: working template builds._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The `buildTimeout` option on `Template.build()`, `BuildInfo.logs`,
|
|
15
|
+
`TemplateBuildStatus.templateId`, `buildId` / `logs` on `BuildError`, and
|
|
16
|
+
`TemplateBase.hasCopies()`.
|
|
17
|
+
- Status checks that fail temporarily while waiting (5xx, 408, 429, network
|
|
18
|
+
errors) are retried for up to two minutes instead of ending the wait. The
|
|
19
|
+
server keeps the newest 2,000 lines of a build's output; if more arrive
|
|
20
|
+
between two status checks, a `... [earlier build output truncated]` line
|
|
21
|
+
marks the gap.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `Template.build()` now waits for the build to finish, as documented, and
|
|
26
|
+
resolves to a `BuildInfo` whose `logs` hold the output. A failed build
|
|
27
|
+
rejects with `BuildError` carrying its `buildId` and `logs`. A build still
|
|
28
|
+
running after `buildTimeout` milliseconds (default one hour) rejects with
|
|
29
|
+
`TimeoutError` naming it; the build keeps running, and
|
|
30
|
+
`Template.getBuildStatus()` follows it. To return immediately, use
|
|
31
|
+
`Template.buildInBackground()`.
|
|
32
|
+
- A template that uses `copy()` now rejects with `InvalidArgumentError` when
|
|
33
|
+
built, before anything is sent. `copy()` never copied anything: a build
|
|
34
|
+
cannot upload local files, and the copies were silently dropped. Fetch files
|
|
35
|
+
in a `runCmd` step, or use `fromDockerfile()`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Packages added with `aptInstall()` were sent under a name the API ignores,
|
|
40
|
+
so builds succeeded without installing them. They are now installed.
|
|
41
|
+
- `onBuildLogs` never fired. It now receives each new line of build output
|
|
42
|
+
while `Template.build()` waits.
|
|
43
|
+
|
|
44
|
+
## [1.4.0]
|
|
45
|
+
|
|
46
|
+
_2026-08 train: idempotent sandbox creation._
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `Sandbox.create` now sends an `Idempotency-Key`. A create that times out or is
|
|
51
|
+
retried no longer risks leaving a second running, billable sandbox the caller
|
|
52
|
+
has no handle for. The key is generated once per logical create and reused
|
|
53
|
+
across that call's retries.
|
|
54
|
+
- A `409` carrying `idempotency_in_progress` is retried automatically, honoring
|
|
55
|
+
`Retry-After`.
|
|
56
|
+
- `SandboxError.code` exposes the API's machine-readable error code, with the
|
|
57
|
+
`CODE_IDEMPOTENCY_IN_PROGRESS`, `CODE_IDEMPOTENCY_KEY_REUSED` and
|
|
58
|
+
`CODE_TEMPLATE_NOT_READY` constants exported. Branch on the code, never the
|
|
59
|
+
message.
|
|
60
|
+
|
|
61
|
+
_(Retry backoff already carried jitter in this SDK; unlike the Go and Python
|
|
62
|
+
clients it needed no change.)_
|
|
63
|
+
|
|
8
64
|
## [1.3.0]
|
|
9
65
|
|
|
10
66
|
_2026-07 train: credential vault client + injection domain scoping._
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,9 @@ __export(index_exports, {
|
|
|
34
34
|
ApiClient: () => ApiClient,
|
|
35
35
|
AuthenticationError: () => AuthenticationError,
|
|
36
36
|
BuildError: () => BuildError,
|
|
37
|
+
CODE_IDEMPOTENCY_IN_PROGRESS: () => CODE_IDEMPOTENCY_IN_PROGRESS,
|
|
38
|
+
CODE_IDEMPOTENCY_KEY_REUSED: () => CODE_IDEMPOTENCY_KEY_REUSED,
|
|
39
|
+
CODE_TEMPLATE_NOT_READY: () => CODE_TEMPLATE_NOT_READY,
|
|
37
40
|
CommandExitError: () => CommandExitError,
|
|
38
41
|
CommandHandle: () => CommandHandle,
|
|
39
42
|
Commands: () => Commands,
|
|
@@ -170,13 +173,51 @@ var ConnectionConfig = class {
|
|
|
170
173
|
}
|
|
171
174
|
};
|
|
172
175
|
|
|
176
|
+
// src/api/idempotency.ts
|
|
177
|
+
var CODE_IDEMPOTENCY_IN_PROGRESS = "idempotency_in_progress";
|
|
178
|
+
var CODE_IDEMPOTENCY_KEY_REUSED = "idempotency_key_reused";
|
|
179
|
+
var CODE_TEMPLATE_NOT_READY = "template_not_ready";
|
|
180
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
181
|
+
function newIdempotencyKey() {
|
|
182
|
+
const c = globalThis.crypto;
|
|
183
|
+
if (c?.randomUUID) {
|
|
184
|
+
return c.randomUUID();
|
|
185
|
+
}
|
|
186
|
+
if (c?.getRandomValues) {
|
|
187
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
188
|
+
b[6] = b[6] & 15 | 64;
|
|
189
|
+
b[8] = b[8] & 63 | 128;
|
|
190
|
+
const hex = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
191
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
192
|
+
}
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
function retryAfterMs(response) {
|
|
196
|
+
const raw = response.headers.get("Retry-After");
|
|
197
|
+
if (raw === null) return void 0;
|
|
198
|
+
const secs = Number(raw);
|
|
199
|
+
if (!Number.isFinite(secs) || secs < 0) return void 0;
|
|
200
|
+
return Math.min(secs * 1e3, MAX_RETRY_AFTER_MS);
|
|
201
|
+
}
|
|
202
|
+
|
|
173
203
|
// src/errors.ts
|
|
174
204
|
var SandboxError = class extends Error {
|
|
175
205
|
sandboxId;
|
|
206
|
+
/**
|
|
207
|
+
* Machine-readable error code from the API's `code` field, when present.
|
|
208
|
+
*
|
|
209
|
+
* Branch on this, never on `message`. Messages are prose and change; codes are
|
|
210
|
+
* contract. It matters most where one status means several unrelated things:
|
|
211
|
+
* a 409 from `POST /sandboxes` is either `idempotency_in_progress` (the
|
|
212
|
+
* original create is still running — retry the identical request) or
|
|
213
|
+
* `template_not_ready` (rebuild the template; retrying cannot help).
|
|
214
|
+
*/
|
|
215
|
+
code;
|
|
176
216
|
constructor(message, opts) {
|
|
177
217
|
super(message);
|
|
178
218
|
this.name = "SandboxError";
|
|
179
219
|
this.sandboxId = opts?.sandboxId;
|
|
220
|
+
this.code = opts?.code;
|
|
180
221
|
}
|
|
181
222
|
};
|
|
182
223
|
var TimeoutError = class extends SandboxError {
|
|
@@ -222,9 +263,15 @@ var TemplateError = class extends SandboxError {
|
|
|
222
263
|
}
|
|
223
264
|
};
|
|
224
265
|
var BuildError = class extends TemplateError {
|
|
266
|
+
/** The failed build's ID, when the error comes from a build that ran (`Template.build`). */
|
|
267
|
+
buildId;
|
|
268
|
+
/** The failed build's full output, when the error comes from a build that ran. */
|
|
269
|
+
logs;
|
|
225
270
|
constructor(message, opts) {
|
|
226
271
|
super(message, opts);
|
|
227
272
|
this.name = "BuildError";
|
|
273
|
+
this.buildId = opts?.buildId;
|
|
274
|
+
this.logs = opts?.logs ?? [];
|
|
228
275
|
}
|
|
229
276
|
};
|
|
230
277
|
var FileUploadError = class extends SandboxError {
|
|
@@ -434,6 +481,15 @@ var ApiClient = class {
|
|
|
434
481
|
await this.delay(attempt);
|
|
435
482
|
continue;
|
|
436
483
|
}
|
|
484
|
+
if (response.status === 409 && attempt < this.maxRetries - 1) {
|
|
485
|
+
const parsed = await this.readErrorBody(response);
|
|
486
|
+
if (parsed.code === CODE_IDEMPOTENCY_IN_PROGRESS) {
|
|
487
|
+
const after = retryAfterMs(response);
|
|
488
|
+
await (after !== void 0 ? new Promise((r) => setTimeout(r, after)) : this.delay(attempt));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
throw this.errorFrom(response, parsed);
|
|
492
|
+
}
|
|
437
493
|
if (!response.ok) {
|
|
438
494
|
throw await this.buildError(response);
|
|
439
495
|
}
|
|
@@ -459,20 +515,36 @@ var ApiClient = class {
|
|
|
459
515
|
`Request failed after ${this.maxRetries} retries: ${lastError?.message ?? "unknown error"}`
|
|
460
516
|
);
|
|
461
517
|
}
|
|
462
|
-
|
|
463
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Read an error body ONCE.
|
|
520
|
+
*
|
|
521
|
+
* `Response` bodies are single-read streams, so the 409 path cannot inspect
|
|
522
|
+
* the code and then hand the response to a separate error builder — the
|
|
523
|
+
* second read yields nothing and the error loses its message. Everything that
|
|
524
|
+
* needs the body goes through here, and the parsed result is passed around
|
|
525
|
+
* instead of the response.
|
|
526
|
+
*/
|
|
527
|
+
async readErrorBody(response) {
|
|
464
528
|
try {
|
|
465
|
-
const body = await response.
|
|
529
|
+
const body = JSON.parse(await response.text());
|
|
466
530
|
const bodyMsg = body.message ?? body.error ?? response.statusText;
|
|
467
|
-
|
|
531
|
+
return {
|
|
532
|
+
message: `HTTP ${response.status}: ${bodyMsg}`,
|
|
533
|
+
code: typeof body.code === "string" ? body.code : void 0
|
|
534
|
+
};
|
|
468
535
|
} catch {
|
|
469
|
-
message
|
|
536
|
+
return { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
470
537
|
}
|
|
538
|
+
}
|
|
539
|
+
errorFrom(response, parsed) {
|
|
471
540
|
const ErrorClass = STATUS_ERROR_MAP[response.status];
|
|
472
541
|
if (ErrorClass) {
|
|
473
|
-
return new ErrorClass(message);
|
|
542
|
+
return new ErrorClass(parsed.message, { code: parsed.code });
|
|
474
543
|
}
|
|
475
|
-
return new SandboxError(message);
|
|
544
|
+
return new SandboxError(parsed.message, { code: parsed.code });
|
|
545
|
+
}
|
|
546
|
+
async buildError(response) {
|
|
547
|
+
return this.errorFrom(response, await this.readErrorBody(response));
|
|
476
548
|
}
|
|
477
549
|
async parseResponseBody(response) {
|
|
478
550
|
const contentLength = response.headers.get("content-length");
|
|
@@ -2681,9 +2753,11 @@ var Sandbox = class _Sandbox {
|
|
|
2681
2753
|
if (opts?.volumes && opts.volumes.length > 0) {
|
|
2682
2754
|
body.volumes = opts.volumes.map(volumeAttachmentToJSON);
|
|
2683
2755
|
}
|
|
2756
|
+
const idempotencyKey = newIdempotencyKey();
|
|
2684
2757
|
const data = await client.post("/sandboxes", {
|
|
2685
2758
|
json: body,
|
|
2686
|
-
timeout: opts?.requestTimeout
|
|
2759
|
+
timeout: opts?.requestTimeout,
|
|
2760
|
+
...idempotencyKey ? { headers: { "Idempotency-Key": idempotencyKey } } : {}
|
|
2687
2761
|
});
|
|
2688
2762
|
const sandboxId = data.sandbox_id;
|
|
2689
2763
|
assertValidId(sandboxId, "sandbox ID (from server)");
|
|
@@ -3180,7 +3254,13 @@ var TemplateBase = class {
|
|
|
3180
3254
|
this._runCmds.push(cmds);
|
|
3181
3255
|
return this;
|
|
3182
3256
|
}
|
|
3183
|
-
/**
|
|
3257
|
+
/**
|
|
3258
|
+
* Copy a local file into the template.
|
|
3259
|
+
*
|
|
3260
|
+
* Not supported yet: a template build cannot upload local files, so
|
|
3261
|
+
* building a template that uses `copy()` throws `InvalidArgumentError`.
|
|
3262
|
+
* Fetch the file in a `runCmd` step, or use `fromDockerfile`.
|
|
3263
|
+
*/
|
|
3184
3264
|
copy(src, dst, mode) {
|
|
3185
3265
|
this._copies.push({ src, dst, mode });
|
|
3186
3266
|
return this;
|
|
@@ -3200,6 +3280,14 @@ var TemplateBase = class {
|
|
|
3200
3280
|
this._startCmd = cmd;
|
|
3201
3281
|
return this;
|
|
3202
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Whether `copy()` was used. Builds reject such a template until builds can
|
|
3285
|
+
* upload local files.
|
|
3286
|
+
* @internal
|
|
3287
|
+
*/
|
|
3288
|
+
hasCopies() {
|
|
3289
|
+
return this._copies.length > 0;
|
|
3290
|
+
}
|
|
3203
3291
|
/** Serialize the template to a JSON-friendly object. */
|
|
3204
3292
|
toJSON() {
|
|
3205
3293
|
if (this._dockerfile !== void 0) {
|
|
@@ -3211,14 +3299,11 @@ var TemplateBase = class {
|
|
|
3211
3299
|
if (this._runCmds.length > 0) {
|
|
3212
3300
|
result.run_cmds = this._runCmds.map((c) => c.join(" "));
|
|
3213
3301
|
}
|
|
3214
|
-
if (this._copies.length > 0) {
|
|
3215
|
-
result.copies = this._copies;
|
|
3216
|
-
}
|
|
3217
3302
|
if (Object.keys(this._envs).length > 0) {
|
|
3218
3303
|
result.envs = this._envs;
|
|
3219
3304
|
}
|
|
3220
3305
|
if (this._aptPackages.length > 0) {
|
|
3221
|
-
result.
|
|
3306
|
+
result.packages = this._aptPackages;
|
|
3222
3307
|
}
|
|
3223
3308
|
if (this._startCmd !== void 0) {
|
|
3224
3309
|
result.start_cmd = this._startCmd;
|
|
@@ -3230,19 +3315,70 @@ function parseBuildInfo(data) {
|
|
|
3230
3315
|
return {
|
|
3231
3316
|
buildId: data.build_id ?? data.buildId ?? "",
|
|
3232
3317
|
status: data.status ?? "",
|
|
3233
|
-
templateId: data.template_id ?? data.templateId ?? void 0
|
|
3318
|
+
templateId: data.template_id ?? data.templateId ?? void 0,
|
|
3319
|
+
logs: data.logs ?? []
|
|
3234
3320
|
};
|
|
3235
3321
|
}
|
|
3236
3322
|
function parseTemplateBuildStatus(data) {
|
|
3237
3323
|
return {
|
|
3238
3324
|
buildId: data.build_id ?? data.buildId ?? "",
|
|
3239
3325
|
status: data.status ?? "",
|
|
3240
|
-
|
|
3326
|
+
// A build with no output yet serializes its logs as null.
|
|
3327
|
+
logs: data.logs ?? [],
|
|
3328
|
+
templateId: data.template_id ?? data.templateId ?? void 0
|
|
3241
3329
|
};
|
|
3242
3330
|
}
|
|
3243
3331
|
|
|
3244
3332
|
// src/template/template.ts
|
|
3245
3333
|
var VALID_BUILD_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
3334
|
+
var BUILD_STATUS_COMPLETED = "completed";
|
|
3335
|
+
var BUILD_STATUS_FAILED = "failed";
|
|
3336
|
+
var DEFAULT_BUILD_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
3337
|
+
var FAILURE_LOG_LINES = 20;
|
|
3338
|
+
var buildPolling = {
|
|
3339
|
+
/** How often a waiting build is re-read. */
|
|
3340
|
+
intervalMs: 3e3,
|
|
3341
|
+
/**
|
|
3342
|
+
* How long status checks may keep failing temporarily (5xx, 408, 429, or no
|
|
3343
|
+
* response) before the wait gives up. It rides out a sandbox-manager
|
|
3344
|
+
* restart; the build itself keeps running.
|
|
3345
|
+
*/
|
|
3346
|
+
errorWindowMs: 2 * 60 * 1e3
|
|
3347
|
+
};
|
|
3348
|
+
var BUILD_LOG_TRUNCATION_NOTICE = "... [earlier build output truncated]";
|
|
3349
|
+
var LOG_ANCHOR_LINES = 64;
|
|
3350
|
+
var REJECTIONS = [
|
|
3351
|
+
AuthenticationError,
|
|
3352
|
+
ConflictError,
|
|
3353
|
+
InvalidArgumentError,
|
|
3354
|
+
NotEnoughSpaceError,
|
|
3355
|
+
NotFoundError
|
|
3356
|
+
];
|
|
3357
|
+
var LogCursor = class {
|
|
3358
|
+
seen = 0;
|
|
3359
|
+
tail = [];
|
|
3360
|
+
newLines(logs) {
|
|
3361
|
+
const fresh = logs.length === 0 || logs[0] !== BUILD_LOG_TRUNCATION_NOTICE ? logs.slice(this.seen) : this.afterTail(logs);
|
|
3362
|
+
this.seen = logs.length;
|
|
3363
|
+
this.tail = [...this.tail, ...fresh].slice(-LOG_ANCHOR_LINES);
|
|
3364
|
+
return fresh;
|
|
3365
|
+
}
|
|
3366
|
+
afterTail(logs) {
|
|
3367
|
+
const window = logs.slice(1);
|
|
3368
|
+
const n = this.tail.length;
|
|
3369
|
+
if (n > 0) {
|
|
3370
|
+
for (let end = window.length; end >= n; end--) {
|
|
3371
|
+
if (window[end - 1] === this.tail[n - 1] && this.tail.every((line, i) => window[end - n + i] === line)) {
|
|
3372
|
+
return window.slice(end);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return logs;
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3379
|
+
function isTemporaryPollError(err) {
|
|
3380
|
+
return err instanceof SandboxError && !REJECTIONS.some((Rejection) => err instanceof Rejection);
|
|
3381
|
+
}
|
|
3246
3382
|
function assertValidBuildId(buildId) {
|
|
3247
3383
|
if (!buildId || !VALID_BUILD_ID_RE.test(buildId)) {
|
|
3248
3384
|
throw new InvalidArgumentError(
|
|
@@ -3250,72 +3386,131 @@ function assertValidBuildId(buildId) {
|
|
|
3250
3386
|
);
|
|
3251
3387
|
}
|
|
3252
3388
|
}
|
|
3389
|
+
function buildRequestBody(template, alias, opts) {
|
|
3390
|
+
if (template.hasCopies()) {
|
|
3391
|
+
throw new InvalidArgumentError(
|
|
3392
|
+
"TemplateBase.copy() is not supported yet: a template build cannot upload local files. Fetch them in a runCmd step, or use fromDockerfile()."
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
const body = {
|
|
3396
|
+
template: template.toJSON(),
|
|
3397
|
+
alias
|
|
3398
|
+
};
|
|
3399
|
+
if (opts?.cpuCount !== void 0) {
|
|
3400
|
+
body.cpu_count = opts.cpuCount;
|
|
3401
|
+
}
|
|
3402
|
+
if (opts?.memoryMb !== void 0) {
|
|
3403
|
+
body.memory_mb = opts.memoryMb;
|
|
3404
|
+
}
|
|
3405
|
+
if (opts?.diskMb !== void 0) {
|
|
3406
|
+
body.disk_mb = opts.diskMb;
|
|
3407
|
+
}
|
|
3408
|
+
return body;
|
|
3409
|
+
}
|
|
3410
|
+
function buildFailedError(status) {
|
|
3411
|
+
let message = `template build ${status.buildId} failed`;
|
|
3412
|
+
const tail = status.logs.slice(-FAILURE_LOG_LINES);
|
|
3413
|
+
if (tail.length > 0) {
|
|
3414
|
+
message += ":\n" + tail.join("\n");
|
|
3415
|
+
}
|
|
3416
|
+
return new BuildError(message, { buildId: status.buildId, logs: status.logs });
|
|
3417
|
+
}
|
|
3253
3418
|
var Template = class {
|
|
3254
3419
|
/**
|
|
3255
|
-
* Build a template and wait for
|
|
3420
|
+
* Build a template and wait for the build to finish.
|
|
3421
|
+
*
|
|
3422
|
+
* Builds usually take several minutes. While waiting, each new line of build
|
|
3423
|
+
* output is passed to `onBuildLogs`. Sandboxes are created from the finished
|
|
3424
|
+
* template by its alias: `Sandbox.create({ template: alias })`.
|
|
3256
3425
|
*
|
|
3257
|
-
*
|
|
3258
|
-
*
|
|
3426
|
+
* @throws {BuildError} The build failed; the error carries its logs.
|
|
3427
|
+
* @throws {TimeoutError} The build was still running after `buildTimeout`.
|
|
3428
|
+
* It keeps running; follow it with `getBuildStatus()`.
|
|
3429
|
+
* @throws {InvalidArgumentError} The template uses `copy()`, which is not
|
|
3430
|
+
* supported yet.
|
|
3259
3431
|
*/
|
|
3260
3432
|
static async build(template, alias, opts) {
|
|
3433
|
+
const body = buildRequestBody(template, alias, opts);
|
|
3261
3434
|
const config = new ConnectionConfig({
|
|
3262
3435
|
apiKey: opts?.apiKey,
|
|
3263
3436
|
domain: opts?.domain,
|
|
3264
3437
|
requestTimeout: opts?.requestTimeout
|
|
3265
3438
|
});
|
|
3266
3439
|
const client = getSharedClient(config);
|
|
3267
|
-
const body = {
|
|
3268
|
-
template: template.toJSON(),
|
|
3269
|
-
alias
|
|
3270
|
-
};
|
|
3271
|
-
if (opts?.cpuCount !== void 0) {
|
|
3272
|
-
body.cpu_count = opts.cpuCount;
|
|
3273
|
-
}
|
|
3274
|
-
if (opts?.memoryMb !== void 0) {
|
|
3275
|
-
body.memory_mb = opts.memoryMb;
|
|
3276
|
-
}
|
|
3277
|
-
if (opts?.diskMb !== void 0) {
|
|
3278
|
-
body.disk_mb = opts.diskMb;
|
|
3279
|
-
}
|
|
3280
3440
|
const data = await client.post("/templates/build", {
|
|
3281
3441
|
json: body,
|
|
3282
3442
|
timeout: opts?.requestTimeout
|
|
3283
3443
|
});
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3444
|
+
const info = parseBuildInfo(data);
|
|
3445
|
+
assertValidBuildId(info.buildId);
|
|
3446
|
+
let status = {
|
|
3447
|
+
buildId: info.buildId,
|
|
3448
|
+
status: info.status,
|
|
3449
|
+
logs: info.logs,
|
|
3450
|
+
templateId: info.templateId
|
|
3451
|
+
};
|
|
3452
|
+
const buildTimeout = opts?.buildTimeout ?? DEFAULT_BUILD_TIMEOUT_MS;
|
|
3453
|
+
const deadline = Date.now() + buildTimeout;
|
|
3454
|
+
const timedOut = (cause) => new TimeoutError(
|
|
3455
|
+
`template build ${info.buildId} was still running after ${buildTimeout}ms. It keeps running; follow it with Template.getBuildStatus('${info.buildId}').` + (cause instanceof Error ? ` Last status check: ${cause.message}` : "")
|
|
3456
|
+
);
|
|
3457
|
+
const cursor = new LogCursor();
|
|
3458
|
+
for (; ; ) {
|
|
3459
|
+
if (opts?.onBuildLogs) {
|
|
3460
|
+
for (const line of cursor.newLines(status.logs)) {
|
|
3461
|
+
opts.onBuildLogs(line);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (status.status === BUILD_STATUS_COMPLETED) {
|
|
3465
|
+
return {
|
|
3466
|
+
buildId: status.buildId,
|
|
3467
|
+
status: status.status,
|
|
3468
|
+
templateId: status.templateId ?? info.templateId,
|
|
3469
|
+
logs: status.logs
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
if (status.status === BUILD_STATUS_FAILED) {
|
|
3473
|
+
throw buildFailedError(status);
|
|
3474
|
+
}
|
|
3475
|
+
if (Date.now() >= deadline) {
|
|
3476
|
+
throw timedOut();
|
|
3477
|
+
}
|
|
3478
|
+
let failingSince;
|
|
3479
|
+
for (; ; ) {
|
|
3480
|
+
await new Promise((resolve) => setTimeout(resolve, buildPolling.intervalMs));
|
|
3481
|
+
try {
|
|
3482
|
+
const next = await client.get(`/templates/builds/${info.buildId}`, {
|
|
3483
|
+
timeout: opts?.requestTimeout
|
|
3484
|
+
});
|
|
3485
|
+
status = parseTemplateBuildStatus(next);
|
|
3486
|
+
break;
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
if (!isTemporaryPollError(err)) {
|
|
3489
|
+
throw err;
|
|
3490
|
+
}
|
|
3491
|
+
failingSince ??= Date.now();
|
|
3492
|
+
if (Date.now() - failingSince >= buildPolling.errorWindowMs) {
|
|
3493
|
+
throw err;
|
|
3494
|
+
}
|
|
3495
|
+
if (Date.now() >= deadline) {
|
|
3496
|
+
throw timedOut(err);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3289
3499
|
}
|
|
3290
3500
|
}
|
|
3291
|
-
return result;
|
|
3292
3501
|
}
|
|
3293
3502
|
/**
|
|
3294
|
-
* Start a template build
|
|
3295
|
-
*
|
|
3296
|
-
* Sends POST /templates/build with `background: true`.
|
|
3503
|
+
* Start a template build and return as soon as the server has accepted it,
|
|
3504
|
+
* with status `building`. Follow the build with `getBuildStatus()`.
|
|
3297
3505
|
*/
|
|
3298
3506
|
static async buildInBackground(template, alias, opts) {
|
|
3507
|
+
const body = buildRequestBody(template, alias, opts);
|
|
3299
3508
|
const config = new ConnectionConfig({
|
|
3300
3509
|
apiKey: opts?.apiKey,
|
|
3301
3510
|
domain: opts?.domain,
|
|
3302
3511
|
requestTimeout: opts?.requestTimeout
|
|
3303
3512
|
});
|
|
3304
3513
|
const client = getSharedClient(config);
|
|
3305
|
-
const body = {
|
|
3306
|
-
template: template.toJSON(),
|
|
3307
|
-
alias,
|
|
3308
|
-
background: true
|
|
3309
|
-
};
|
|
3310
|
-
if (opts?.cpuCount !== void 0) {
|
|
3311
|
-
body.cpu_count = opts.cpuCount;
|
|
3312
|
-
}
|
|
3313
|
-
if (opts?.memoryMb !== void 0) {
|
|
3314
|
-
body.memory_mb = opts.memoryMb;
|
|
3315
|
-
}
|
|
3316
|
-
if (opts?.diskMb !== void 0) {
|
|
3317
|
-
body.disk_mb = opts.diskMb;
|
|
3318
|
-
}
|
|
3319
3514
|
const data = await client.post("/templates/build", {
|
|
3320
3515
|
json: body,
|
|
3321
3516
|
timeout: opts?.requestTimeout
|
|
@@ -3323,7 +3518,7 @@ var Template = class {
|
|
|
3323
3518
|
return parseBuildInfo(data);
|
|
3324
3519
|
}
|
|
3325
3520
|
/**
|
|
3326
|
-
* Get the status of a template build.
|
|
3521
|
+
* Get the status of a template build, including its logs so far.
|
|
3327
3522
|
*
|
|
3328
3523
|
* Sends GET /templates/builds/:buildId.
|
|
3329
3524
|
*/
|
|
@@ -3865,6 +4060,9 @@ var Governance = class {
|
|
|
3865
4060
|
ApiClient,
|
|
3866
4061
|
AuthenticationError,
|
|
3867
4062
|
BuildError,
|
|
4063
|
+
CODE_IDEMPOTENCY_IN_PROGRESS,
|
|
4064
|
+
CODE_IDEMPOTENCY_KEY_REUSED,
|
|
4065
|
+
CODE_TEMPLATE_NOT_READY,
|
|
3868
4066
|
CommandExitError,
|
|
3869
4067
|
CommandHandle,
|
|
3870
4068
|
Commands,
|