@mastra/docker 0.9.0-alpha.0 → 0.9.0-alpha.2

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 CHANGED
@@ -91,6 +91,11 @@ const b = new DockerSandbox({ template });
91
91
  const result = await template.build();
92
92
  if (result.status !== 'ready') throw new Error(result.error);
93
93
 
94
+ // Cancel repository resolution or template preparation when needed.
95
+ const controller = new AbortController();
96
+ await a.start({ abortSignal: controller.signal });
97
+ // Explicit builds accept the same option: template.build({ abortSignal: controller.signal }).
98
+
94
99
  // Remove the built image when done (independent of any sandbox's destroy()).
95
100
  await template.dispose();
96
101
  ```
@@ -105,6 +110,9 @@ template, and their signatures match the E2B and platform template builders.
105
110
  The image tag is content-addressed (`mastra-template:<hash>`), so `build()` is
106
111
  idempotent and reuses an existing image unless you pass `{ force: true }`,
107
112
  which also bypasses the daemon's layer cache so every step really re-runs.
113
+ Concurrent callers share preparation work, but each caller can cancel its own
114
+ wait independently; the underlying build is cancelled only after its last
115
+ active caller aborts.
108
116
 
109
117
  Never put secrets in `setEnvs` — they are baked into the image. For a step that
110
118
  needs a credential, use `runWithSecrets`: the command runs in a throwaway build
@@ -0,0 +1,7 @@
1
+ import { SandboxAbortError } from '@mastra/core/workspace';
2
+ export declare function createAbortError(signal: AbortSignal, operation: string): SandboxAbortError;
3
+ export declare function throwIfAborted(signal: AbortSignal | undefined, operation: string): void;
4
+ export declare function isAbortError(error: unknown): boolean;
5
+ export declare function normalizeAbortError(error: unknown, operation: string): unknown;
6
+ export declare function waitForAbortable<T>(promise: Promise<T>, signal: AbortSignal | undefined, operation: string, onAbort?: () => void): Promise<T>;
7
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAE1F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKpD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAM9E;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,IAAI,GACnB,OAAO,CAAC,CAAC,CAAC,CAqBZ"}
package/dist/index.cjs CHANGED
@@ -32,6 +32,38 @@ let path_posix = require("path/posix");
32
32
  path_posix = __toESM(path_posix, 1);
33
33
  let _grpc_grpc_js = require("@grpc/grpc-js");
34
34
  let child_process = require("child_process");
35
+ //#region src/abort.ts
36
+ function createAbortError(signal, operation) {
37
+ return new _mastra_core_workspace.SandboxAbortError(operation, signal.reason);
38
+ }
39
+ function throwIfAborted(signal, operation) {
40
+ if (signal?.aborted) throw createAbortError(signal, operation);
41
+ }
42
+ function isAbortError(error) {
43
+ return error instanceof _mastra_core_workspace.SandboxAbortError || error instanceof Error && (error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR");
44
+ }
45
+ function normalizeAbortError(error, operation) {
46
+ return error instanceof _mastra_core_workspace.SandboxAbortError ? error : isAbortError(error) ? new _mastra_core_workspace.SandboxAbortError(operation, error) : error;
47
+ }
48
+ function waitForAbortable(promise, signal, operation, onAbort) {
49
+ throwIfAborted(signal, operation);
50
+ if (!signal) return promise;
51
+ return new Promise((resolve, reject) => {
52
+ const abort = () => {
53
+ onAbort?.();
54
+ reject(new _mastra_core_workspace.SandboxAbortError(operation, signal.reason));
55
+ };
56
+ signal.addEventListener("abort", abort, { once: true });
57
+ promise.then((value) => {
58
+ signal.removeEventListener("abort", abort);
59
+ resolve(value);
60
+ }, (error) => {
61
+ signal.removeEventListener("abort", abort);
62
+ reject(normalizeAbortError(error, operation));
63
+ });
64
+ });
65
+ }
66
+ //#endregion
35
67
  //#region src/sandbox/process-manager.ts
36
68
  /**
37
69
  * Docker Process Manager
@@ -204,16 +236,20 @@ var DockerProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
204
236
  AttachStdout: false,
205
237
  AttachStderr: false
206
238
  });
207
- await killExec.start({});
208
- let killInfo = await killExec.inspect();
209
- while (killInfo.Running) {
210
- await new Promise((resolve) => setTimeout(resolve, 10));
211
- killInfo = await killExec.inspect();
239
+ const killStream = await killExec.start({});
240
+ try {
241
+ let killInfo = await killExec.inspect();
242
+ while (killInfo.Running) {
243
+ await new Promise((resolve) => setTimeout(resolve, 10));
244
+ killInfo = await killExec.inspect();
245
+ }
246
+ if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
247
+ this._killed = true;
248
+ this._destroyStream();
249
+ return true;
250
+ } finally {
251
+ killStream.destroy();
212
252
  }
213
- if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
214
- this._killed = true;
215
- this._destroyStream();
216
- return true;
217
253
  } catch (error) {
218
254
  const msg = error instanceof Error ? error.message.toLowerCase() : "";
219
255
  if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
@@ -270,6 +306,7 @@ var DockerProcessManager = class extends _mastra_core_workspace.SandboxProcessMa
270
306
  const container = this.container;
271
307
  const pgidFile = `${PROC_DIR}/${(0, crypto.randomUUID)()}`;
272
308
  const envArray = Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
309
+ const attachStdin = options.stdinMode !== "ignore";
273
310
  const exec = await container.exec({
274
311
  Cmd: [
275
312
  "sh",
@@ -281,17 +318,17 @@ var DockerProcessManager = class extends _mastra_core_workspace.SandboxProcessMa
281
318
  ],
282
319
  AttachStdout: true,
283
320
  AttachStderr: true,
284
- AttachStdin: true,
321
+ AttachStdin: attachStdin,
285
322
  Tty: false,
286
323
  Env: envArray.length > 0 ? envArray : void 0,
287
324
  WorkingDir: options.cwd
288
325
  });
289
326
  const stream = await exec.start({
290
327
  hijack: true,
291
- stdin: true
328
+ stdin: attachStdin
292
329
  });
293
330
  const startTime = Date.now();
294
- const handle = new DockerProcessHandle(exec, container, startTime, stream, pgidFile, options);
331
+ const handle = new DockerProcessHandle(exec, container, startTime, attachStdin ? stream : null, pgidFile, options);
295
332
  handle._setExecStream(stream);
296
333
  const waitPromise = new Promise((resolve) => {
297
334
  const buffer = [];
@@ -564,7 +601,9 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
564
601
  if (!this._container) throw new _mastra_core_workspace.SandboxNotReadyError(this.id);
565
602
  return this._container;
566
603
  }
567
- async start() {
604
+ async start(options = {}) {
605
+ const { abortSignal } = options;
606
+ throwIfAborted(abortSignal, "start Docker sandbox");
568
607
  this.logger.debug(`${LOG_PREFIX} Starting sandbox ${this.id}...`);
569
608
  const existing = await this._findExistingContainer();
570
609
  if (existing) {
@@ -585,7 +624,7 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
585
624
  return;
586
625
  }
587
626
  this._warnOnPrivilegedHardeningConflict(this._privileged);
588
- await this._resolveTemplate();
627
+ await this._resolveTemplate(abortSignal);
589
628
  await this._ensureImage();
590
629
  const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
591
630
  const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
@@ -845,10 +884,20 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
845
884
  * creates a container, so a resolver-form template re-resolves each time.
846
885
  * Adopts the template's workdir unless the sandbox was given one explicitly.
847
886
  */
848
- async _resolveTemplate() {
887
+ async _resolveTemplate(abortSignal) {
849
888
  if (!this._templateSpec) return;
850
- const template = typeof this._templateSpec === "function" ? await this._templateSpec() : this._templateSpec;
851
- const result = await template.build({ docker: this._docker });
889
+ throwIfAborted(abortSignal, "start Docker sandbox");
890
+ let template;
891
+ try {
892
+ template = typeof this._templateSpec === "function" ? await this._templateSpec({ abortSignal }) : this._templateSpec;
893
+ } catch (error) {
894
+ throw normalizeAbortError(error, "start Docker sandbox");
895
+ }
896
+ throwIfAborted(abortSignal, "start Docker sandbox");
897
+ const result = await template.build({
898
+ docker: this._docker,
899
+ abortSignal
900
+ });
852
901
  if (result.status !== "ready") throw new _mastra_core_workspace.SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
853
902
  templateId: result.templateId,
854
903
  reason: "template_build_failed"
@@ -1221,9 +1270,17 @@ const AUTH_CREDENTIALS_METHOD = "/moby.filesync.v1.Auth/Credentials";
1221
1270
  * Open a session that answers `GetSecret` from `secrets`. The returned id must
1222
1271
  * be sent as the `session` query parameter of the build request.
1223
1272
  */
1224
- function openBuildSession(docker, secrets) {
1273
+ function openBuildSession(docker, secrets, abortSignal) {
1274
+ throwIfAborted(abortSignal, "build Docker template");
1225
1275
  const id = (0, crypto.randomUUID)();
1226
1276
  return new Promise((resolve, reject) => {
1277
+ let settled = false;
1278
+ const abort = () => {
1279
+ if (settled || !abortSignal) return;
1280
+ settled = true;
1281
+ reject(createAbortError(abortSignal, "build Docker template"));
1282
+ };
1283
+ abortSignal?.addEventListener("abort", abort, { once: true });
1227
1284
  docker.modem.dial({
1228
1285
  method: "POST",
1229
1286
  path: "/session",
@@ -1239,8 +1296,14 @@ function openBuildSession(docker, secrets) {
1239
1296
  500: "server error"
1240
1297
  }
1241
1298
  }, (err, socket) => {
1299
+ if (settled) {
1300
+ socket?.end?.();
1301
+ return;
1302
+ }
1303
+ settled = true;
1304
+ abortSignal?.removeEventListener("abort", abort);
1242
1305
  if (err) {
1243
- reject(err);
1306
+ reject(abortSignal?.aborted ? createAbortError(abortSignal, "build Docker template") : err);
1244
1307
  return;
1245
1308
  }
1246
1309
  const server = new _grpc_grpc_js.Server();
@@ -1561,7 +1624,8 @@ var DockerTemplate = class DockerTemplate {
1561
1624
  #secrets;
1562
1625
  #docker;
1563
1626
  #built = false;
1564
- #inFlight;
1627
+ #queueTail;
1628
+ #plainBuild;
1565
1629
  constructor(options = {}, state) {
1566
1630
  this.#baseImage = state ? state.baseImage : validateString(options.baseImage ?? "node:22-slim", "baseImage");
1567
1631
  this.#operations = state?.operations ?? [];
@@ -1708,19 +1772,49 @@ var DockerTemplate = class DockerTemplate {
1708
1772
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1709
1773
  */
1710
1774
  async build(options = {}) {
1711
- if (!options.force && options.secrets === void 0 && options.docker === void 0 && this.#inFlight) return this.#inFlight;
1712
- const run = (this.#inFlight?.catch(() => void 0) ?? Promise.resolve()).then(() => this.#build(options));
1713
- this.#inFlight = run;
1714
- run.finally(() => {
1715
- if (this.#inFlight === run) this.#inFlight = void 0;
1775
+ throwIfAborted(options.abortSignal, "build Docker template");
1776
+ const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1777
+ if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
1778
+ const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1779
+ const controller = new AbortController();
1780
+ const run = {
1781
+ controller,
1782
+ waiters: 0,
1783
+ promise: previous.then(() => {
1784
+ throwIfAborted(controller.signal, "build Docker template");
1785
+ return this.#build({
1786
+ ...options,
1787
+ abortSignal: controller.signal
1788
+ });
1789
+ })
1790
+ };
1791
+ this.#queueTail = run;
1792
+ if (isPlain) this.#plainBuild = run;
1793
+ run.promise.finally(() => {
1794
+ if (this.#queueTail === run) this.#queueTail = void 0;
1795
+ if (this.#plainBuild === run) this.#plainBuild = void 0;
1716
1796
  }).catch(() => void 0);
1717
- return run;
1797
+ return this.#waitForBuild(run, options.abortSignal);
1798
+ }
1799
+ #waitForBuild(run, abortSignal) {
1800
+ run.waiters += 1;
1801
+ let settled = false;
1802
+ const release = (aborted) => {
1803
+ if (settled) return;
1804
+ settled = true;
1805
+ run.waiters -= 1;
1806
+ if (aborted && run.waiters === 0) run.controller.abort();
1807
+ };
1808
+ return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1718
1809
  }
1719
1810
  async #build(options) {
1720
1811
  const docker = options.docker ?? this.#getDocker();
1721
1812
  const tag = this.templateId;
1813
+ const { abortSignal } = options;
1814
+ throwIfAborted(abortSignal, "build Docker template");
1722
1815
  if (!options.force) try {
1723
1816
  await docker.getImage(tag).inspect();
1817
+ throwIfAborted(abortSignal, "build Docker template");
1724
1818
  this.#built = true;
1725
1819
  return {
1726
1820
  status: "ready",
@@ -1730,24 +1824,37 @@ var DockerTemplate = class DockerTemplate {
1730
1824
  if (!isImageNotFoundError(error)) throw error;
1731
1825
  }
1732
1826
  const secrets = await this.#resolveSecrets(options.secrets);
1827
+ throwIfAborted(abortSignal, "build Docker template");
1733
1828
  const context = (0, tar_stream.pack)();
1734
1829
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1735
1830
  context.finalize();
1736
1831
  try {
1737
1832
  const nocache = options.force === true;
1738
1833
  if (secrets) {
1739
- const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache);
1834
+ const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1740
1835
  try {
1741
- await this.#followBuild(docker, stream);
1836
+ await this.#followBuild(docker, stream, abortSignal);
1742
1837
  } finally {
1743
1838
  session.close();
1744
1839
  }
1745
- } else await this.#followBuild(docker, await docker.buildImage(context, {
1746
- t: tag,
1747
- nocache
1748
- }));
1840
+ } else {
1841
+ const stream = await waitForAbortable(docker.buildImage(context, {
1842
+ t: tag,
1843
+ nocache
1844
+ }).then((stream) => {
1845
+ if (abortSignal?.aborted) {
1846
+ stream.destroy?.();
1847
+ throw createAbortError(abortSignal, "build Docker template");
1848
+ }
1849
+ return stream;
1850
+ }), abortSignal, "build Docker template", () => {
1851
+ context.destroy?.();
1852
+ });
1853
+ await this.#followBuild(docker, stream, abortSignal);
1854
+ }
1749
1855
  } catch (error) {
1750
1856
  this.#built = false;
1857
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
1751
1858
  return {
1752
1859
  status: "failed",
1753
1860
  templateId: tag,
@@ -1778,12 +1885,35 @@ var DockerTemplate = class DockerTemplate {
1778
1885
  * directly because `docker.buildImage` replaces any session id with its own
1779
1886
  * auth-only session when `version` is `'2'`.
1780
1887
  */
1781
- async #buildWithSecrets(docker, context, tag, secrets, nocache) {
1782
- const session = await openBuildSession(docker, secrets);
1888
+ async #buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal) {
1889
+ throwIfAborted(abortSignal, "build Docker template");
1890
+ const session = await openBuildSession(docker, secrets, abortSignal);
1783
1891
  let stream;
1784
1892
  try {
1785
1893
  stream = await new Promise((resolve, reject) => {
1786
- docker.modem.dial({
1894
+ let settled = false;
1895
+ let request;
1896
+ const finish = (error, data) => {
1897
+ if (settled) {
1898
+ if (data) data.destroy?.();
1899
+ return;
1900
+ }
1901
+ settled = true;
1902
+ abortSignal?.removeEventListener("abort", abort);
1903
+ error === void 0 ? resolve(data) : reject(error);
1904
+ };
1905
+ const abort = () => {
1906
+ context.destroy?.();
1907
+ request?.destroy?.();
1908
+ session.close();
1909
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1910
+ };
1911
+ abortSignal?.addEventListener("abort", abort, { once: true });
1912
+ if (abortSignal?.aborted) {
1913
+ abort();
1914
+ return;
1915
+ }
1916
+ request = docker.modem.dial({
1787
1917
  path: "/build?",
1788
1918
  method: "POST",
1789
1919
  file: context,
@@ -1798,7 +1928,7 @@ var DockerTemplate = class DockerTemplate {
1798
1928
  200: true,
1799
1929
  500: "server error"
1800
1930
  }
1801
- }, (err, data) => err ? reject(err) : resolve(data));
1931
+ }, (err, data) => finish(err ?? void 0, data));
1802
1932
  });
1803
1933
  } catch (error) {
1804
1934
  session.close();
@@ -1809,20 +1939,36 @@ var DockerTemplate = class DockerTemplate {
1809
1939
  session
1810
1940
  };
1811
1941
  }
1812
- #followBuild(docker, stream) {
1942
+ #followBuild(docker, stream, abortSignal) {
1813
1943
  return new Promise((resolve, reject) => {
1944
+ let settled = false;
1945
+ const finish = (error) => {
1946
+ if (settled) return;
1947
+ settled = true;
1948
+ abortSignal?.removeEventListener("abort", abort);
1949
+ error === void 0 ? resolve() : reject(error);
1950
+ };
1951
+ const abort = () => {
1952
+ stream.destroy?.();
1953
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1954
+ };
1955
+ abortSignal?.addEventListener("abort", abort, { once: true });
1956
+ if (abortSignal?.aborted) {
1957
+ abort();
1958
+ return;
1959
+ }
1814
1960
  docker.modem.followProgress(stream, (err, output) => {
1815
1961
  if (err) {
1816
- reject(err);
1962
+ finish(err);
1817
1963
  return;
1818
1964
  }
1819
1965
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
1820
1966
  if (failure) {
1821
1967
  const detail = failure.errorDetail;
1822
- reject(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1968
+ finish(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1823
1969
  return;
1824
1970
  }
1825
- resolve();
1971
+ finish();
1826
1972
  });
1827
1973
  });
1828
1974
  }
@@ -1971,16 +2117,31 @@ function createDockerRepoTemplate(options) {
1971
2117
  if (options.ref !== void 0 && !REF_PATTERN.test(options.ref)) throw new Error(`Invalid ref '${options.ref}': expected a git ref name`);
1972
2118
  const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
1973
2119
  if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
1974
- return () => resolveRepoTemplate(options, workingDirectory);
2120
+ return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
1975
2121
  }
1976
- async function resolveRepoTemplate(options, workingDirectory) {
1977
- const access = await options.getRepositoryAccess();
2122
+ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
2123
+ const { abortSignal } = resolveOptions;
2124
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2125
+ let access;
2126
+ try {
2127
+ access = await options.getRepositoryAccess(resolveOptions);
2128
+ } catch (error) {
2129
+ throw normalizeAbortError(error, "resolve Docker repository template");
2130
+ }
2131
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1978
2132
  const cloneUrl = access?.cloneUrl;
1979
2133
  if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1980
2134
  assertCloneUrl(cloneUrl);
1981
2135
  const token = access?.authorization?.token;
1982
- const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1983
- const sha = await resolveHead(cloneUrl, options.ref, token);
2136
+ let buildEnv;
2137
+ try {
2138
+ buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
2139
+ } catch (error) {
2140
+ throw normalizeAbortError(error, "resolve Docker repository template");
2141
+ }
2142
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2143
+ const sha = await resolveHead(cloneUrl, options.ref, token, abortSignal);
2144
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1984
2145
  if (!sha) throw new Error(`Could not resolve ${options.ref ?? "HEAD"} of ${cloneUrl} with git ls-remote; check the ref, the credential and network access`);
1985
2146
  return buildRepoTemplate({
1986
2147
  cloneUrl,
@@ -2029,7 +2190,8 @@ function buildRepoTemplate(inputs) {
2029
2190
  * yields undefined; the caller decides how to surface it.
2030
2191
  * @internal exported for tests.
2031
2192
  */
2032
- async function resolveHead(cloneUrl, ref, token) {
2193
+ async function resolveHead(cloneUrl, ref, token, abortSignal) {
2194
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2033
2195
  if (ref && FULL_SHA_PATTERN.test(ref)) return ref.toLowerCase();
2034
2196
  try {
2035
2197
  const authEnv = token ? {
@@ -2044,6 +2206,7 @@ async function resolveHead(cloneUrl, ref, token) {
2044
2206
  ref ?? "HEAD"
2045
2207
  ], {
2046
2208
  timeout: 1e4,
2209
+ signal: abortSignal,
2047
2210
  env: {
2048
2211
  ...process.env,
2049
2212
  ...authEnv,
@@ -2053,7 +2216,8 @@ async function resolveHead(cloneUrl, ref, token) {
2053
2216
  const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
2054
2217
  const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
2055
2218
  return sha && SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
2056
- } catch {
2219
+ } catch (error) {
2220
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
2057
2221
  return;
2058
2222
  }
2059
2223
  }