@mastra/e2b 0.12.4-alpha.0 → 0.13.0-alpha.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.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { E2BSandbox, type E2BSandboxOptions } from './sandbox/index.js';
2
2
  export { E2BProcessManager } from './sandbox/process-manager.js';
3
3
  export { createDefaultMountableTemplate, isNamedTemplateSpec, isDeferredNamedTemplateSpec, DEFAULT_NODE_VERSION, type TemplateSpec, type NamedTemplateSpec, type DeferredNamedTemplateSpec, type MountableTemplateResult, type MountableTemplateOptions, } from './utils/template.js';
4
- export { createRepoTemplate, refreshRepoTemplate, repoTemplateRef, type RepoTemplateOptions, type RepoTemplateIdentity, type RepositoryAccess, type RefreshRepoTemplateResult, } from './utils/repo-template.js';
4
+ export { createRepoTemplate, refreshRepoTemplate, repoTemplateRef, type RepoTemplateOptions, type RepoTemplateRepository, type RepoTemplateIdentity, type RepositoryAccess, type RefreshRepoTemplateResult, } from './utils/repo-template.js';
5
5
  export { type E2BS3MountConfig, type E2BGCSMountConfig, type E2BAzureBlobMountConfig, type E2BMountConfig, } from './sandbox/mounts/index.js';
6
6
  export { e2bSandboxProvider } from './provider.js';
7
7
  export { E2BCodeModeTransport } from './code-mode/transport.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EACL,8BAA8B,EAC9B,mBAAmB,EACnB,2BAA2B,EAC3B,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EACL,8BAA8B,EAC9B,mBAAmB,EACnB,2BAA2B,EAC3B,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -126,7 +126,7 @@ function validatePrefix(prefix) {
126
126
  * Arguments containing only safe characters are returned as-is.
127
127
  * All others are wrapped in single quotes with embedded single quotes escaped.
128
128
  */
129
- function shellQuote$1(arg) {
129
+ function shellQuote$2(arg) {
130
130
  if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
131
131
  return "'" + arg.replace(/'/g, "'\\''") + "'";
132
132
  }
@@ -179,12 +179,12 @@ Error details: ${installResult.stderr || installResult.stdout}`);
179
179
  };
180
180
  if (hasCredentials) {
181
181
  const credentialsContent = useSessionToken ? [
182
- `export AWSACCESSKEYID=${shellQuote$1(config.accessKeyId)}`,
183
- `export AWSSECRETACCESSKEY=${shellQuote$1(config.secretAccessKey)}`,
184
- `export AWSSESSIONTOKEN=${shellQuote$1(config.sessionToken)}`,
185
- `export AWS_ACCESS_KEY_ID=${shellQuote$1(config.accessKeyId)}`,
186
- `export AWS_SECRET_ACCESS_KEY=${shellQuote$1(config.secretAccessKey)}`,
187
- `export AWS_SESSION_TOKEN=${shellQuote$1(config.sessionToken)}`
182
+ `export AWSACCESSKEYID=${shellQuote$2(config.accessKeyId)}`,
183
+ `export AWSSECRETACCESSKEY=${shellQuote$2(config.secretAccessKey)}`,
184
+ `export AWSSESSIONTOKEN=${shellQuote$2(config.sessionToken)}`,
185
+ `export AWS_ACCESS_KEY_ID=${shellQuote$2(config.accessKeyId)}`,
186
+ `export AWS_SECRET_ACCESS_KEY=${shellQuote$2(config.secretAccessKey)}`,
187
+ `export AWS_SESSION_TOKEN=${shellQuote$2(config.sessionToken)}`
188
188
  ].join("\n") : `${config.accessKeyId}:${config.secretAccessKey}`;
189
189
  await sandbox.commands.run(`sudo rm -f ${credentialsPath}`);
190
190
  try {
@@ -212,8 +212,8 @@ Error details: ${installResult.stderr || installResult.stdout}`);
212
212
  mountOptions.push("ro");
213
213
  logger.debug(`${LOG_PREFIX} Mounting as read-only`);
214
214
  }
215
- const s3fsCmd = `s3fs ${shellQuote$1(bucketArg)} ${shellQuote$1(mountPath)} -o ${mountOptions.join(" -o ")}`;
216
- const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote$1(`. ${credentialsPath} && exec ${s3fsCmd}`)}` : `sudo ${s3fsCmd}`;
215
+ const s3fsCmd = `s3fs ${shellQuote$2(bucketArg)} ${shellQuote$2(mountPath)} -o ${mountOptions.join(" -o ")}`;
216
+ const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote$2(`. ${credentialsPath} && exec ${s3fsCmd}`)}` : `sudo ${s3fsCmd}`;
217
217
  logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
218
218
  try {
219
219
  const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
@@ -236,7 +236,7 @@ Error details: ${installResult.stderr || installResult.stdout}`);
236
236
  } finally {
237
237
  if (useSessionToken) await removeSessionCredentials();
238
238
  }
239
- if ((await sandbox.commands.run(`mountpoint -q ${shellQuote$1(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
239
+ if ((await sandbox.commands.run(`mountpoint -q ${shellQuote$2(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
240
240
  }
241
241
  //#endregion
242
242
  //#region src/sandbox/mounts/gcs.ts
@@ -256,7 +256,7 @@ async function mountGCS(mountPath, config, ctx) {
256
256
  }
257
257
  const [uid, gid] = (await sandbox.commands.run("id -u && id -g")).stdout.trim().split("\n");
258
258
  const uidGidFlags = uid && gid ? `--uid=${uid} --gid=${gid}` : "";
259
- const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote$1(validatePrefix(config.prefix))}` : "";
259
+ const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote$2(validatePrefix(config.prefix))}` : "";
260
260
  const hasCredentials = !!config.serviceAccountKey;
261
261
  let mountCmd;
262
262
  if (hasCredentials) {
@@ -464,9 +464,9 @@ Error details: ${verifyResult.stderr || verifyResult.stdout || installResult?.st
464
464
  await sandbox.commands.run(`sudo rm -f ${configPath}`);
465
465
  await sandbox.files.write(configPath, yaml);
466
466
  await sandbox.commands.run(`sudo chown root:root ${configPath} && sudo chmod 600 ${configPath}`);
467
- await sandbox.commands.run(`sudo rm -rf ${shellQuote$1(cachePath)} && sudo mkdir -p ${shellQuote$1(cachePath)}`);
468
- const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote$1(prefix)}` : "";
469
- const mountCmd = `sudo blobfuse2 mount ${shellQuote$1(mountPath)} --config-file=${shellQuote$1(configPath)}${prefixFlags}`;
467
+ await sandbox.commands.run(`sudo rm -rf ${shellQuote$2(cachePath)} && sudo mkdir -p ${shellQuote$2(cachePath)}`);
468
+ const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote$2(prefix)}` : "";
469
+ const mountCmd = `sudo blobfuse2 mount ${shellQuote$2(mountPath)} --config-file=${shellQuote$2(configPath)}${prefixFlags}`;
470
470
  logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
471
471
  try {
472
472
  const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
@@ -1593,6 +1593,8 @@ var E2BSandbox = class E2BSandbox extends MastraSandbox {
1593
1593
  };
1594
1594
  //#endregion
1595
1595
  //#region ../../packages/_internals/workspace/dist/index.js
1596
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
1597
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
1596
1598
  /**
1597
1599
  * Setup completion marker shared by repo templates and their consumers.
1598
1600
  *
@@ -1602,8 +1604,29 @@ var E2BSandbox = class E2BSandbox extends MastraSandbox {
1602
1604
  * booted from the image tell whether the setup it is about to run already
1603
1605
  * happened. Relative to the template's build cwd, which is also the runtime
1604
1606
  * working directory the repo was cloned into.
1607
+ *
1608
+ * This is the single-repository marker. Templates built from a repository
1609
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
1605
1610
  */
1606
1611
  const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
1612
+ /**
1613
+ * Marker for the workspace-level setup of a multi-repository template: the
1614
+ * commands that ran at the working directory after every repository was set
1615
+ * up. Written even when there were no such commands, so a consumer can tell
1616
+ * "ran nothing" from "never built".
1617
+ */
1618
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
1619
+ /**
1620
+ * One line per repository directory whose setup failed while the build was
1621
+ * told to continue. Absent when nothing failed. Consumers check this before
1622
+ * trusting a per-repository marker: that marker means every step ran, not
1623
+ * that every step succeeded.
1624
+ */
1625
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
1626
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
1627
+ function repoSetupMarkerPath(repoDir) {
1628
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
1629
+ }
1607
1630
  /** Blank entries never become build steps, so they never count toward the digest either. */
1608
1631
  function normalizeSetupCommands$1(setupCommand) {
1609
1632
  return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
@@ -1613,8 +1636,26 @@ function setupMarkerContent(setupCommand) {
1613
1636
  return `sha256:${createHash("sha256").update(normalizeSetupCommands$1(setupCommand).join("\n")).digest("hex")}`;
1614
1637
  }
1615
1638
  /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1616
- function setupMarkerCommand(content) {
1617
- return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
1639
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
1640
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
1641
+ }
1642
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
1643
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
1644
+ /**
1645
+ * Shell step that runs one setup command inside a repository clone.
1646
+ *
1647
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
1648
+ * the `cd` sits inside the parenthesized group so that the failure branch
1649
+ * runs back at the build cwd and the failure list lands at the workspace
1650
+ * level, never inside the repository.
1651
+ */
1652
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
1653
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
1654
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
1655
+ return `( cd "${repoDir}" && sh -c ${shellQuote$1(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
1656
+ }
1657
+ function shellQuote$1(value) {
1658
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1618
1659
  }
1619
1660
  function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1620
1661
  return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
@@ -1706,8 +1747,16 @@ function isValidCloneUrl(cloneUrl) {
1706
1747
  */
1707
1748
  function repoTemplateRef(identity) {
1708
1749
  const name = repoTemplateName(identity);
1750
+ if (identity.repos) {
1751
+ const shas = identity.repos.map((repo) => repo.sha);
1752
+ return shas.every(Boolean) ? `${name}:${shaTag(listSha(shas))}` : `${name}:${CURRENT_TAG}`;
1753
+ }
1709
1754
  return identity.sha ? `${name}:${shaTag(identity.sha)}` : `${name}:${CURRENT_TAG}`;
1710
1755
  }
1756
+ /** One digest over every repository's pinned sha, in caller order. */
1757
+ function listSha(shas) {
1758
+ return createHash("sha256").update(shas.map((sha) => sha.toLowerCase()).join(",")).digest("hex");
1759
+ }
1711
1760
  function repoTemplateName(identity) {
1712
1761
  const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
1713
1762
  const config = [
@@ -1717,7 +1766,13 @@ function repoTemplateName(identity) {
1717
1766
  identity.buildEnv ? Object.entries(identity.buildEnv).sort(([a], [b]) => a.localeCompare(b)) : null,
1718
1767
  identity.cpuCount ?? 2,
1719
1768
  identity.memoryMB ?? 1024,
1720
- ...identity.workingDirectory !== void 0 ? [identity.workingDirectory] : []
1769
+ ...identity.workingDirectory !== void 0 ? [identity.workingDirectory] : [],
1770
+ ...identity.repos ? [
1771
+ "repos",
1772
+ identity.repos.map((repo) => [normalizeCloneUrl(repo.cloneUrl), repo.setupCommand ?? null]),
1773
+ normalizeSetupCommands(identity.workspaceSetupCommand),
1774
+ identity.continueOnSetupFailure ?? false
1775
+ ] : []
1721
1776
  ];
1722
1777
  const hash = createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 8);
1723
1778
  const { owner, repo } = parseCloneUrl(cloneUrl);
@@ -1749,7 +1804,9 @@ function shaTag(sha) {
1749
1804
  * setup performs the full clone, so a broken build never wedges a session.
1750
1805
  */
1751
1806
  function createRepoTemplate(options) {
1752
- if (!options.getRepositoryAccess) return void 0;
1807
+ if (options.getRepositoryAccess && options.repos) throw new TypeError("createRepoTemplate: pass either getRepositoryAccess or repos, not both");
1808
+ if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
1809
+ if (!options.getRepositoryAccess && !options.repos?.length) return void 0;
1753
1810
  return { resolveSpec: async () => (await resolveSpecAtHead(options)).spec };
1754
1811
  }
1755
1812
  /**
@@ -1761,24 +1818,58 @@ function createRepoTemplate(options) {
1761
1818
  * turns into its default-template fallback rather than a failed start.
1762
1819
  */
1763
1820
  async function resolveSpecAtHead(options) {
1764
- const access = options.getRepositoryAccess ? await options.getRepositoryAccess().catch(() => void 0) : void 0;
1765
- const cloneUrl = access?.cloneUrl;
1766
- if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1767
- assertCloneUrl(cloneUrl);
1768
- const token = access?.authorization?.token;
1821
+ const isList = options.repos !== void 0;
1822
+ const entries = options.repos ? options.repos : options.getRepositoryAccess ? [{
1823
+ getRepositoryAccess: options.getRepositoryAccess,
1824
+ setupCommand: options.setupCommand
1825
+ }] : [];
1826
+ if (entries.length === 0) throw new Error("Repo template has no clone URL: repository access returned none.");
1769
1827
  const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1770
- const resolved = await resolveDefaultBranchHead(cloneUrl, token).catch(() => void 0);
1771
- const sha = resolved && SHA_PATTERN.test(resolved) ? resolved : void 0;
1772
- return {
1773
- spec: buildRepoTemplateSpec({
1828
+ const repos = [];
1829
+ const seenDirs = /* @__PURE__ */ new Map();
1830
+ for (const [index, entry] of entries.entries()) {
1831
+ const access = await entry.getRepositoryAccess().catch(() => void 0);
1832
+ const cloneUrl = access?.cloneUrl;
1833
+ if (!cloneUrl) throw new Error(`Repo template has no clone URL: repository access returned none${isList ? ` (repos[${index}])` : ""}.`);
1834
+ assertCloneUrl(cloneUrl);
1835
+ const repoDir = repoDirName(normalizeCloneUrl(cloneUrl));
1836
+ const other = seenDirs.get(repoDir);
1837
+ if (other) throw new Error(`Repo template: repositories ${other} and ${cloneUrl} would both clone into "${repoDir}"`);
1838
+ seenDirs.set(repoDir, cloneUrl);
1839
+ const token = access?.authorization?.token;
1840
+ const resolved = await resolveDefaultBranchHead(cloneUrl, token).catch(() => void 0);
1841
+ const sha = resolved && SHA_PATTERN.test(resolved) ? resolved : void 0;
1842
+ repos.push({
1774
1843
  cloneUrl,
1775
- ...sha ? { sha } : {},
1776
- ...normalizeSetupCommands(options.setupCommand).length > 0 ? { setupCommand: options.setupCommand } : {},
1777
- ...buildEnv ? { buildEnv } : {},
1778
- ...options.cpuCount !== void 0 ? { cpuCount: options.cpuCount } : {},
1779
- ...options.memoryMB !== void 0 ? { memoryMB: options.memoryMB } : {},
1780
- ...options.workingDirectory !== void 0 ? { workingDirectory: trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory)) } : {}
1781
- }, token),
1844
+ repoDir,
1845
+ token,
1846
+ tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
1847
+ sha,
1848
+ setupCommand: entry.setupCommand
1849
+ });
1850
+ }
1851
+ const first = repos[0];
1852
+ const spec = buildRepoTemplateSpec({
1853
+ cloneUrl: first.cloneUrl,
1854
+ ...first.sha ? { sha: first.sha } : {},
1855
+ ...normalizeSetupCommands(first.setupCommand).length > 0 ? { setupCommand: first.setupCommand } : {},
1856
+ ...buildEnv ? { buildEnv } : {},
1857
+ ...options.cpuCount !== void 0 ? { cpuCount: options.cpuCount } : {},
1858
+ ...options.memoryMB !== void 0 ? { memoryMB: options.memoryMB } : {},
1859
+ ...options.workingDirectory !== void 0 ? { workingDirectory: trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory)) } : {},
1860
+ ...isList ? {
1861
+ repos: repos.map((repo) => ({
1862
+ cloneUrl: repo.cloneUrl,
1863
+ ...repo.sha ? { sha: repo.sha } : {},
1864
+ ...normalizeSetupCommands(repo.setupCommand).length > 0 ? { setupCommand: repo.setupCommand } : {}
1865
+ })),
1866
+ ...options.workspaceSetupCommand !== void 0 ? { workspaceSetupCommand: options.workspaceSetupCommand } : {},
1867
+ ...options.continueOnSetupFailure !== void 0 ? { continueOnSetupFailure: options.continueOnSetupFailure } : {}
1868
+ } : {}
1869
+ }, repos, options.continueOnSetupFailure ?? false);
1870
+ const sha = isList ? repos.every((repo) => repo.sha) ? listSha(repos.map((repo) => repo.sha)) : void 0 : first.sha;
1871
+ return {
1872
+ spec,
1782
1873
  ...sha ? { sha } : {}
1783
1874
  };
1784
1875
  }
@@ -1828,31 +1919,45 @@ function assertCloneUrl(cloneUrl) {
1828
1919
  * REFERENCE — the token value never appears in the command string, and no
1829
1920
  * credential is written to the build filesystem.
1830
1921
  */
1831
- function gitAuthFlag() {
1832
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
1922
+ function gitAuthFlag(tokenEnv) {
1923
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
1833
1924
  }
1834
- function buildRepoTemplateSpec(identity, token) {
1835
- const { sha, setupCommand, buildEnv, workingDirectory } = identity;
1836
- const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
1837
- const repoDir = repoDirName(cloneUrl);
1838
- const auth = token ? `${gitAuthFlag()} ` : "";
1925
+ function buildRepoTemplateSpec(identity, repos, continueOnFailure) {
1926
+ const { buildEnv, workingDirectory } = identity;
1927
+ const isList = identity.repos !== void 0;
1839
1928
  let template = createDefaultMountableTemplate().template;
1840
1929
  const env = { ...buildEnv };
1841
- if (token) env[BUILD_TOKEN_ENV] = token;
1930
+ for (const repo of repos) if (repo.tokenEnv && repo.token) env[repo.tokenEnv] = repo.token;
1842
1931
  if (Object.keys(env).length > 0) template = template.setEnvs(env);
1843
1932
  if (workingDirectory) {
1844
1933
  const dir = trimTrailingSlashes(workingDirectory);
1845
1934
  template = template.runCmd(`mkdir -p "${dir}"`).setWorkdir(dir);
1846
1935
  }
1847
- template = template.runCmd(repoCloneCommand({
1848
- cloneUrl,
1849
- destination: repoDir,
1850
- ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
1851
- }));
1852
- if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
1853
- const setupCommands = normalizeSetupCommands(setupCommand);
1854
- for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
1855
- template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
1936
+ const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
1937
+ for (const repo of ordered) {
1938
+ const { repoDir, sha, tokenEnv } = repo;
1939
+ const cloneUrl = normalizeCloneUrl(repo.cloneUrl);
1940
+ const auth = tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : "";
1941
+ const setupCommands = normalizeSetupCommands(repo.setupCommand);
1942
+ template = template.runCmd(repoCloneCommand({
1943
+ cloneUrl,
1944
+ destination: repoDir,
1945
+ ...tokenEnv ? { tokenEnv } : {}
1946
+ }));
1947
+ if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
1948
+ for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
1949
+ repoDir,
1950
+ command,
1951
+ continueOnFailure
1952
+ }));
1953
+ const content = setupMarkerContent(setupCommands);
1954
+ template = template.runCmd(isList ? setupMarkerCommand(content, repoSetupMarkerPath(repoDir)) : setupMarkerCommand(content));
1955
+ }
1956
+ if (isList) {
1957
+ const workspaceCommands = normalizeSetupCommands(identity.workspaceSetupCommand);
1958
+ for (const command of workspaceCommands) template = template.runCmd(command);
1959
+ template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
1960
+ }
1856
1961
  return {
1857
1962
  ref: repoTemplateRef(identity),
1858
1963
  template,