openship 0.7.0 → 0.7.1

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.
Files changed (56) hide show
  1. package/dist/bare-AB4SMTNJ.js +29 -0
  2. package/dist/{chunk-J2NDOB7D.js → chunk-2BAKEGGF.js} +1 -1
  3. package/dist/{chunk-JAKVPLHT.js → chunk-2YAY5XHK.js} +1 -1
  4. package/dist/{chunk-YGA52DHX.js → chunk-37XZTLBJ.js} +3 -3
  5. package/dist/{chunk-GYDROM4W.js → chunk-3K65MMYV.js} +1 -1
  6. package/dist/{chunk-XQC6KYZD.js → chunk-3WBKLDGC.js} +3 -3
  7. package/dist/{chunk-5CFUYMPE.js → chunk-3YSSXSC2.js} +12 -12
  8. package/dist/{chunk-TPGBT5UR.js → chunk-7MCPDYKP.js} +621 -94
  9. package/dist/{chunk-CMGRMPRQ.js → chunk-7TQP6723.js} +2 -2
  10. package/dist/{chunk-MZYD3LFP.js → chunk-AMADTKC6.js} +1 -1
  11. package/dist/{chunk-JWW5YAFL.js → chunk-B2BQX6JX.js} +3 -3
  12. package/dist/{chunk-DZBXOF5Q.js → chunk-BBPKAQ54.js} +6 -6
  13. package/dist/{chunk-IVRTIKNZ.js → chunk-COI4VFRW.js} +34 -2
  14. package/dist/{chunk-ZFA3MZ5N.js → chunk-CQ2JOXEQ.js} +3 -3
  15. package/dist/{chunk-BOD2IQ3P.js → chunk-DQR5TPFB.js} +5 -5
  16. package/dist/{chunk-AUBB5DLY.js → chunk-ETK6IBCH.js} +6 -6
  17. package/dist/{chunk-RHHCAWW3.js → chunk-FNWNUYS3.js} +3 -3
  18. package/dist/{chunk-3KUW2UC6.js → chunk-GPTBEPUE.js} +21 -14
  19. package/dist/{chunk-TT7P5ZZ3.js → chunk-H36ASAYI.js} +8 -8
  20. package/dist/{chunk-SYQ3FOLE.js → chunk-IVMPA6RC.js} +2 -2
  21. package/dist/{chunk-SPR7N2EK.js → chunk-IXBOV7BE.js} +9 -9
  22. package/dist/{chunk-3LWGDMJU.js → chunk-KATXZ5AB.js} +1 -1
  23. package/dist/{chunk-J4SYG2LC.js → chunk-KCMEWNP7.js} +1 -1
  24. package/dist/{chunk-N5YZMO33.js → chunk-L4DRH6ZA.js} +12 -1
  25. package/dist/{chunk-GEHV7JBS.js → chunk-NF7NQOAP.js} +2 -2
  26. package/dist/{chunk-KYTTPWRP.js → chunk-NIOLZENU.js} +9 -9
  27. package/dist/{chunk-LL7M7GL4.js → chunk-OEYXXSAM.js} +4 -4
  28. package/dist/{chunk-QNQFS37A.js → chunk-S5GU345P.js} +3 -3
  29. package/dist/{chunk-HBQWOJRP.js → chunk-TKK5NPDQ.js} +2 -2
  30. package/dist/{chunk-5D27WZBW.js → chunk-WG5PDWZ3.js} +5 -5
  31. package/dist/{chunk-7MKZP4TW.js → chunk-XJSLJLNY.js} +14 -14
  32. package/dist/{cloud-RFZSO6P5.js → cloud-T2JOFEQL.js} +20 -20
  33. package/dist/{detect-NXD3PYBM.js → detect-SWLZURYX.js} +5 -5
  34. package/dist/{docker-U5APUZBI.js → docker-SQNZRZXN.js} +11 -11
  35. package/dist/{docker-edge-executor-HUQOWFQU.js → docker-edge-executor-BSSQQS76.js} +8 -8
  36. package/dist/{edge-import-UXD52WQR.js → edge-import-LUDX77C2.js} +27 -27
  37. package/dist/{edge-preflight-3PMJ5BWM.js → edge-preflight-JAEFWBRI.js} +14 -14
  38. package/dist/{ensure-container-edge-GDPY7UJD.js → ensure-container-edge-U7FTLFVJ.js} +12 -12
  39. package/dist/{executor-XOVSO6QZ.js → executor-GYRZ7RIU.js} +7 -7
  40. package/dist/host-channel-preflight-U4IZCRXR.js +54 -0
  41. package/dist/index.js +45 -45
  42. package/dist/{local-executor-KR7BFCJP.js → local-executor-JCR46VAP.js} +3 -3
  43. package/dist/{nginx-JMLAS7YS.js → nginx-YA6IRPJT.js} +10 -10
  44. package/dist/{openresty-lua-FJH3KEC4.js → openresty-lua-DUOB64TP.js} +8 -8
  45. package/dist/{privilege-H2QTJ5TE.js → privilege-4B2TMVW2.js} +6 -6
  46. package/dist/server/index.js +3719 -1002
  47. package/dist/server/migrations/0122_data_transfer_staging.sql +45 -0
  48. package/dist/server/migrations/meta/_journal.json +7 -0
  49. package/dist/setup-T3WN3HRS.js +25 -0
  50. package/dist/{ssh-client-WVLLOAYK.js → ssh-client-FZDWP5PQ.js} +3 -3
  51. package/dist/up-U73RUN5R.js +61 -0
  52. package/package.json +1 -1
  53. package/dist/bare-3POKLHTW.js +0 -29
  54. package/dist/host-channel-preflight-T2QUDQXU.js +0 -54
  55. package/dist/setup-ERGE5XNW.js +0 -25
  56. package/dist/up-EFCE6KA6.js +0 -61
@@ -9,11 +9,11 @@ import {
9
9
  prepareSourceTree,
10
10
  resolveDockerBuildArgs,
11
11
  resolveServiceDockerfile
12
- } from "./chunk-QNQFS37A.js";
12
+ } from "./chunk-S5GU345P.js";
13
13
  import {
14
14
  isArtifactPathRef,
15
15
  removeManagedArtifact
16
- } from "./chunk-MZYD3LFP.js";
16
+ } from "./chunk-AMADTKC6.js";
17
17
  import {
18
18
  BuildCancelledError,
19
19
  BuildLogger,
@@ -35,7 +35,7 @@ import {
35
35
  sq,
36
36
  staticBuilderOutputPath,
37
37
  transferLocalDirectory
38
- } from "./chunk-SYQ3FOLE.js";
38
+ } from "./chunk-IVMPA6RC.js";
39
39
  import {
40
40
  DOCKER_DIAL_STDIO_COMMAND,
41
41
  attachDialStdioDiagnostics,
@@ -44,17 +44,17 @@ import {
44
44
  execSshCommand,
45
45
  openDockerDialStdioChannel,
46
46
  openSshUnixSocket
47
- } from "./chunk-HBQWOJRP.js";
47
+ } from "./chunk-TKK5NPDQ.js";
48
48
  import {
49
49
  dirOf,
50
50
  ensureOwnedDir
51
- } from "./chunk-JWW5YAFL.js";
51
+ } from "./chunk-B2BQX6JX.js";
52
52
  import {
53
53
  OPENSHIP_LABEL
54
- } from "./chunk-CMGRMPRQ.js";
54
+ } from "./chunk-7TQP6723.js";
55
55
  import {
56
56
  isRuntimeNotFoundError
57
- } from "./chunk-N5YZMO33.js";
57
+ } from "./chunk-L4DRH6ZA.js";
58
58
  import {
59
59
  formatCpuCores,
60
60
  formatMemoryMb,
@@ -65,7 +65,7 @@ import {
65
65
  registryForImage,
66
66
  safeErrorMessage,
67
67
  withTimeout
68
- } from "./chunk-IVRTIKNZ.js";
68
+ } from "./chunk-COI4VFRW.js";
69
69
  import {
70
70
  __commonJS,
71
71
  __require,
@@ -4627,6 +4627,7 @@ var require_tar_fs = __commonJS({
4627
4627
  // ../../packages/adapters/src/runtime/docker.ts
4628
4628
  var tarFs = __toESM(require_tar_fs(), 1);
4629
4629
  import Dockerode from "dockerode";
4630
+ import { randomUUID } from "crypto";
4630
4631
  import { createGzip } from "zlib";
4631
4632
  import { PassThrough, Writable } from "stream";
4632
4633
  import { relative, sep } from "path";
@@ -5630,6 +5631,244 @@ function resolveDockerTransport(opts) {
5630
5631
  };
5631
5632
  }
5632
5633
 
5634
+ // ../../packages/adapters/src/runtime/docker-build-diagnostics.ts
5635
+ var DEFAULT_DOCKER_BUILD_IDLE_TIMEOUT_MS = 10 * 6e4;
5636
+ var MIN_DOCKER_BUILD_IDLE_TIMEOUT_MS = 6e4;
5637
+ var MAX_DOCKER_BUILD_IDLE_TIMEOUT_MS = 24 * 60 * 6e4;
5638
+ function getDockerBuildIdleTimeoutMs(raw = process.env.OPENSHIP_BUILD_IDLE_TIMEOUT_MS) {
5639
+ if (raw == null || raw.trim() === "") return DEFAULT_DOCKER_BUILD_IDLE_TIMEOUT_MS;
5640
+ const value = Number(raw);
5641
+ return Number.isSafeInteger(value) && value >= MIN_DOCKER_BUILD_IDLE_TIMEOUT_MS && value <= MAX_DOCKER_BUILD_IDLE_TIMEOUT_MS ? value : DEFAULT_DOCKER_BUILD_IDLE_TIMEOUT_MS;
5642
+ }
5643
+ function memoryGuidance(context) {
5644
+ const memoryMb = context.configuredMemoryMb;
5645
+ if (context.memoryLimitApplied && memoryMb && memoryMb > 0) {
5646
+ return `The classic Docker builder was capped at ${memoryMb} MB RAM; raise Build Memory in Project Settings \u2192 Resources if this build needs more.`;
5647
+ }
5648
+ return "This build path was not under an OpenShip-enforced memory cap; check available RAM/swap and the host or Docker daemon OOM logs.";
5649
+ }
5650
+ function exitCodeFromLine(line) {
5651
+ const match = line.match(
5652
+ /(?:returned a non-zero code|exited? with(?: exit)? code|exit code)\s*:?\s*(\d+)\b/i
5653
+ );
5654
+ return match ? Number(match[1]) : null;
5655
+ }
5656
+ function extractDockerBuildFailureHint(line, context = {}) {
5657
+ const code = exitCodeFromLine(line);
5658
+ if (code === 137) {
5659
+ return `${line} \u2014 The build process was killed by SIGKILL (exit code 137). Memory exhaustion is the most common cause, but exit 137 alone does not prove OOM. ${memoryGuidance(context)}`;
5660
+ }
5661
+ if (code === 143) {
5662
+ return `${line} \u2014 The build process received SIGTERM (exit code 143), usually because it was cancelled or stopped by another supervisor.`;
5663
+ }
5664
+ if (code !== null) return line;
5665
+ if (/JavaScript heap out of memory|Fatal process out of memory|Ineffective mark-compacts near heap limit|Reached heap limit Allocation failed|fatal error:\s*(?:runtime:\s*)?out of memory/i.test(
5666
+ line
5667
+ )) {
5668
+ return `${line} \u2014 The build process explicitly reported that it ran out of memory. ${memoryGuidance(context)}`;
5669
+ }
5670
+ if (/\bENOMEM\b|cannot allocate memory/i.test(line)) {
5671
+ return `${line} \u2014 The build process could not allocate memory (ENOMEM). ${memoryGuidance(context)}`;
5672
+ }
5673
+ return null;
5674
+ }
5675
+ function chooseDockerBuildFailureHint(current, candidate) {
5676
+ if (!candidate) return current;
5677
+ if (!current) return candidate;
5678
+ const score = (message) => {
5679
+ if (/explicitly reported that it ran out of memory|could not allocate memory/i.test(message)) {
5680
+ return 4;
5681
+ }
5682
+ if (/SIGKILL \(exit code 137\)/i.test(message)) return 3;
5683
+ if (/BuildKit|package\.json was not found/i.test(message)) return 2;
5684
+ if (/returned a non-zero code|failed to solve|executor failed running|error: build/i.test(message)) {
5685
+ return 1;
5686
+ }
5687
+ return 0;
5688
+ };
5689
+ return score(candidate) > score(current) ? candidate : current;
5690
+ }
5691
+ function dockerBuildExitMessage(code, streamedHint, context = {}) {
5692
+ if (streamedHint) return streamedHint;
5693
+ const plain = `docker build exited with code ${code}`;
5694
+ return extractDockerBuildFailureHint(plain, context) ?? plain;
5695
+ }
5696
+ function formatDuration(ms) {
5697
+ if (ms % 6e4 === 0) {
5698
+ const minutes = ms / 6e4;
5699
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
5700
+ }
5701
+ const seconds = Math.ceil(ms / 1e3);
5702
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
5703
+ }
5704
+ function dockerBuildIdleTimeoutError(timeoutMs, context = {}) {
5705
+ const limit = context.memoryLimitApplied && context.configuredMemoryMb && context.configuredMemoryMb > 0 ? ` The classic Docker builder was capped at ${context.configuredMemoryMb} MB RAM, so sustained memory pressure is one possible cause.` : "";
5706
+ return new Error(
5707
+ `Docker build produced no output for ${formatDuration(timeoutMs)} and was cancelled.${limit} Other common causes are package-registry or DNS failures, a process waiting for input, and a stalled Docker daemon.`
5708
+ );
5709
+ }
5710
+ function startDockerBuildIdleMonitor(options) {
5711
+ let stopped = false;
5712
+ let lastProgressAt = Date.now();
5713
+ let lastReportedMinute = 0;
5714
+ let deadline;
5715
+ const armDeadline = () => {
5716
+ clearTimeout(deadline);
5717
+ deadline = setTimeout(() => {
5718
+ if (stopped) return;
5719
+ stopped = true;
5720
+ clearInterval(heartbeat);
5721
+ options.onTimeout();
5722
+ }, options.timeoutMs);
5723
+ deadline.unref?.();
5724
+ };
5725
+ const heartbeat = setInterval(() => {
5726
+ if (stopped) return;
5727
+ const idleMs = Date.now() - lastProgressAt;
5728
+ const idleMinutes = Math.floor(idleMs / 6e4);
5729
+ if (idleMinutes > lastReportedMinute) {
5730
+ lastReportedMinute = idleMinutes;
5731
+ options.onIdle(idleMs);
5732
+ }
5733
+ }, 6e4);
5734
+ heartbeat.unref?.();
5735
+ armDeadline();
5736
+ return {
5737
+ progress() {
5738
+ if (stopped) return;
5739
+ lastProgressAt = Date.now();
5740
+ lastReportedMinute = 0;
5741
+ armDeadline();
5742
+ },
5743
+ stop() {
5744
+ if (stopped) return;
5745
+ stopped = true;
5746
+ clearTimeout(deadline);
5747
+ clearInterval(heartbeat);
5748
+ }
5749
+ };
5750
+ }
5751
+
5752
+ // ../../packages/adapters/src/runtime/docker-build-container-tracker.ts
5753
+ function normalizedImageId(value) {
5754
+ return (value ?? "").replace(/^sha256:/, "").toLowerCase();
5755
+ }
5756
+ function commandMatches(instruction, actual) {
5757
+ const run = instruction.match(/^RUN\s+([\s\S]+)$/i)?.[1]?.trim();
5758
+ if (!run || !actual?.length) return false;
5759
+ if (run.startsWith("[")) {
5760
+ try {
5761
+ const parsed = JSON.parse(run);
5762
+ return Array.isArray(parsed) && parsed.every((part) => typeof part === "string") && parsed.length === actual.length && parsed.every((part, index) => part === actual[index]);
5763
+ } catch {
5764
+ return false;
5765
+ }
5766
+ }
5767
+ return actual[actual.length - 1] === run;
5768
+ }
5769
+ var LegacyBuildContainerTracker = class {
5770
+ constructor(client, expectedMemoryBytes, ownershipHost) {
5771
+ this.client = client;
5772
+ this.expectedMemoryBytes = expectedMemoryBytes;
5773
+ this.ownershipHost = ownershipHost;
5774
+ }
5775
+ parentImageId = null;
5776
+ pendingStep = null;
5777
+ active = null;
5778
+ expectedStepTotal = null;
5779
+ lastStepNumber = 0;
5780
+ chunkBuffer = "";
5781
+ queue = Promise.resolve();
5782
+ termination = null;
5783
+ /** Consume one complete dockerode `event.stream` field. */
5784
+ observe(stream) {
5785
+ if (!stream) return;
5786
+ for (const rawLine of stream.split(/\r?\n/)) {
5787
+ this.enqueueLine(rawLine);
5788
+ }
5789
+ }
5790
+ /** Consume an arbitrary SSH stdout/stderr chunk, retaining a partial line. */
5791
+ observeChunk(chunk) {
5792
+ if (!chunk) return;
5793
+ const parts = `${this.chunkBuffer}${chunk}`.split(/\r\n|\n|\r/);
5794
+ this.chunkBuffer = parts.pop() ?? "";
5795
+ if (this.chunkBuffer.length > 8192) this.chunkBuffer = this.chunkBuffer.slice(-8192);
5796
+ for (const line of parts) this.enqueueLine(line);
5797
+ }
5798
+ flush() {
5799
+ this.enqueueLine(this.chunkBuffer);
5800
+ this.chunkBuffer = "";
5801
+ }
5802
+ enqueueLine(rawLine) {
5803
+ const line = rawLine.trim();
5804
+ if (!line) return;
5805
+ this.queue = this.queue.then(
5806
+ () => this.observeLine(line),
5807
+ () => this.observeLine(line)
5808
+ );
5809
+ }
5810
+ async observeLine(line) {
5811
+ if (this.active) {
5812
+ const removed = line.match(/^---> Removed intermediate container\s+([a-f0-9]{12,64})$/i);
5813
+ if (removed?.[1]?.toLowerCase() === this.active.id) {
5814
+ try {
5815
+ const info = await this.client.inspect(this.active.id);
5816
+ if (!info?.State?.Running) this.active = null;
5817
+ } catch {
5818
+ }
5819
+ }
5820
+ return;
5821
+ }
5822
+ const layer = line.match(/^--->\s+([a-f0-9]{12,64})$/i);
5823
+ if (layer?.[1]) {
5824
+ this.parentImageId = layer[1].toLowerCase();
5825
+ return;
5826
+ }
5827
+ const step = line.match(/^Step\s+(\d+)\/(\d+)\s*:\s*([\s\S]+)$/i);
5828
+ if (step?.[1] && step[2] && step[3]) {
5829
+ const number = Number(step[1]);
5830
+ const total = Number(step[2]);
5831
+ const firstStep = this.lastStepNumber === 0 && number === 1;
5832
+ const nextStep = number === this.lastStepNumber + 1;
5833
+ const sameTotal = this.expectedStepTotal === null || total === this.expectedStepTotal;
5834
+ if (number >= 1 && total >= number && sameTotal && (firstStep || nextStep)) {
5835
+ this.expectedStepTotal ??= total;
5836
+ this.lastStepNumber = number;
5837
+ this.pendingStep = {
5838
+ number,
5839
+ total,
5840
+ instruction: step[3].trim(),
5841
+ parentImageId: this.parentImageId
5842
+ };
5843
+ }
5844
+ return;
5845
+ }
5846
+ const running = line.match(/^---> Running in\s+([a-f0-9]{12,64})$/i);
5847
+ if (running?.[1] && this.pendingStep && /^RUN\s+/i.test(this.pendingStep.instruction)) {
5848
+ this.active = { ...this.pendingStep, id: running[1].toLowerCase() };
5849
+ this.pendingStep = null;
5850
+ }
5851
+ }
5852
+ async terminate() {
5853
+ await this.queue;
5854
+ const candidate = this.active;
5855
+ if (!candidate) return { status: "none" };
5856
+ const info = await this.client.inspect(candidate.id);
5857
+ if (!info?.State?.Running) return { status: "not-running", id: candidate.id };
5858
+ const actualId = info.Id?.toLowerCase() ?? "";
5859
+ const actualImage = normalizedImageId(info.Config?.Image);
5860
+ const expectedMemory = this.expectedMemoryBytes ?? 0;
5861
+ const ownershipEntry = `${this.ownershipHost}:127.0.0.1`;
5862
+ const verified = actualId.startsWith(candidate.id) && candidate.parentImageId !== null && actualImage.startsWith(candidate.parentImageId) && (info.HostConfig?.Memory ?? 0) === expectedMemory && info.HostConfig?.ExtraHosts?.includes(ownershipEntry) === true && commandMatches(candidate.instruction, info.Config?.Cmd);
5863
+ if (!verified) return { status: "unverified", id: candidate.id };
5864
+ await this.client.kill(candidate.id);
5865
+ return { status: "killed", id: candidate.id };
5866
+ }
5867
+ terminateActive() {
5868
+ return this.termination ??= this.terminate();
5869
+ }
5870
+ };
5871
+
5633
5872
  // ../../packages/adapters/src/runtime/docker.ts
5634
5873
  var isDockerNotFoundError = isRuntimeNotFoundError;
5635
5874
  function ownsBuiltImage(imageRef) {
@@ -5647,7 +5886,42 @@ var RESTART_POLICIES = {
5647
5886
  "unless-stopped": { Name: "unless-stopped", MaximumRetryCount: 0 },
5648
5887
  no: { Name: "no", MaximumRetryCount: 0 }
5649
5888
  };
5650
- var DOCKER_BUILD_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
5889
+ var BUILD_CONTAINER_CLEANUP_TIMEOUT_MS = 5e3;
5890
+ var BUILD_CONTAINER_IDENTIFICATION_GRACE_MS = 500;
5891
+ function newBuildOwnershipHost() {
5892
+ return `openship-build-${randomUUID()}.invalid`;
5893
+ }
5894
+ async function boundedBuildContainerCleanup(cleanup) {
5895
+ let timer = null;
5896
+ const guarded = cleanup.catch(() => null);
5897
+ const deadline = new Promise((resolve) => {
5898
+ timer = setTimeout(() => resolve(null), BUILD_CONTAINER_CLEANUP_TIMEOUT_MS);
5899
+ timer.unref?.();
5900
+ });
5901
+ try {
5902
+ return await Promise.race([guarded, deadline]);
5903
+ } finally {
5904
+ if (timer) clearTimeout(timer);
5905
+ }
5906
+ }
5907
+ async function stopTrackedBuildContainer(tracker, log, reason) {
5908
+ if (!tracker) return;
5909
+ tracker.flush();
5910
+ const cleanup = await boundedBuildContainerCleanup(tracker.terminateActive());
5911
+ if (cleanup?.status === "killed") {
5912
+ log.log(`Stopped ${reason} Docker build container ${cleanup.id.slice(0, 12)}.`);
5913
+ } else if (cleanup?.status === "unverified") {
5914
+ log.log(
5915
+ `${reason === "timed-out" ? "Timed-out" : "Interrupted"} build container ${cleanup.id.slice(0, 12)} did not match this build's verified step; it was left untouched.`,
5916
+ "warn"
5917
+ );
5918
+ } else if (cleanup === null) {
5919
+ log.log(
5920
+ `${reason === "timed-out" ? "Timed-out" : "Interrupted"} Docker build cleanup could not be confirmed within 5 seconds; Docker may still be releasing the build container.`,
5921
+ "warn"
5922
+ );
5923
+ }
5924
+ }
5651
5925
  var activeDockerBuilds = /* @__PURE__ */ new Map();
5652
5926
  var pendingDockerBuildCancellations = /* @__PURE__ */ new Map();
5653
5927
  var PENDING_CANCEL_TTL_MS = 10 * 60 * 1e3;
@@ -5817,21 +6091,34 @@ function parsePortBindings(portSpecs) {
5817
6091
  }
5818
6092
  function packBuildContext(contextDir, entries, cancelSignal, ignoreContextPath) {
5819
6093
  const controller = new AbortController();
6094
+ const abort = (reason) => {
6095
+ if (!controller.signal.aborted) controller.abort(reason);
6096
+ };
5820
6097
  const ignore = ignoreContextPath ? (absolutePath) => ignoreContextPath(relative(contextDir, absolutePath).split(sep).join("/")) : void 0;
5821
6098
  const pack2 = tarFs.pack(contextDir, { entries, ignore });
5822
6099
  const body = pack2.pipe(createGzip());
5823
6100
  let contextError = null;
5824
6101
  const capture = (err) => {
5825
6102
  contextError ??= err instanceof Error ? err : new Error(String(err));
5826
- if (!controller.signal.aborted) controller.abort();
6103
+ abort(contextError);
5827
6104
  };
6105
+ const onCancel = () => abort(cancelSignal?.reason);
5828
6106
  if (cancelSignal) {
5829
- if (cancelSignal.aborted) controller.abort();
5830
- else cancelSignal.addEventListener("abort", () => controller.abort(), { once: true });
6107
+ if (cancelSignal.aborted) abort(cancelSignal.reason);
6108
+ else cancelSignal.addEventListener("abort", onCancel, { once: true });
5831
6109
  }
5832
6110
  pack2.once("error", capture);
5833
6111
  body.once("error", capture);
5834
- return { body, abortSignal: controller.signal, takeError: () => contextError };
6112
+ return {
6113
+ body,
6114
+ abortSignal: controller.signal,
6115
+ abort,
6116
+ // Once buildImage has returned the response stream, streamDockerodeBuild
6117
+ // owns cancellation. It briefly keeps that stream alive to receive Docker's
6118
+ // exact intermediate-container id before aborting the request.
6119
+ detachCancellation: () => cancelSignal?.removeEventListener("abort", onCancel),
6120
+ takeError: () => contextError
6121
+ };
5835
6122
  }
5836
6123
  var DURATION_UNITS_NS = {
5837
6124
  ns: 1,
@@ -6179,17 +6466,20 @@ var DockerRuntime = class _DockerRuntime {
6179
6466
  logger.log(`[${step}] ${label} ${message}`);
6180
6467
  logger.step(step, status, message);
6181
6468
  }
6182
- handleBuildEvent(event, logger, trace) {
6469
+ handleBuildEvent(event, logger, trace, diagnosticContext = {}) {
6183
6470
  const errorMessage = event.errorDetail?.message ?? event.error;
6184
6471
  if (errorMessage) {
6185
6472
  logger.log(errorMessage, "error");
6186
- return errorMessage;
6473
+ return this.extractBuildFailureHint(errorMessage, diagnosticContext) ?? errorMessage;
6187
6474
  }
6188
6475
  if (event.id === "moby.buildkit.trace" && typeof event.aux === "string") {
6189
6476
  let hint = null;
6190
6477
  for (const line of trace?.push(event.aux) ?? []) {
6191
6478
  logger.log(line.message, line.level === "error" ? "error" : parseLogLevel(line.message));
6192
- hint ??= this.extractBuildFailureHint(line.message);
6479
+ hint = chooseDockerBuildFailureHint(
6480
+ hint,
6481
+ this.extractBuildFailureHint(line.message, diagnosticContext)
6482
+ );
6193
6483
  }
6194
6484
  return hint;
6195
6485
  }
@@ -6216,7 +6506,7 @@ var DockerRuntime = class _DockerRuntime {
6216
6506
  return null;
6217
6507
  }
6218
6508
  logger.log(line, parseLogLevel(line));
6219
- return this.extractBuildFailureHint(line);
6509
+ return this.extractBuildFailureHint(line, diagnosticContext);
6220
6510
  }
6221
6511
  if (event.status) {
6222
6512
  const parts = [event.id, event.status, event.progress].filter((p) => Boolean(p?.trim())).map((p) => p.trim());
@@ -6245,10 +6535,9 @@ var DockerRuntime = class _DockerRuntime {
6245
6535
  isLowSignalDockerLine(line) {
6246
6536
  return _DockerRuntime.DOCKER_BUILDER_NOISE.some((p) => p.test(line));
6247
6537
  }
6248
- extractBuildFailureHint(line) {
6249
- if (/returned a non-zero code:\s*\d+/i.test(line)) {
6250
- return line;
6251
- }
6538
+ extractBuildFailureHint(line, diagnosticContext = {}) {
6539
+ const processFailure = extractDockerBuildFailureHint(line, diagnosticContext);
6540
+ if (processFailure) return processFailure;
6252
6541
  if (/requires? BuildKit|BuildKit is enabled but the buildx component/i.test(line)) {
6253
6542
  return `${line} \u2014 this Dockerfile needs BuildKit. Install the docker buildx plugin on the build host (\`docker-buildx-plugin\`), or remove the BuildKit-only instruction.`;
6254
6543
  }
@@ -6345,7 +6634,8 @@ var DockerRuntime = class _DockerRuntime {
6345
6634
  const dockerfileFlag = dockerfileName && dockerfileName !== "Dockerfile" ? ` -f ${sq(dockerfileName)}` : "";
6346
6635
  const buildKit = await this.remoteBuildKitAvailable();
6347
6636
  const builderEnv = buildKit ? "DOCKER_BUILDKIT=1 " : "";
6348
- const builderFlags = buildKit ? " --progress=plain" : " --force-rm";
6637
+ const ownershipHost = buildKit ? null : newBuildOwnershipHost();
6638
+ const builderFlags = buildKit ? " --progress=plain" : ` --force-rm --add-host ${sq(`${ownershipHost}:127.0.0.1`)}`;
6349
6639
  if (buildKit) {
6350
6640
  log.log("Builder: BuildKit (docker buildx detected on the host)");
6351
6641
  } else if (opts?.requiresBuildKit) {
@@ -6369,16 +6659,79 @@ var DockerRuntime = class _DockerRuntime {
6369
6659
  "running",
6370
6660
  "Running install inside container (docker build)"
6371
6661
  );
6372
- const { code } = await executor.streamExec(
6373
- buildCmd,
6374
- (entry) => {
6375
- log.log(entry.message, parseLogLevel(entry.message));
6662
+ const diagnosticContext = {
6663
+ configuredMemoryMb: config.resources.memoryMb,
6664
+ memoryLimitApplied: false
6665
+ };
6666
+ const buildContainerTracker = buildKit ? null : this.remoteLegacyBuildContainerTracker(executor, ownershipHost);
6667
+ const commandAbort = new AbortController();
6668
+ const forwardCancellation = () => {
6669
+ buildContainerTracker?.flush();
6670
+ if (buildContainerTracker) void buildContainerTracker.terminateActive();
6671
+ commandAbort.abort(signal?.reason);
6672
+ };
6673
+ if (signal?.aborted) forwardCancellation();
6674
+ else signal?.addEventListener("abort", forwardCancellation, { once: true });
6675
+ let timeoutError = null;
6676
+ let streamedFailureHint = null;
6677
+ const timeoutMs = getDockerBuildIdleTimeoutMs();
6678
+ const idleMonitor = startDockerBuildIdleMonitor({
6679
+ timeoutMs,
6680
+ onIdle: (idleMs) => {
6681
+ log.log(`Still building... (no output for ${Math.floor(idleMs / 6e4)}m)`);
6376
6682
  },
6377
- { signal }
6378
- );
6683
+ onTimeout: () => {
6684
+ timeoutError = dockerBuildIdleTimeoutError(timeoutMs, diagnosticContext);
6685
+ buildContainerTracker?.flush();
6686
+ if (buildContainerTracker) void buildContainerTracker.terminateActive();
6687
+ commandAbort.abort(timeoutError);
6688
+ }
6689
+ });
6690
+ let code = null;
6691
+ let execError = null;
6692
+ try {
6693
+ const result = await executor.streamExec(
6694
+ buildCmd,
6695
+ (entry) => {
6696
+ idleMonitor.progress();
6697
+ buildContainerTracker?.observeChunk(entry.message);
6698
+ streamedFailureHint = chooseDockerBuildFailureHint(
6699
+ streamedFailureHint,
6700
+ this.extractBuildFailureHint(entry.message, diagnosticContext)
6701
+ );
6702
+ log.log(entry.message, parseLogLevel(entry.message));
6703
+ },
6704
+ { signal: commandAbort.signal }
6705
+ );
6706
+ code = result.code;
6707
+ } catch (error) {
6708
+ execError = error;
6709
+ } finally {
6710
+ idleMonitor.stop();
6711
+ buildContainerTracker?.flush();
6712
+ signal?.removeEventListener("abort", forwardCancellation);
6713
+ }
6379
6714
  log.log("\u2500\u2500\u2500 end docker build output \u2500\u2500\u2500");
6380
- if (signal?.aborted) throw new BuildCancelledError();
6381
- if (code !== 0) throw new Error(`docker build exited with code ${code}`);
6715
+ if (timeoutError) {
6716
+ await stopTrackedBuildContainer(buildContainerTracker, log, "timed-out");
6717
+ throw timeoutError;
6718
+ }
6719
+ if (signal?.aborted) {
6720
+ await stopTrackedBuildContainer(buildContainerTracker, log, "interrupted");
6721
+ throw new BuildCancelledError();
6722
+ }
6723
+ if (execError) {
6724
+ await stopTrackedBuildContainer(buildContainerTracker, log, "interrupted");
6725
+ throw execError;
6726
+ }
6727
+ if (code === null) {
6728
+ await stopTrackedBuildContainer(buildContainerTracker, log, "interrupted");
6729
+ throw new Error("docker build ended without an exit code");
6730
+ }
6731
+ if (code !== 0) {
6732
+ await stopTrackedBuildContainer(buildContainerTracker, log, "interrupted");
6733
+ throw new Error(dockerBuildExitMessage(code, streamedFailureHint, diagnosticContext));
6734
+ }
6382
6735
  this.emitDockerStep(log, "install", "completed", "Image build finished");
6383
6736
  }
6384
6737
  /**
@@ -6607,6 +6960,55 @@ var DockerRuntime = class _DockerRuntime {
6607
6960
  }
6608
6961
  return { options: { version: "2" }, trace: new BuildKitTraceDecoder() };
6609
6962
  }
6963
+ /**
6964
+ * Bind legacy-builder cleanup to the exact container id Docker announces in
6965
+ * this build's own progress stream. A 404 is an ordinary "already cleaned"
6966
+ * result; every other inspection failure remains inconclusive and therefore
6967
+ * can never authorize a kill.
6968
+ */
6969
+ legacyBuildContainerTracker(expectedMemoryBytes, ownershipHost) {
6970
+ return new LegacyBuildContainerTracker(
6971
+ {
6972
+ inspect: async (id) => {
6973
+ try {
6974
+ return await this.docker.getContainer(id).inspect();
6975
+ } catch (error) {
6976
+ if (isDockerNotFoundError(error)) return null;
6977
+ throw error;
6978
+ }
6979
+ },
6980
+ kill: async (id) => {
6981
+ await this.docker.getContainer(id).kill({ signal: "SIGKILL" });
6982
+ }
6983
+ },
6984
+ expectedMemoryBytes,
6985
+ ownershipHost
6986
+ );
6987
+ }
6988
+ /** Same proof model as the Engine-API tracker, using the already-live SSH executor. */
6989
+ remoteLegacyBuildContainerTracker(executor, ownershipHost) {
6990
+ return new LegacyBuildContainerTracker(
6991
+ {
6992
+ inspect: async (id) => {
6993
+ try {
6994
+ const output = await executor.exec(`docker container inspect ${sq(id)}`);
6995
+ const parsed = JSON.parse(output);
6996
+ return Array.isArray(parsed) && parsed[0] && typeof parsed[0] === "object" ? parsed[0] : null;
6997
+ } catch (error) {
6998
+ if (/No such (?:object|container)|not found/i.test(safeErrorMessage(error)))
6999
+ return null;
7000
+ throw error;
7001
+ }
7002
+ },
7003
+ kill: async (id) => {
7004
+ await executor.exec(`docker container kill --signal KILL ${sq(id)} >/dev/null`);
7005
+ }
7006
+ },
7007
+ // The native CLI path intentionally applies no OpenShip memory flag.
7008
+ void 0,
7009
+ ownershipHost
7010
+ );
7011
+ }
6610
7012
  /**
6611
7013
  * Dockerode build path. Used for local socket and TCP transports, plus
6612
7014
  * SSH transports that didn't get an executor wired in (shouldn't happen
@@ -6618,13 +7020,21 @@ var DockerRuntime = class _DockerRuntime {
6618
7020
  */
6619
7021
  async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
6620
7022
  log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
6621
- const { body, abortSignal, takeError } = packBuildContext(
7023
+ const { body, abortSignal, abort, detachCancellation, takeError } = packBuildContext(
6622
7024
  buildContext.buildContextDir,
6623
7025
  buildContext.contextEntries,
6624
7026
  cancelSignal,
6625
7027
  buildContext.ignoreContextPath
6626
7028
  );
6627
7029
  const builder = this.selectDockerodeBuilder(config, buildContext.requiresBuildKit, log);
7030
+ const buildLimits = dockerBuildResourceLimits(config.resources);
7031
+ const ownershipHost = builder.options.version !== "2" ? newBuildOwnershipHost() : null;
7032
+ const diagnosticContext = {
7033
+ configuredMemoryMb: config.resources.memoryMb,
7034
+ // Engine API build limits work only with the classic builder. The
7035
+ // BuildKit selection above already logs that its build runs uncapped.
7036
+ memoryLimitApplied: builder.options.version !== "2" && buildLimits.memory !== void 0
7037
+ };
6628
7038
  try {
6629
7039
  const stream = await this.docker.buildImage(body, {
6630
7040
  t: tag,
@@ -6634,13 +7044,23 @@ var DockerRuntime = class _DockerRuntime {
6634
7044
  // Omitted entirely unless the project set a build cap — a self-hosted
6635
7045
  // build should be free to use the machine (a production build often
6636
7046
  // needs several GB). Opt-in only; see dockerBuildResourceLimits.
6637
- ...dockerBuildResourceLimits(config.resources),
7047
+ ...buildLimits,
7048
+ ...ownershipHost && { extrahosts: `${ownershipHost}:127.0.0.1` },
6638
7049
  forcerm: true,
6639
7050
  ...builder.options,
6640
7051
  abortSignal
6641
7052
  });
7053
+ detachCancellation();
6642
7054
  log.log("Connected to Docker daemon. Build output follows:");
6643
- await this.streamDockerodeBuild(stream, log, builder.trace);
7055
+ await this.streamDockerodeBuild(stream, log, {
7056
+ trace: builder.trace,
7057
+ diagnosticContext,
7058
+ abortBuild: abort,
7059
+ cancelSignal,
7060
+ ...ownershipHost && {
7061
+ legacyBuilder: { expectedMemoryBytes: buildLimits.memory, ownershipHost }
7062
+ }
7063
+ });
6644
7064
  log.log("Docker daemon finished streaming build output. Finalizing image...\n");
6645
7065
  } catch (err) {
6646
7066
  const contextErr = takeError();
@@ -6648,6 +7068,7 @@ var DockerRuntime = class _DockerRuntime {
6648
7068
  cause: contextErr
6649
7069
  }) : err;
6650
7070
  } finally {
7071
+ detachCancellation();
6651
7072
  await buildContext.cleanup();
6652
7073
  }
6653
7074
  }
@@ -6656,69 +7077,107 @@ var DockerRuntime = class _DockerRuntime {
6656
7077
  * idle timeout + keepalive, and throw on a fatal build event. Shared by the
6657
7078
  * single-image dockerode path and the local batch build (buildImages).
6658
7079
  */
6659
- async streamDockerodeBuild(stream, log, trace) {
7080
+ async streamDockerodeBuild(stream, log, options = {}) {
6660
7081
  let fatalBuildError = null;
6661
- await new Promise((resolve, reject) => {
6662
- let settled = false;
6663
- let idleTimer = null;
6664
- let keepaliveTimer = null;
6665
- let idleMinutes = 0;
6666
- const clearTimers = () => {
6667
- if (idleTimer) {
6668
- clearTimeout(idleTimer);
6669
- idleTimer = null;
6670
- }
6671
- if (keepaliveTimer) {
6672
- clearInterval(keepaliveTimer);
6673
- keepaliveTimer = null;
6674
- }
6675
- idleMinutes = 0;
6676
- };
6677
- const fail = (error) => {
6678
- if (settled) return;
6679
- settled = true;
6680
- clearTimers();
6681
- stream.destroy?.(error);
6682
- reject(error);
6683
- };
6684
- const succeed = () => {
6685
- if (settled) return;
6686
- settled = true;
6687
- clearTimers();
6688
- resolve();
6689
- };
6690
- const resetIdleTimer = () => {
6691
- clearTimers();
6692
- keepaliveTimer = setInterval(() => {
6693
- idleMinutes += 1;
6694
- log.log(`Still building... (no output for ${idleMinutes}m)`);
6695
- }, 6e4);
6696
- if (keepaliveTimer.unref) keepaliveTimer.unref();
6697
- idleTimer = setTimeout(() => {
6698
- fail(
6699
- new Error(
6700
- "Docker build produced no output for 30 minutes. This usually means the remote server cannot reach the package registry, has broken DNS, or the Docker daemon stalled during the build."
6701
- )
7082
+ const timeoutMs = getDockerBuildIdleTimeoutMs();
7083
+ const diagnosticContext = options.diagnosticContext ?? {};
7084
+ const buildContainerTracker = options.legacyBuilder ? this.legacyBuildContainerTracker(
7085
+ options.legacyBuilder.expectedMemoryBytes,
7086
+ options.legacyBuilder.ownershipHost
7087
+ ) : null;
7088
+ let inactivityTimedOut = false;
7089
+ try {
7090
+ await new Promise((resolve, reject) => {
7091
+ let settled = false;
7092
+ let idleMonitor = null;
7093
+ let cancellationTimer = null;
7094
+ const clearTimers = () => {
7095
+ idleMonitor?.stop();
7096
+ if (cancellationTimer) clearTimeout(cancellationTimer);
7097
+ options.cancelSignal?.removeEventListener("abort", requestCancellation);
7098
+ };
7099
+ const fail = (error) => {
7100
+ if (settled) return;
7101
+ settled = true;
7102
+ clearTimers();
7103
+ stream.destroy?.(error);
7104
+ reject(error);
7105
+ };
7106
+ const abortCancelledBuild = () => {
7107
+ if (settled) return;
7108
+ buildContainerTracker?.flush();
7109
+ if (buildContainerTracker) void buildContainerTracker.terminateActive();
7110
+ const error = new BuildCancelledError();
7111
+ fail(error);
7112
+ options.abortBuild?.(error);
7113
+ };
7114
+ const requestCancellation = () => {
7115
+ if (settled || cancellationTimer) return;
7116
+ idleMonitor?.stop();
7117
+ cancellationTimer = setTimeout(
7118
+ abortCancelledBuild,
7119
+ buildContainerTracker ? BUILD_CONTAINER_IDENTIFICATION_GRACE_MS : 0
6702
7120
  );
6703
- }, DOCKER_BUILD_IDLE_TIMEOUT_MS);
6704
- if (idleTimer.unref) idleTimer.unref();
6705
- };
6706
- resetIdleTimer();
6707
- this.docker.modem.followProgress(
6708
- stream,
6709
- (err) => {
6710
- if (err) {
6711
- fail(err);
7121
+ cancellationTimer.unref?.();
7122
+ };
7123
+ const succeed = () => {
7124
+ if (settled) return;
7125
+ if (options.cancelSignal?.aborted) {
7126
+ abortCancelledBuild();
6712
7127
  return;
6713
7128
  }
6714
- succeed();
6715
- },
6716
- (event) => {
6717
- resetIdleTimer();
6718
- fatalBuildError ??= this.handleBuildEvent(event, log, trace);
6719
- }
7129
+ settled = true;
7130
+ clearTimers();
7131
+ resolve();
7132
+ };
7133
+ idleMonitor = startDockerBuildIdleMonitor({
7134
+ timeoutMs,
7135
+ onIdle: (idleMs) => {
7136
+ log.log(`Still building... (no output for ${Math.floor(idleMs / 6e4)}m)`);
7137
+ },
7138
+ onTimeout: () => {
7139
+ const error = dockerBuildIdleTimeoutError(timeoutMs, diagnosticContext);
7140
+ inactivityTimedOut = true;
7141
+ if (buildContainerTracker) {
7142
+ void buildContainerTracker.terminateActive();
7143
+ }
7144
+ fail(error);
7145
+ options.abortBuild?.(error);
7146
+ }
7147
+ });
7148
+ if (options.cancelSignal?.aborted) requestCancellation();
7149
+ else options.cancelSignal?.addEventListener("abort", requestCancellation, { once: true });
7150
+ this.docker.modem.followProgress(
7151
+ stream,
7152
+ (err) => {
7153
+ if (err) {
7154
+ fail(err);
7155
+ return;
7156
+ }
7157
+ succeed();
7158
+ },
7159
+ (event) => {
7160
+ idleMonitor?.progress();
7161
+ buildContainerTracker?.observe(event.stream);
7162
+ fatalBuildError = chooseDockerBuildFailureHint(
7163
+ fatalBuildError,
7164
+ this.handleBuildEvent(event, log, options.trace, diagnosticContext)
7165
+ );
7166
+ }
7167
+ );
7168
+ });
7169
+ } catch (error) {
7170
+ await stopTrackedBuildContainer(
7171
+ buildContainerTracker,
7172
+ log,
7173
+ inactivityTimedOut ? "timed-out" : "interrupted"
6720
7174
  );
6721
- });
7175
+ throw error;
7176
+ }
7177
+ if (options.cancelSignal?.aborted) {
7178
+ await stopTrackedBuildContainer(buildContainerTracker, log, "interrupted");
7179
+ throw new BuildCancelledError();
7180
+ }
6722
7181
  if (fatalBuildError) {
6723
7182
  throw new Error(fatalBuildError);
6724
7183
  }
@@ -7445,13 +7904,19 @@ var DockerRuntime = class _DockerRuntime {
7445
7904
  { signal, requiresBuildKit }
7446
7905
  );
7447
7906
  } else {
7448
- const { body, abortSignal, takeError } = packBuildContext(
7907
+ const { body, abortSignal, abort, detachCancellation, takeError } = packBuildContext(
7449
7908
  buildContextDir ?? tree.contextDir,
7450
7909
  contextEntries ?? [],
7451
7910
  signal,
7452
7911
  ignoreContextPath
7453
7912
  );
7454
7913
  const builder = this.selectDockerodeBuilder(spec.config, requiresBuildKit, spec.logger);
7914
+ const buildLimits = dockerBuildResourceLimits(spec.config.resources);
7915
+ const ownershipHost = builder.options.version !== "2" ? newBuildOwnershipHost() : null;
7916
+ const diagnosticContext = {
7917
+ configuredMemoryMb: spec.config.resources.memoryMb,
7918
+ memoryLimitApplied: builder.options.version !== "2" && buildLimits.memory !== void 0
7919
+ };
7455
7920
  try {
7456
7921
  const stream = await this.docker.buildImage(body, {
7457
7922
  t: tag,
@@ -7461,18 +7926,30 @@ var DockerRuntime = class _DockerRuntime {
7461
7926
  sessionId: spec.config.sessionId
7462
7927
  }),
7463
7928
  buildargs: resolveDockerBuildArgs(spec.config),
7464
- ...dockerBuildResourceLimits(spec.config.resources),
7929
+ ...buildLimits,
7930
+ ...ownershipHost && { extrahosts: `${ownershipHost}:127.0.0.1` },
7465
7931
  forcerm: true,
7466
7932
  ...builder.options,
7467
7933
  abortSignal
7468
7934
  });
7469
- await this.streamDockerodeBuild(stream, spec.logger, builder.trace);
7935
+ detachCancellation();
7936
+ await this.streamDockerodeBuild(stream, spec.logger, {
7937
+ trace: builder.trace,
7938
+ diagnosticContext,
7939
+ abortBuild: abort,
7940
+ cancelSignal: signal,
7941
+ ...ownershipHost && {
7942
+ legacyBuilder: { expectedMemoryBytes: buildLimits.memory, ownershipHost }
7943
+ }
7944
+ });
7470
7945
  } catch (err) {
7471
7946
  const contextErr = takeError();
7472
7947
  throw contextErr ? new Error(
7473
7948
  `Failed to read Docker build context: ${safeErrorMessage(contextErr)}`,
7474
7949
  { cause: contextErr }
7475
7950
  ) : err;
7951
+ } finally {
7952
+ detachCancellation();
7476
7953
  }
7477
7954
  }
7478
7955
  if (isCancelled(spec.config.sessionId)) {
@@ -7792,6 +8269,56 @@ var DockerRuntime = class _DockerRuntime {
7792
8269
  } catch {
7793
8270
  }
7794
8271
  }
8272
+ /**
8273
+ * Reclaim BuildKit/classic-builder cache through Docker Engine's native API.
8274
+ *
8275
+ * Build cache belongs to the DAEMON, not to a project: intermediate records
8276
+ * do not reliably retain OpenShip's final-image labels. Callers therefore own
8277
+ * the host-wide policy (bounded automatic retention vs an explicit full
8278
+ * clear). Docker itself excludes records leased by an active build.
8279
+ *
8280
+ * dockerode 4.0.9's `pruneBuilder(opts)` silently discards `opts`, so use its
8281
+ * modem directly until upstream forwards the query parameters.
8282
+ */
8283
+ async pruneBuildCache(options = {}) {
8284
+ const keepStorageBytes = options.keepStorageBytes;
8285
+ if (keepStorageBytes !== void 0 && (!Number.isSafeInteger(keepStorageBytes) || keepStorageBytes < 0)) {
8286
+ throw new RangeError("keepStorageBytes must be a non-negative safe integer");
8287
+ }
8288
+ const query = {
8289
+ all: options.all ?? false
8290
+ };
8291
+ if (keepStorageBytes !== void 0) query["keep-storage"] = keepStorageBytes;
8292
+ if (options.filters && Object.keys(options.filters).length > 0) {
8293
+ query.filters = options.filters;
8294
+ }
8295
+ const raw = await new Promise((resolve, reject) => {
8296
+ this.docker.modem.dial(
8297
+ {
8298
+ path: "/build/prune?",
8299
+ method: "POST",
8300
+ // `_body: {}` prevents docker-modem from serializing the query object
8301
+ // into an irrelevant POST body while preserving its normal query
8302
+ // encoding (including JSON-encoded filter maps).
8303
+ options: { _query: query, _body: {} },
8304
+ abortSignal: options.abortSignal,
8305
+ statusCodes: {
8306
+ 200: true,
8307
+ 500: "server error"
8308
+ }
8309
+ },
8310
+ (err, data) => {
8311
+ if (err) reject(err);
8312
+ else resolve(data);
8313
+ }
8314
+ );
8315
+ });
8316
+ const response = raw ?? {};
8317
+ return {
8318
+ cachesDeleted: Array.isArray(response.CachesDeleted) ? response.CachesDeleted.filter((id) => typeof id === "string") : [],
8319
+ spaceReclaimed: typeof response.SpaceReclaimed === "number" && Number.isFinite(response.SpaceReclaimed) && response.SpaceReclaimed >= 0 ? response.SpaceReclaimed : 0
8320
+ };
8321
+ }
7795
8322
  /**
7796
8323
  * Containers labeled for this deployment, with live state — the reconcile
7797
8324
  * read-back. `State` is dockerode's `running | exited | paused | ...`; map it