@mastra/platform-workspace 1.6.2 → 1.7.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
@@ -2,7 +2,7 @@ export { PlatformClient, PlatformApiError, type PlatformClientOptions, type Plat
2
2
  export { PlatformFilesystem, type PlatformFilesystemOptions } from './filesystem.js';
3
3
  export { Template, type AptInstallOptions, type JsonPrimitive, type JsonValue, type NpmInstallOptions, type PipInstallOptions, type SandboxTemplateBuildOptions, type SandboxTemplateBuildResult, type SandboxTemplateBuilder, type SandboxTemplateMethod, type SetEnvsOptions, type SandboxTemplateOperation, type SerializedSandboxTemplate, } from './template.js';
4
4
  export { PlatformSandbox, SandboxExecTransportError, SandboxDestroyedError, type PlatformSandboxOptions, type PlatformSandboxNetworkIsolation, type PlatformSandboxTemplate, type SandboxAddressRegistry, type SandboxTemplatePending, } from './sandbox.js';
5
- export { createRepoTemplate, type PlatformRepoTemplateOptions, type PlatformRepoTemplateResolver, } from './repo-template.js';
5
+ export { createRepoTemplate, type PlatformRepoTemplateOptions, type PlatformRepoTemplateRepository, type PlatformRepoTemplateResolver, } from './repo-template.js';
6
6
  export { platformFilesystemProvider, platformSandboxProvider } from './provider.js';
7
7
  export { execViaPrivateNetwork, PrivateNetExecHttpError, type PrivateNetExecOptions, type PrivateNetExecResult, type PrivateNetFetch, } from './private-net-exec.js';
8
8
  export { InProcessSandboxAddressRegistry } from './address-registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import nodePath from "path";
3
3
  import { FileExistsError, FileNotFoundError, MastraFilesystem, MastraSandbox, ProcessHandle, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, WorkspaceReadOnlyError } from "@mastra/core/workspace";
4
4
  import { CommandExitError, Sandbox, TimeoutError } from "e2b";
5
5
  import { execFile } from "child_process";
6
- import { promisify } from "util";
7
6
  import { createHash } from "crypto";
7
+ import { promisify } from "util";
8
8
  //#region src/client.ts
9
9
  const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
10
10
  const REGIONAL_PROXY_URLS = {
@@ -1052,9 +1052,9 @@ var SandboxDestroyedError = class extends Error {
1052
1052
  * sandbox.
1053
1053
  */
1054
1054
  function buildCommand(command, args) {
1055
- return args?.length ? `${command} ${args.map(shellQuote$1).join(" ")}` : command;
1055
+ return args?.length ? `${command} ${args.map(shellQuote$2).join(" ")}` : command;
1056
1056
  }
1057
- function shellQuote$1(arg) {
1057
+ function shellQuote$2(arg) {
1058
1058
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
1059
1059
  return `'${arg.replace(/'/g, `'\\''`)}'`;
1060
1060
  }
@@ -1127,6 +1127,16 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1127
1127
  * on a later start.
1128
1128
  */
1129
1129
  templatePending;
1130
+ /**
1131
+ * The platform's id for the provisioned sandbox, as returned by the create
1132
+ * or reattach response. Persist this to reattach later via the `sandboxId`
1133
+ * option. Undefined until the sandbox has been started in this process; a
1134
+ * `sandboxId` passed to the constructor is only a hint for `start()` and is
1135
+ * not reported here until the proxy confirms it.
1136
+ */
1137
+ get sandboxId() {
1138
+ return this.status === "running" ? this._sandboxId : void 0;
1139
+ }
1130
1140
  _client;
1131
1141
  _usesProviderRoutes;
1132
1142
  _environmentId;
@@ -1311,7 +1321,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1311
1321
  }
1312
1322
  this._sandboxId = void 0;
1313
1323
  } catch (error) {
1314
- if (!(error instanceof PlatformApiError) || error.status !== 404) throw error;
1324
+ if (!(error instanceof PlatformApiError) || error.status !== 404 && error.status !== 410) throw error;
1315
1325
  this._sandboxId = void 0;
1316
1326
  }
1317
1327
  if (!this._environmentId) throw new Error("environmentId is required");
@@ -1494,23 +1504,28 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1494
1504
  await Promise.race([this._transportReadyPromise, new Promise((r) => setTimeout(r, TRANSPORT_READY_WAIT_MS))]);
1495
1505
  }
1496
1506
  /**
1497
- * Stop the sandbox while **preserving its recovery checkpoint**.
1507
+ * Stop this process's use of the sandbox without touching the remote one.
1498
1508
  *
1499
- * Semantic parity with `@mastra/railway` `RailwaySandbox.stop()`: the VM
1500
- * is released but the on-provider checkpoint survives, so a subsequent
1501
- * `start()` on a sandbox constructed with the same `id` can restore from
1502
- * it. Any in-flight capture is awaited first so the preserved checkpoint
1503
- * reflects the latest disk state we asked for.
1509
+ * Nothing is sent to workspace-proxy. The VM keeps running and idles out
1510
+ * under the environment's own idle policy, the recovery checkpoint stays,
1511
+ * and `_sandboxId` is kept so a later `start()` on this instance, or on a
1512
+ * new instance constructed with the same `id` or `sandboxId`, reattaches
1513
+ * instead of provisioning. `Mastra.shutdown()` stops every registered
1514
+ * workspace on SIGINT; releasing the VM here would destroy every live
1515
+ * session on a host restart. Use {@link destroy} to release the VM and
1516
+ * its checkpoint.
1504
1517
  *
1505
- * Corresponds to `DELETE /v1/:provider/projects/:pid/sandbox/:sandboxId` on
1506
- * workspace-proxy, which by contract does not touch the checkpoint. Use
1507
- * {@link destroy} when you want the checkpoint released too.
1518
+ * Any in-flight capture is awaited first so the checkpoint reflects the
1519
+ * latest disk state the caller asked for.
1508
1520
  */
1509
1521
  async stop() {
1510
1522
  if (this._captureInFlight) await this._captureInFlight.catch((error) => {
1511
- this.logger.warn(`stop(): failed to flush in-flight capture before teardown:`, error);
1523
+ this.logger.warn(`stop(): failed to flush in-flight capture before stop:`, error);
1512
1524
  });
1513
- await this._teardownSandbox();
1525
+ this._probeGeneration++;
1526
+ this._probeTarget = null;
1527
+ this._transportReadyPromise = null;
1528
+ this._lease = null;
1514
1529
  }
1515
1530
  /**
1516
1531
  * Destroy the sandbox **and release its recovery checkpoint**.
@@ -1547,12 +1562,9 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1547
1562
  /**
1548
1563
  * Release the remote sandbox VM and clear the local state pointing at it.
1549
1564
  *
1550
- * Shared body of {@link stop} and {@link destroy} — both funnel through
1551
- * here after they've dealt with the checkpoint (preserve vs release).
1552
- * The VM DELETE is safe to issue in either mode: the proxy's DELETE
1553
- * route does not touch the checkpoint on its own, so `stop()` correctly
1554
- * leaves the checkpoint intact and `destroy()` has already removed it
1555
- * before this call.
1565
+ * Body of {@link destroy}, called after the checkpoint has been released.
1566
+ * The proxy's DELETE route releases the VM (on E2B that kills it), so
1567
+ * only destroy() may reach here; stop() never does.
1556
1568
  */
1557
1569
  async _teardownSandbox() {
1558
1570
  if (!this._sandboxId) return;
@@ -1931,7 +1943,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1931
1943
  }
1932
1944
  }
1933
1945
  async getInfo() {
1934
- if (!this._sandboxId) return {
1946
+ if (!this._sandboxId || this.status !== "running") return {
1935
1947
  id: this.id,
1936
1948
  name: this.name,
1937
1949
  provider: this.provider,
@@ -1972,6 +1984,8 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1972
1984
  };
1973
1985
  //#endregion
1974
1986
  //#region ../../packages/_internals/workspace/dist/index.js
1987
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
1988
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
1975
1989
  /**
1976
1990
  * Setup completion marker shared by repo templates and their consumers.
1977
1991
  *
@@ -1981,8 +1995,29 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1981
1995
  * booted from the image tell whether the setup it is about to run already
1982
1996
  * happened. Relative to the template's build cwd, which is also the runtime
1983
1997
  * working directory the repo was cloned into.
1998
+ *
1999
+ * This is the single-repository marker. Templates built from a repository
2000
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
1984
2001
  */
1985
2002
  const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
2003
+ /**
2004
+ * Marker for the workspace-level setup of a multi-repository template: the
2005
+ * commands that ran at the working directory after every repository was set
2006
+ * up. Written even when there were no such commands, so a consumer can tell
2007
+ * "ran nothing" from "never built".
2008
+ */
2009
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
2010
+ /**
2011
+ * One line per repository directory whose setup failed while the build was
2012
+ * told to continue. Absent when nothing failed. Consumers check this before
2013
+ * trusting a per-repository marker: that marker means every step ran, not
2014
+ * that every step succeeded.
2015
+ */
2016
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
2017
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
2018
+ function repoSetupMarkerPath(repoDir) {
2019
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
2020
+ }
1986
2021
  /** Blank entries never become build steps, so they never count toward the digest either. */
1987
2022
  function normalizeSetupCommands(setupCommand) {
1988
2023
  return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
@@ -1992,8 +2027,26 @@ function setupMarkerContent(setupCommand) {
1992
2027
  return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
1993
2028
  }
1994
2029
  /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1995
- function setupMarkerCommand(content) {
1996
- return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
2030
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
2031
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
2032
+ }
2033
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
2034
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
2035
+ /**
2036
+ * Shell step that runs one setup command inside a repository clone.
2037
+ *
2038
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
2039
+ * the `cd` sits inside the parenthesized group so that the failure branch
2040
+ * runs back at the build cwd and the failure list lands at the workspace
2041
+ * level, never inside the repository.
2042
+ */
2043
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
2044
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
2045
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
2046
+ 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}"; }`;
2047
+ }
2048
+ function shellQuote$1(value) {
2049
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1997
2050
  }
1998
2051
  function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1999
2052
  return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
@@ -2046,77 +2099,126 @@ function rememberHead(cloneUrl, sha) {
2046
2099
  * requested size, and the caller's runtime setup materializes the checkout.
2047
2100
  */
2048
2101
  function createRepoTemplate(options) {
2049
- const getRepositoryAccess = options.getRepositoryAccess;
2102
+ if (options.getRepositoryAccess && options.repos) throw new TypeError("createRepoTemplate: pass either getRepositoryAccess or repos, not both");
2103
+ if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
2104
+ const isList = options.repos !== void 0;
2105
+ const entries = options.repos ? options.repos : options.getRepositoryAccess ? [{
2106
+ getRepositoryAccess: options.getRepositoryAccess,
2107
+ setupCommand: options.setupCommand
2108
+ }] : [];
2050
2109
  const resourcesOnly = () => {
2051
2110
  if (options.cpuCount === void 0 && options.memoryMB === void 0) return void 0;
2052
2111
  return withResources(Template(), options);
2053
2112
  };
2054
- if (!getRepositoryAccess) {
2113
+ if (entries.length === 0) {
2055
2114
  const template = resourcesOnly();
2056
2115
  return template ? async () => template : void 0;
2057
2116
  }
2058
2117
  const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
2118
+ const continueOnFailure = options.continueOnSetupFailure ?? false;
2059
2119
  return async () => {
2060
- let accessError;
2061
- const access = await getRepositoryAccess().catch((error) => {
2062
- accessError = error;
2063
- });
2064
- if (!access?.cloneUrl) {
2065
- console.warn("[platform-workspace] repo template skipped: repository access unavailable", { error: redactSecrets(accessError) });
2066
- return resourcesOnly();
2067
- }
2068
- const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2069
- if (!isValidCloneUrl(cloneUrl)) {
2070
- console.warn("[platform-workspace] repo template skipped: clone URL failed validation", { cloneUrl: redactSecrets(cloneUrl) });
2071
- return resourcesOnly();
2072
- }
2073
- const token = access.authorization?.token;
2074
- let headError;
2075
- const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2076
- headError = error;
2077
- });
2078
- let sha;
2079
- if (resolved && SHA_PATTERN.test(resolved)) {
2080
- sha = resolved;
2081
- rememberHead(cloneUrl, sha);
2082
- } else {
2083
- const lastKnown = lastKnownHeads.get(cloneUrl);
2084
- if (!lastKnown) {
2085
- console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2120
+ const repos = [];
2121
+ for (const [index, entry] of entries.entries()) {
2122
+ let accessError;
2123
+ const access = await entry.getRepositoryAccess().catch((error) => {
2124
+ accessError = error;
2125
+ });
2126
+ if (!access?.cloneUrl) {
2127
+ console.warn("[platform-workspace] repo template skipped: repository access unavailable", {
2128
+ ...isList ? { index } : {},
2129
+ error: redactSecrets(accessError)
2130
+ });
2131
+ return resourcesOnly();
2132
+ }
2133
+ const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2134
+ if (!isValidCloneUrl(cloneUrl)) {
2135
+ console.warn("[platform-workspace] repo template skipped: clone URL failed validation", {
2136
+ ...isList ? { index } : {},
2137
+ cloneUrl: redactSecrets(cloneUrl)
2138
+ });
2139
+ return resourcesOnly();
2140
+ }
2141
+ const token = access.authorization?.token;
2142
+ let headError;
2143
+ const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2144
+ headError = error;
2145
+ });
2146
+ let sha;
2147
+ if (resolved && SHA_PATTERN.test(resolved)) {
2148
+ sha = resolved;
2149
+ rememberHead(cloneUrl, sha);
2150
+ } else {
2151
+ const lastKnown = lastKnownHeads.get(cloneUrl);
2152
+ if (lastKnown) {
2153
+ console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2154
+ cloneUrl,
2155
+ sha: lastKnown,
2156
+ resolved,
2157
+ error: redactSecrets(headError)
2158
+ });
2159
+ sha = lastKnown;
2160
+ } else if (isList) console.warn("[platform-workspace] repo template clones unpinned: could not resolve default-branch head", {
2086
2161
  cloneUrl,
2087
2162
  sha: resolved,
2088
2163
  error: redactSecrets(headError)
2089
2164
  });
2090
- return resourcesOnly();
2165
+ else {
2166
+ console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2167
+ cloneUrl,
2168
+ sha: resolved,
2169
+ error: redactSecrets(headError)
2170
+ });
2171
+ return resourcesOnly();
2172
+ }
2091
2173
  }
2092
- console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2174
+ const repoDir = repoDirName(cloneUrl);
2175
+ repos.push({
2176
+ index,
2093
2177
  cloneUrl,
2094
- sha: lastKnown,
2095
- resolved,
2096
- error: redactSecrets(headError)
2178
+ repoDir,
2179
+ token,
2180
+ tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
2181
+ sha,
2182
+ setupCommands: normalizeSetupCommands(entry.setupCommand)
2097
2183
  });
2098
- sha = lastKnown;
2184
+ }
2185
+ const seenDirs = /* @__PURE__ */ new Map();
2186
+ for (const repo of repos) {
2187
+ const other = seenDirs.get(repo.repoDir);
2188
+ if (other) throw new Error(`createRepoTemplate: repositories ${other} and ${repo.cloneUrl} would both clone into "${repo.repoDir}"`);
2189
+ seenDirs.set(repo.repoDir, repo.cloneUrl);
2099
2190
  }
2100
2191
  const workingDirectory = options.workingDirectory === void 0 ? void 0 : trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory));
2101
- const repoDir = repoDirName(cloneUrl);
2102
- const auth = token ? `${gitAuthFlag()} ` : "";
2103
- const setupCommands = (options.setupCommand === void 0 ? [] : Array.isArray(options.setupCommand) ? options.setupCommand : [options.setupCommand]).filter((command) => command.trim() !== "");
2104
- const family = `repo:${cloneUrl}:${workingDirectory ?? ""}/${repoDir}`;
2192
+ const family = isList ? `repos:${createHash("sha256").update(JSON.stringify([workingDirectory ?? "", repos.map((repo) => [repo.cloneUrl, repo.repoDir])])).digest("hex").slice(0, 32)}` : `repo:${repos[0].cloneUrl}:${workingDirectory ?? ""}/${repos[0].repoDir}`;
2105
2193
  let template = Template();
2106
- const buildEnv = {
2107
- ...options.buildEnv,
2108
- ...token ? { [BUILD_TOKEN_ENV]: token } : {}
2109
- };
2194
+ const buildEnv = { ...options.buildEnv };
2195
+ for (const repo of repos) if (repo.tokenEnv && repo.token) buildEnv[repo.tokenEnv] = repo.token;
2110
2196
  if (Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv, { ephemeral: true });
2111
2197
  template = withResources(template, options);
2112
2198
  if (workingDirectory) template = template.runCmd(`mkdir -p "${workingDirectory}"`).setWorkdir(workingDirectory);
2113
- template = template.runCmd(repoCloneCommand({
2114
- cloneUrl,
2115
- destination: repoDir,
2116
- ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
2117
- })).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2118
- for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
2119
- template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
2199
+ const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
2200
+ for (const repo of ordered) {
2201
+ const { repoDir, sha, tokenEnv, setupCommands } = repo;
2202
+ const auth = tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : "";
2203
+ template = template.runCmd(repoCloneCommand({
2204
+ cloneUrl: repo.cloneUrl,
2205
+ destination: repoDir,
2206
+ ...tokenEnv ? { tokenEnv } : {}
2207
+ }));
2208
+ if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2209
+ for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
2210
+ repoDir,
2211
+ command,
2212
+ continueOnFailure
2213
+ }));
2214
+ const content = setupMarkerContent(setupCommands);
2215
+ template = template.runCmd(isList ? setupMarkerCommand(content, repoSetupMarkerPath(repoDir)) : setupMarkerCommand(content));
2216
+ }
2217
+ if (isList) {
2218
+ const workspaceCommands = normalizeSetupCommands(options.workspaceSetupCommand);
2219
+ for (const command of workspaceCommands) template = template.runCmd(command);
2220
+ template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
2221
+ }
2120
2222
  return template.withFamily(family);
2121
2223
  };
2122
2224
  }
@@ -2171,8 +2273,8 @@ function redactSecrets(value) {
2171
2273
  if (value === void 0 || value === null) return void 0;
2172
2274
  return (value instanceof Error ? value.message : typeof value === "string" ? value : String(value)).replace(/\/\/[^/@\s]+@/g, "//***@").replace(/\b(bearer|basic)\s+[^\s"']+/gi, "$1 ***").replace(/\b(gh[pousr]_|github_pat_)[A-Za-z0-9_]+/g, "$1***");
2173
2275
  }
2174
- function gitAuthFlag() {
2175
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2276
+ function gitAuthFlag(tokenEnv) {
2277
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
2176
2278
  }
2177
2279
  /**
2178
2280
  * `owner/repo` for a github.com clone URL, else undefined. Only the public