@mastra/docker 0.11.1-alpha.1 → 0.12.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/README.md +37 -0
- package/dist/index.cjs +350 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +348 -16
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +8 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/template/build-progress.d.ts +92 -0
- package/dist/template/build-progress.d.ts.map +1 -0
- package/dist/template/build-session.d.ts.map +1 -1
- package/dist/template/index.d.ts +1 -0
- package/dist/template/index.d.ts.map +1 -1
- package/dist/template/template.d.ts +32 -2
- package/dist/template/template.d.ts.map +1 -1
- package/package.json +8 -9
package/README.md
CHANGED
|
@@ -141,6 +141,43 @@ const template = new DockerTemplate({ secrets: { GITHUB_TOKEN: token } })
|
|
|
141
141
|
`dispose()` removes the image; Docker refuses while a container still references
|
|
142
142
|
it, so destroy the template's sandboxes first.
|
|
143
143
|
|
|
144
|
+
#### Build progress
|
|
145
|
+
|
|
146
|
+
Pass `onProgress` to `build()` (or `onTemplateBuildProgress` to `DockerSandbox`)
|
|
147
|
+
to follow a long preparation while it runs:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const result = await template.build({
|
|
151
|
+
onProgress: event => {
|
|
152
|
+
if (event.type === 'step-start') console.log(`> ${event.step.name}${event.step.cached ? ' (cached)' : ''}`);
|
|
153
|
+
if (event.type === 'output') process.stdout.write(redact(event.text));
|
|
154
|
+
},
|
|
155
|
+
maxOutputBytes: 32 * 1024, // default 64 KiB
|
|
156
|
+
});
|
|
157
|
+
if (result.status === 'failed') {
|
|
158
|
+
console.error(result.error, result.failedStep?.name, result.output, result.outputTruncated);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Events are `reused` (the image already exists, nothing is built),
|
|
163
|
+
`build-start` (with `builder: 'legacy' | 'buildkit'`), `step-start`,
|
|
164
|
+
`step-complete` (with `cached` when the builder reports a cache hit), `output`,
|
|
165
|
+
and `build-complete`. Plain builds use the legacy builder, which reports
|
|
166
|
+
`Step N/M` steps; builds with secrets use BuildKit, which reports build
|
|
167
|
+
vertices. The callback contract:
|
|
168
|
+
|
|
169
|
+
- Delivery is synchronous, in arrival order, with no backpressure — keep the
|
|
170
|
+
callback fast and hand heavy work off elsewhere.
|
|
171
|
+
- Errors thrown by the callback are ignored and never fail the build.
|
|
172
|
+
- Concurrent callers sharing a build each receive its events from the moment
|
|
173
|
+
they join (no replay). A caller that aborts stops receiving events; the build
|
|
174
|
+
continues for the remaining callers.
|
|
175
|
+
- Only the most recent `maxOutputBytes` of output are retained, and only to
|
|
176
|
+
populate `output` on failed results; `outputTruncated` reports whether older
|
|
177
|
+
output was dropped.
|
|
178
|
+
- `output` text is raw command output. Commands run with `runWithSecrets` can
|
|
179
|
+
echo secret values into it, so redact before logging or persisting.
|
|
180
|
+
|
|
144
181
|
#### Repository templates
|
|
145
182
|
|
|
146
183
|
`createDockerRepoTemplate` prepares a repository checkout plus setup commands,
|
package/dist/index.cjs
CHANGED
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
@@ -607,6 +607,7 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
607
607
|
/** Image the container boots from; rewritten by a template resolution on `start()`. */
|
|
608
608
|
_image;
|
|
609
609
|
_templateSpec;
|
|
610
|
+
_onTemplateBuildProgress;
|
|
610
611
|
_workingDirectoryWasSet;
|
|
611
612
|
_command;
|
|
612
613
|
_env;
|
|
@@ -650,6 +651,7 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
650
651
|
this._containerName = sanitizeContainerName(options.name ?? this.id);
|
|
651
652
|
if (options.image !== void 0 && options.template !== void 0) throw new TypeError("DockerSandbox: `image` and `template` are mutually exclusive");
|
|
652
653
|
this._templateSpec = options.template;
|
|
654
|
+
this._onTemplateBuildProgress = options.onTemplateBuildProgress;
|
|
653
655
|
this._image = options.image ?? "node:22-slim";
|
|
654
656
|
this._workingDirectoryWasSet = options.workingDirectory !== void 0 || options.workingDir !== void 0;
|
|
655
657
|
this._command = options.command ?? ["sleep", "infinity"];
|
|
@@ -1009,7 +1011,8 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
1009
1011
|
throwIfAborted(abortSignal, "start Docker sandbox");
|
|
1010
1012
|
const result = await template.build({
|
|
1011
1013
|
docker: this._docker,
|
|
1012
|
-
abortSignal
|
|
1014
|
+
abortSignal,
|
|
1015
|
+
...this._onTemplateBuildProgress && { onProgress: this._onTemplateBuildProgress }
|
|
1013
1016
|
});
|
|
1014
1017
|
if (result.status !== "ready") throw new _mastra_core_workspace.SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
|
|
1015
1018
|
templateId: result.templateId,
|
|
@@ -1081,7 +1084,7 @@ function normalizeHostConfigValue(field, value) {
|
|
|
1081
1084
|
}
|
|
1082
1085
|
if (field === "Tmpfs" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
1083
1086
|
if (Object.keys(value).length === 0) return void 0;
|
|
1084
|
-
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([path
|
|
1087
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([path, options]) => [path, typeof options === "string" ? normalizeTmpfsOptions(options) : options]));
|
|
1085
1088
|
}
|
|
1086
1089
|
if (Array.isArray(value)) {
|
|
1087
1090
|
if (field === "Ulimits" && value.length === 0) return void 0;
|
|
@@ -1362,6 +1365,287 @@ const dockerSandboxProvider = {
|
|
|
1362
1365
|
createSandbox: (config) => new DockerSandbox(config)
|
|
1363
1366
|
};
|
|
1364
1367
|
//#endregion
|
|
1368
|
+
//#region src/template/build-progress.ts
|
|
1369
|
+
/**
|
|
1370
|
+
* Build progress for `DockerTemplate.build()`.
|
|
1371
|
+
*
|
|
1372
|
+
* The daemon reports build progress as newline-delimited JSON. The legacy
|
|
1373
|
+
* builder sends plain text (`{stream: "Step 2/5 : RUN ..."}`); BuildKit sends
|
|
1374
|
+
* `{id: 'moby.buildkit.trace', aux: <base64 StatusResponse protobuf>}`. This
|
|
1375
|
+
* module normalizes both into {@link DockerTemplateBuildEvent}s and keeps a
|
|
1376
|
+
* bounded tail of raw output for failure diagnostics.
|
|
1377
|
+
*/
|
|
1378
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 65536;
|
|
1379
|
+
/** Keeps at most `maxBytes` (UTF-8) of the most recent text. */
|
|
1380
|
+
var BoundedTail = class {
|
|
1381
|
+
#maxBytes;
|
|
1382
|
+
#chunks = [];
|
|
1383
|
+
#bytes = 0;
|
|
1384
|
+
#truncated = false;
|
|
1385
|
+
constructor(maxBytes) {
|
|
1386
|
+
this.#maxBytes = maxBytes;
|
|
1387
|
+
}
|
|
1388
|
+
get truncated() {
|
|
1389
|
+
return this.#truncated;
|
|
1390
|
+
}
|
|
1391
|
+
push(text) {
|
|
1392
|
+
if (text.length === 0) return;
|
|
1393
|
+
let chunk = Buffer.from(text, "utf8");
|
|
1394
|
+
if (chunk.length > this.#maxBytes) {
|
|
1395
|
+
chunk = chunk.subarray(chunk.length - this.#maxBytes);
|
|
1396
|
+
this.#chunks = [];
|
|
1397
|
+
this.#bytes = 0;
|
|
1398
|
+
this.#truncated = true;
|
|
1399
|
+
}
|
|
1400
|
+
this.#chunks.push(chunk);
|
|
1401
|
+
this.#bytes += chunk.length;
|
|
1402
|
+
while (this.#bytes > this.#maxBytes) {
|
|
1403
|
+
const head = this.#chunks[0];
|
|
1404
|
+
const excess = this.#bytes - this.#maxBytes;
|
|
1405
|
+
this.#truncated = true;
|
|
1406
|
+
if (head.length <= excess) {
|
|
1407
|
+
this.#chunks.shift();
|
|
1408
|
+
this.#bytes -= head.length;
|
|
1409
|
+
} else {
|
|
1410
|
+
this.#chunks[0] = head.subarray(excess);
|
|
1411
|
+
this.#bytes -= excess;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
toString() {
|
|
1416
|
+
let text = Buffer.concat(this.#chunks).toString("utf8");
|
|
1417
|
+
if (this.#truncated) text = text.replace(/^\uFFFD+/, "");
|
|
1418
|
+
return text;
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
function readFields(buffer) {
|
|
1422
|
+
const fields = [];
|
|
1423
|
+
let offset = 0;
|
|
1424
|
+
const readVarint = () => {
|
|
1425
|
+
let result = 0;
|
|
1426
|
+
let shift = 0;
|
|
1427
|
+
for (;;) {
|
|
1428
|
+
if (offset >= buffer.length) throw new Error("truncated varint");
|
|
1429
|
+
const byte = buffer[offset++];
|
|
1430
|
+
result += (byte & 127) * 2 ** shift;
|
|
1431
|
+
if ((byte & 128) === 0) return result;
|
|
1432
|
+
shift += 7;
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
while (offset < buffer.length) {
|
|
1436
|
+
const key = readVarint();
|
|
1437
|
+
const field = Math.floor(key / 8);
|
|
1438
|
+
const wireType = key % 8;
|
|
1439
|
+
switch (wireType) {
|
|
1440
|
+
case 0:
|
|
1441
|
+
fields.push({
|
|
1442
|
+
field,
|
|
1443
|
+
value: readVarint()
|
|
1444
|
+
});
|
|
1445
|
+
break;
|
|
1446
|
+
case 1:
|
|
1447
|
+
offset += 8;
|
|
1448
|
+
break;
|
|
1449
|
+
case 5:
|
|
1450
|
+
offset += 4;
|
|
1451
|
+
break;
|
|
1452
|
+
case 2: {
|
|
1453
|
+
const length = readVarint();
|
|
1454
|
+
if (offset + length > buffer.length) throw new Error("truncated field");
|
|
1455
|
+
fields.push({
|
|
1456
|
+
field,
|
|
1457
|
+
value: buffer.subarray(offset, offset + length)
|
|
1458
|
+
});
|
|
1459
|
+
offset += length;
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
default: throw new Error(`unsupported protobuf wire type ${wireType}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return fields;
|
|
1466
|
+
}
|
|
1467
|
+
/** @internal exported for tests. Throws on malformed input. */
|
|
1468
|
+
function decodeBuildkitStatus(aux) {
|
|
1469
|
+
const vertexes = [];
|
|
1470
|
+
const logs = [];
|
|
1471
|
+
for (const { field, value } of readFields(Buffer.from(aux, "base64"))) {
|
|
1472
|
+
if (!Buffer.isBuffer(value)) continue;
|
|
1473
|
+
if (field === 1) {
|
|
1474
|
+
const vertex = {
|
|
1475
|
+
digest: "",
|
|
1476
|
+
cached: false,
|
|
1477
|
+
started: false,
|
|
1478
|
+
completed: false
|
|
1479
|
+
};
|
|
1480
|
+
for (const f of readFields(value)) if (f.field === 1 && Buffer.isBuffer(f.value)) vertex.digest = f.value.toString("utf8");
|
|
1481
|
+
else if (f.field === 3 && Buffer.isBuffer(f.value)) vertex.name = f.value.toString("utf8");
|
|
1482
|
+
else if (f.field === 4) vertex.cached = f.value !== 0;
|
|
1483
|
+
else if (f.field === 5) vertex.started = true;
|
|
1484
|
+
else if (f.field === 6) vertex.completed = true;
|
|
1485
|
+
else if (f.field === 7 && Buffer.isBuffer(f.value) && f.value.length > 0) vertex.error = f.value.toString("utf8");
|
|
1486
|
+
if (vertex.digest) vertexes.push(vertex);
|
|
1487
|
+
} else if (field === 3) {
|
|
1488
|
+
let vertex = "";
|
|
1489
|
+
let data = Buffer.alloc(0);
|
|
1490
|
+
for (const f of readFields(value)) if (f.field === 1 && Buffer.isBuffer(f.value)) vertex = f.value.toString("utf8");
|
|
1491
|
+
else if (f.field === 4 && Buffer.isBuffer(f.value)) data = f.value;
|
|
1492
|
+
logs.push({
|
|
1493
|
+
vertex,
|
|
1494
|
+
data
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
vertexes,
|
|
1500
|
+
logs
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const LEGACY_STEP = /^Step (\d+)\/(\d+) : (.*)$/;
|
|
1504
|
+
function createBuildEventNormalizer(emit, tail) {
|
|
1505
|
+
let failure;
|
|
1506
|
+
let failedStep;
|
|
1507
|
+
let legacyStep;
|
|
1508
|
+
let legacyCached;
|
|
1509
|
+
let pending = "";
|
|
1510
|
+
const vertexes = /* @__PURE__ */ new Map();
|
|
1511
|
+
const decoders = /* @__PURE__ */ new Map();
|
|
1512
|
+
const output = (text, stepId) => {
|
|
1513
|
+
if (!text) return;
|
|
1514
|
+
tail.push(text);
|
|
1515
|
+
emit({
|
|
1516
|
+
type: "output",
|
|
1517
|
+
...stepId !== void 0 && { stepId },
|
|
1518
|
+
text
|
|
1519
|
+
});
|
|
1520
|
+
};
|
|
1521
|
+
const completeLegacyStep = (error) => {
|
|
1522
|
+
if (!legacyStep) return;
|
|
1523
|
+
const step = {
|
|
1524
|
+
...legacyStep,
|
|
1525
|
+
...legacyCached !== void 0 && { cached: legacyCached }
|
|
1526
|
+
};
|
|
1527
|
+
if (error !== void 0) failedStep = step;
|
|
1528
|
+
emit({
|
|
1529
|
+
type: "step-complete",
|
|
1530
|
+
step,
|
|
1531
|
+
...error !== void 0 && { error }
|
|
1532
|
+
});
|
|
1533
|
+
legacyStep = void 0;
|
|
1534
|
+
legacyCached = void 0;
|
|
1535
|
+
};
|
|
1536
|
+
const handleLegacyLine = (line) => {
|
|
1537
|
+
const match = LEGACY_STEP.exec(line.replace(/\r?\n$/, ""));
|
|
1538
|
+
if (match) {
|
|
1539
|
+
completeLegacyStep();
|
|
1540
|
+
legacyStep = {
|
|
1541
|
+
id: match[1],
|
|
1542
|
+
name: match[3],
|
|
1543
|
+
index: Number(match[1]),
|
|
1544
|
+
total: Number(match[2])
|
|
1545
|
+
};
|
|
1546
|
+
emit({
|
|
1547
|
+
type: "step-start",
|
|
1548
|
+
step: { ...legacyStep }
|
|
1549
|
+
});
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (legacyStep) {
|
|
1553
|
+
const trimmed = line.trim();
|
|
1554
|
+
if (trimmed === "---> Using cache") legacyCached = true;
|
|
1555
|
+
else if (trimmed.startsWith("---> Running in ") && legacyCached === void 0) legacyCached = false;
|
|
1556
|
+
}
|
|
1557
|
+
output(line, legacyStep?.id);
|
|
1558
|
+
};
|
|
1559
|
+
const handleLegacyStream = (text) => {
|
|
1560
|
+
pending += text;
|
|
1561
|
+
let newline = pending.indexOf("\n");
|
|
1562
|
+
while (newline !== -1) {
|
|
1563
|
+
handleLegacyLine(pending.slice(0, newline + 1));
|
|
1564
|
+
pending = pending.slice(newline + 1);
|
|
1565
|
+
newline = pending.indexOf("\n");
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
const flushLegacy = () => {
|
|
1569
|
+
if (pending) {
|
|
1570
|
+
handleLegacyLine(pending);
|
|
1571
|
+
pending = "";
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
const handleBuildkit = (aux) => {
|
|
1575
|
+
const { vertexes: updates, logs } = decodeBuildkitStatus(aux);
|
|
1576
|
+
for (const update of updates) {
|
|
1577
|
+
let state = vertexes.get(update.digest);
|
|
1578
|
+
if (!state) {
|
|
1579
|
+
state = {
|
|
1580
|
+
step: {
|
|
1581
|
+
id: update.digest,
|
|
1582
|
+
name: update.name ?? ""
|
|
1583
|
+
},
|
|
1584
|
+
started: false,
|
|
1585
|
+
completed: false
|
|
1586
|
+
};
|
|
1587
|
+
vertexes.set(update.digest, state);
|
|
1588
|
+
}
|
|
1589
|
+
if (update.name && !state.step.name) state.step.name = update.name;
|
|
1590
|
+
if (update.cached) state.step.cached = true;
|
|
1591
|
+
if (state.step.name.startsWith("[internal]")) continue;
|
|
1592
|
+
if (!state.started && (update.started || update.completed || update.cached || update.error)) {
|
|
1593
|
+
state.started = true;
|
|
1594
|
+
emit({
|
|
1595
|
+
type: "step-start",
|
|
1596
|
+
step: { ...state.step }
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
if (!state.completed && (update.completed || update.error)) {
|
|
1600
|
+
state.completed = true;
|
|
1601
|
+
if (state.step.cached === void 0) state.step.cached = false;
|
|
1602
|
+
const step = { ...state.step };
|
|
1603
|
+
if (update.error) failedStep = step;
|
|
1604
|
+
emit({
|
|
1605
|
+
type: "step-complete",
|
|
1606
|
+
step,
|
|
1607
|
+
...update.error && { error: update.error }
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
for (const log of logs) {
|
|
1612
|
+
if (vertexes.get(log.vertex)?.step.name.startsWith("[internal]")) continue;
|
|
1613
|
+
let decoder = decoders.get(log.vertex);
|
|
1614
|
+
if (!decoder) {
|
|
1615
|
+
decoder = new string_decoder.StringDecoder("utf8");
|
|
1616
|
+
decoders.set(log.vertex, decoder);
|
|
1617
|
+
}
|
|
1618
|
+
output(decoder.write(log.data), log.vertex || void 0);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
const handleError = (raw) => {
|
|
1622
|
+
const detail = raw.errorDetail;
|
|
1623
|
+
const message = String(detail?.message ?? raw.error ?? "docker build failed");
|
|
1624
|
+
flushLegacy();
|
|
1625
|
+
completeLegacyStep(message);
|
|
1626
|
+
if (!failure) failure = {
|
|
1627
|
+
message,
|
|
1628
|
+
...failedStep && { step: failedStep }
|
|
1629
|
+
};
|
|
1630
|
+
};
|
|
1631
|
+
return {
|
|
1632
|
+
handle(raw) {
|
|
1633
|
+
if (!raw || typeof raw !== "object") return;
|
|
1634
|
+
const event = raw;
|
|
1635
|
+
try {
|
|
1636
|
+
if (event.error !== void 0 || event.errorDetail !== void 0) handleError(event);
|
|
1637
|
+
else if (event.id === "moby.buildkit.trace" && typeof event.aux === "string") handleBuildkit(event.aux);
|
|
1638
|
+
else if (typeof event.stream === "string") handleLegacyStream(event.stream);
|
|
1639
|
+
} catch {}
|
|
1640
|
+
},
|
|
1641
|
+
finish() {
|
|
1642
|
+
flushLegacy();
|
|
1643
|
+
completeLegacyStep();
|
|
1644
|
+
},
|
|
1645
|
+
failure: () => failure
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
//#endregion
|
|
1365
1649
|
//#region src/template/build-session.ts
|
|
1366
1650
|
/**
|
|
1367
1651
|
* BuildKit session that serves build secrets to the daemon.
|
|
@@ -1643,9 +1927,7 @@ function synthesizeDockerfile(definition) {
|
|
|
1643
1927
|
case "pipInstall":
|
|
1644
1928
|
lines.push(renderPipInstall(operation.args[0], operation.args[1]));
|
|
1645
1929
|
break;
|
|
1646
|
-
case "npmInstall":
|
|
1647
|
-
lines.push(renderNpmInstall(operation.args[0], operation.args[1]));
|
|
1648
|
-
break;
|
|
1930
|
+
case "npmInstall": lines.push(renderNpmInstall(operation.args[0], operation.args[1]));
|
|
1649
1931
|
}
|
|
1650
1932
|
});
|
|
1651
1933
|
return `${lines.join("\n")}\n`;
|
|
@@ -1730,7 +2012,7 @@ function templateImageTag(definition) {
|
|
|
1730
2012
|
* ```
|
|
1731
2013
|
*/
|
|
1732
2014
|
const MAX_OPERATIONS = 256;
|
|
1733
|
-
const MAX_STRING_LENGTH =
|
|
2015
|
+
const MAX_STRING_LENGTH = 32768;
|
|
1734
2016
|
const MAX_COLLECTION_ITEMS = 512;
|
|
1735
2017
|
/**
|
|
1736
2018
|
* Immutable, chainable builder + build/lifecycle for a local Docker template.
|
|
@@ -1898,20 +2180,29 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1898
2180
|
* `options.secrets`, the template's `secrets` option, or `process.env`.
|
|
1899
2181
|
*/
|
|
1900
2182
|
async build(options = {}) {
|
|
2183
|
+
const { maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES } = options;
|
|
2184
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) throw new RangeError("maxOutputBytes must be a positive integer");
|
|
1901
2185
|
throwIfAborted(options.abortSignal, "build Docker template");
|
|
1902
2186
|
const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
|
|
1903
|
-
if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
|
|
2187
|
+
if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal, options.onProgress);
|
|
1904
2188
|
const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
|
|
1905
2189
|
const controller = new AbortController();
|
|
2190
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2191
|
+
const emit = (event) => {
|
|
2192
|
+
for (const listener of [...listeners]) try {
|
|
2193
|
+
listener(event);
|
|
2194
|
+
} catch {}
|
|
2195
|
+
};
|
|
1906
2196
|
const run = {
|
|
1907
2197
|
controller,
|
|
1908
2198
|
waiters: 0,
|
|
2199
|
+
listeners,
|
|
1909
2200
|
promise: previous.then(() => {
|
|
1910
2201
|
throwIfAborted(controller.signal, "build Docker template");
|
|
1911
2202
|
return this.#build({
|
|
1912
2203
|
...options,
|
|
1913
2204
|
abortSignal: controller.signal
|
|
1914
|
-
});
|
|
2205
|
+
}, emit, maxOutputBytes);
|
|
1915
2206
|
})
|
|
1916
2207
|
};
|
|
1917
2208
|
this.#queueTail = run;
|
|
@@ -1920,20 +2211,23 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1920
2211
|
if (this.#queueTail === run) this.#queueTail = void 0;
|
|
1921
2212
|
if (this.#plainBuild === run) this.#plainBuild = void 0;
|
|
1922
2213
|
}).catch(() => void 0);
|
|
1923
|
-
return this.#waitForBuild(run, options.abortSignal);
|
|
2214
|
+
return this.#waitForBuild(run, options.abortSignal, options.onProgress);
|
|
1924
2215
|
}
|
|
1925
|
-
#waitForBuild(run, abortSignal) {
|
|
2216
|
+
#waitForBuild(run, abortSignal, onProgress) {
|
|
1926
2217
|
run.waiters += 1;
|
|
2218
|
+
const listener = onProgress && ((event) => onProgress(event));
|
|
2219
|
+
if (listener) run.listeners.add(listener);
|
|
1927
2220
|
let settled = false;
|
|
1928
2221
|
const release = (aborted) => {
|
|
1929
2222
|
if (settled) return;
|
|
1930
2223
|
settled = true;
|
|
2224
|
+
if (listener) run.listeners.delete(listener);
|
|
1931
2225
|
run.waiters -= 1;
|
|
1932
2226
|
if (aborted && run.waiters === 0) run.controller.abort();
|
|
1933
2227
|
};
|
|
1934
2228
|
return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
|
|
1935
2229
|
}
|
|
1936
|
-
async #build(options) {
|
|
2230
|
+
async #build(options, emit, maxOutputBytes) {
|
|
1937
2231
|
const docker = options.docker ?? this.#getDocker();
|
|
1938
2232
|
const tag = this.templateId;
|
|
1939
2233
|
const { abortSignal } = options;
|
|
@@ -1942,6 +2236,10 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1942
2236
|
await docker.getImage(tag).inspect();
|
|
1943
2237
|
throwIfAborted(abortSignal, "build Docker template");
|
|
1944
2238
|
this.#built = true;
|
|
2239
|
+
emit({
|
|
2240
|
+
type: "reused",
|
|
2241
|
+
templateId: tag
|
|
2242
|
+
});
|
|
1945
2243
|
return {
|
|
1946
2244
|
status: "ready",
|
|
1947
2245
|
templateId: tag
|
|
@@ -1954,12 +2252,19 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1954
2252
|
const context = (0, tar_stream.pack)();
|
|
1955
2253
|
context.entry({ name: "Dockerfile" }, this.dockerfile);
|
|
1956
2254
|
context.finalize();
|
|
2255
|
+
const tail = new BoundedTail(maxOutputBytes);
|
|
2256
|
+
const normalizer = createBuildEventNormalizer(emit, tail);
|
|
2257
|
+
emit({
|
|
2258
|
+
type: "build-start",
|
|
2259
|
+
templateId: tag,
|
|
2260
|
+
builder: secrets ? "buildkit" : "legacy"
|
|
2261
|
+
});
|
|
1957
2262
|
try {
|
|
1958
2263
|
const nocache = options.force === true;
|
|
1959
2264
|
if (secrets) {
|
|
1960
2265
|
const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
|
|
1961
2266
|
try {
|
|
1962
|
-
await this.#followBuild(docker, stream, abortSignal);
|
|
2267
|
+
await this.#followBuild(docker, stream, normalizer, abortSignal);
|
|
1963
2268
|
} finally {
|
|
1964
2269
|
session.close();
|
|
1965
2270
|
}
|
|
@@ -1977,18 +2282,31 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1977
2282
|
}), abortSignal, "build Docker template", () => {
|
|
1978
2283
|
context.destroy?.();
|
|
1979
2284
|
});
|
|
1980
|
-
await this.#followBuild(docker, stream, abortSignal);
|
|
2285
|
+
await this.#followBuild(docker, stream, normalizer, abortSignal);
|
|
1981
2286
|
}
|
|
1982
2287
|
} catch (error) {
|
|
1983
2288
|
this.#built = false;
|
|
1984
2289
|
if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
|
|
2290
|
+
emit({
|
|
2291
|
+
type: "build-complete",
|
|
2292
|
+
status: "failed"
|
|
2293
|
+
});
|
|
2294
|
+
const failedStep = normalizer.failure()?.step;
|
|
1985
2295
|
return {
|
|
1986
2296
|
status: "failed",
|
|
1987
2297
|
templateId: tag,
|
|
1988
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2298
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2299
|
+
...failedStep && { failedStep },
|
|
2300
|
+
output: tail.toString(),
|
|
2301
|
+
outputTruncated: tail.truncated
|
|
1989
2302
|
};
|
|
1990
2303
|
}
|
|
2304
|
+
normalizer.finish();
|
|
1991
2305
|
this.#built = true;
|
|
2306
|
+
emit({
|
|
2307
|
+
type: "build-complete",
|
|
2308
|
+
status: "ready"
|
|
2309
|
+
});
|
|
1992
2310
|
return {
|
|
1993
2311
|
status: "ready",
|
|
1994
2312
|
templateId: tag
|
|
@@ -2066,7 +2384,7 @@ var DockerTemplate = class DockerTemplate {
|
|
|
2066
2384
|
session
|
|
2067
2385
|
};
|
|
2068
2386
|
}
|
|
2069
|
-
#followBuild(docker, stream, abortSignal) {
|
|
2387
|
+
#followBuild(docker, stream, normalizer, abortSignal) {
|
|
2070
2388
|
return new Promise((resolve, reject) => {
|
|
2071
2389
|
let settled = false;
|
|
2072
2390
|
const finish = (error) => {
|
|
@@ -2089,6 +2407,11 @@ var DockerTemplate = class DockerTemplate {
|
|
|
2089
2407
|
finish(err);
|
|
2090
2408
|
return;
|
|
2091
2409
|
}
|
|
2410
|
+
const reported = normalizer.failure();
|
|
2411
|
+
if (reported) {
|
|
2412
|
+
finish(new Error(reported.message));
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2092
2415
|
const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
|
|
2093
2416
|
if (failure) {
|
|
2094
2417
|
const detail = failure.errorDetail;
|
|
@@ -2096,6 +2419,8 @@ var DockerTemplate = class DockerTemplate {
|
|
|
2096
2419
|
return;
|
|
2097
2420
|
}
|
|
2098
2421
|
finish();
|
|
2422
|
+
}, (event) => {
|
|
2423
|
+
if (!settled) normalizer.handle(event);
|
|
2099
2424
|
});
|
|
2100
2425
|
});
|
|
2101
2426
|
}
|
|
@@ -2234,11 +2559,18 @@ const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
|
2234
2559
|
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
2235
2560
|
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
2236
2561
|
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
2237
|
-
return `( cd "${repoDir}" && sh -c ${shellQuote$
|
|
2562
|
+
return `( cd "${repoDir}" && sh -c ${shellQuote$2(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
|
|
2238
2563
|
}
|
|
2239
|
-
function shellQuote$
|
|
2564
|
+
function shellQuote$2(value) {
|
|
2240
2565
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2241
2566
|
}
|
|
2567
|
+
const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = {
|
|
2568
|
+
notFound: 20,
|
|
2569
|
+
isDirectory: 21,
|
|
2570
|
+
exists: 22,
|
|
2571
|
+
notDirectory: 23,
|
|
2572
|
+
destIsDirectory: 24
|
|
2573
|
+
};
|
|
2242
2574
|
//#endregion
|
|
2243
2575
|
//#region src/template/repo-template.ts
|
|
2244
2576
|
/**
|