@mastra/docker 0.9.4-alpha.0 → 0.10.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.cjs +171 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +171 -34
- package/dist/index.js.map +1 -1
- package/dist/template/index.d.ts +1 -1
- package/dist/template/index.d.ts.map +1 -1
- package/dist/template/repo-template.d.ts +78 -4
- package/dist/template/repo-template.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -2174,6 +2174,8 @@ function isImageNotFoundError(error) {
|
|
|
2174
2174
|
}
|
|
2175
2175
|
//#endregion
|
|
2176
2176
|
//#region ../../packages/_internals/workspace/dist/index.js
|
|
2177
|
+
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
2178
|
+
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
2177
2179
|
/**
|
|
2178
2180
|
* Setup completion marker shared by repo templates and their consumers.
|
|
2179
2181
|
*
|
|
@@ -2183,8 +2185,29 @@ function isImageNotFoundError(error) {
|
|
|
2183
2185
|
* booted from the image tell whether the setup it is about to run already
|
|
2184
2186
|
* happened. Relative to the template's build cwd, which is also the runtime
|
|
2185
2187
|
* working directory the repo was cloned into.
|
|
2188
|
+
*
|
|
2189
|
+
* This is the single-repository marker. Templates built from a repository
|
|
2190
|
+
* list write one marker per repository instead ({@link repoSetupMarkerPath}).
|
|
2186
2191
|
*/
|
|
2187
2192
|
const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
|
|
2193
|
+
/**
|
|
2194
|
+
* Marker for the workspace-level setup of a multi-repository template: the
|
|
2195
|
+
* commands that ran at the working directory after every repository was set
|
|
2196
|
+
* up. Written even when there were no such commands, so a consumer can tell
|
|
2197
|
+
* "ran nothing" from "never built".
|
|
2198
|
+
*/
|
|
2199
|
+
const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
|
|
2200
|
+
/**
|
|
2201
|
+
* One line per repository directory whose setup failed while the build was
|
|
2202
|
+
* told to continue. Absent when nothing failed. Consumers check this before
|
|
2203
|
+
* trusting a per-repository marker: that marker means every step ran, not
|
|
2204
|
+
* that every step succeeded.
|
|
2205
|
+
*/
|
|
2206
|
+
const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
|
|
2207
|
+
/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
|
|
2208
|
+
function repoSetupMarkerPath(repoDir) {
|
|
2209
|
+
return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
|
|
2210
|
+
}
|
|
2188
2211
|
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
2189
2212
|
function normalizeSetupCommands(setupCommand) {
|
|
2190
2213
|
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
@@ -2194,8 +2217,26 @@ function setupMarkerContent(setupCommand) {
|
|
|
2194
2217
|
return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
|
|
2195
2218
|
}
|
|
2196
2219
|
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
2197
|
-
function setupMarkerCommand(content) {
|
|
2198
|
-
return `mkdir -p "$(dirname "${
|
|
2220
|
+
function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
|
|
2221
|
+
return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
|
|
2222
|
+
}
|
|
2223
|
+
/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
|
|
2224
|
+
const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
2225
|
+
/**
|
|
2226
|
+
* Shell step that runs one setup command inside a repository clone.
|
|
2227
|
+
*
|
|
2228
|
+
* Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
|
|
2229
|
+
* the `cd` sits inside the parenthesized group so that the failure branch
|
|
2230
|
+
* runs back at the build cwd and the failure list lands at the workspace
|
|
2231
|
+
* level, never inside the repository.
|
|
2232
|
+
*/
|
|
2233
|
+
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
2234
|
+
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
2235
|
+
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
2236
|
+
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}"; }`;
|
|
2237
|
+
}
|
|
2238
|
+
function shellQuote$1(value) {
|
|
2239
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2199
2240
|
}
|
|
2200
2241
|
//#endregion
|
|
2201
2242
|
//#region src/template/repo-template.ts
|
|
@@ -2211,7 +2252,11 @@ function setupMarkerCommand(content) {
|
|
|
2211
2252
|
* the template identity. A moved branch therefore yields a fresh image on the
|
|
2212
2253
|
* next new sandbox, and an unmoved one reuses the cached image. When the head
|
|
2213
2254
|
* cannot be resolved the resolver rejects: an unpinned clone cached under a
|
|
2214
|
-
* stable tag would otherwise serve stale repository state forever.
|
|
2255
|
+
* stable tag would otherwise serve stale repository state forever. This holds
|
|
2256
|
+
* for every entry of the `repos` form too, where the platform and E2B
|
|
2257
|
+
* templates clone such a repository unpinned instead: those use a shallow
|
|
2258
|
+
* clone whose tip the session fetches past, while this template clones full
|
|
2259
|
+
* history and pins by `checkout`, so there is no cheap unpinned mode here.
|
|
2215
2260
|
*
|
|
2216
2261
|
* The clone runs in a throwaway build stage; the credential is passed by value
|
|
2217
2262
|
* to that stage only and never enters the template identity or the image.
|
|
@@ -2227,38 +2272,67 @@ function setupMarkerCommand(content) {
|
|
|
2227
2272
|
* ```
|
|
2228
2273
|
*/
|
|
2229
2274
|
const execFileAsync = (0, util.promisify)(child_process.execFile);
|
|
2230
|
-
/** Env var the build's clone reads the credential from (see `
|
|
2275
|
+
/** Env var the build's clone reads the credential from (see `cloneAndPin`). */
|
|
2231
2276
|
const BUILD_TOKEN_ENV = "GH_TOKEN";
|
|
2232
2277
|
const DEFAULT_BASE_IMAGE = "node:22-slim";
|
|
2233
2278
|
const DEFAULT_WORKING_DIRECTORY = "/workspace";
|
|
2234
2279
|
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2235
|
-
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2236
2280
|
const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
|
|
2237
2281
|
const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
|
|
2238
2282
|
const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
|
|
2239
2283
|
/** Refs interpolate into shell too; git ref names are already restricted, so allowlist tightly. */
|
|
2240
2284
|
const REF_PATTERN = /^[\w./-]+$/;
|
|
2241
2285
|
function createDockerRepoTemplate(options) {
|
|
2242
|
-
if (
|
|
2243
|
-
if (options.
|
|
2286
|
+
if (options.getRepositoryAccess && options.repos) throw new TypeError("createDockerRepoTemplate: pass either getRepositoryAccess or repos, not both");
|
|
2287
|
+
if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createDockerRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
|
|
2288
|
+
if (!options.getRepositoryAccess && !options.repos?.length) return void 0;
|
|
2289
|
+
const entries = options.repos ?? [{
|
|
2290
|
+
getRepositoryAccess: options.getRepositoryAccess,
|
|
2291
|
+
ref: options.ref,
|
|
2292
|
+
setupCommand: options.setupCommand
|
|
2293
|
+
}];
|
|
2294
|
+
for (const entry of entries) {
|
|
2295
|
+
if (entry.ref !== void 0 && !REF_PATTERN.test(entry.ref)) throw new Error(`Invalid ref '${entry.ref}': expected a git ref name`);
|
|
2296
|
+
assertSingleLineCommands(entry.setupCommand, "setupCommand");
|
|
2297
|
+
}
|
|
2298
|
+
assertSingleLineCommands(options.workspaceSetupCommand, "workspaceSetupCommand");
|
|
2244
2299
|
const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
|
|
2245
2300
|
if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
|
|
2246
|
-
return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
|
|
2301
|
+
return (resolveOptions) => resolveRepoTemplate(options, entries, workingDirectory, resolveOptions);
|
|
2247
2302
|
}
|
|
2248
|
-
async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
|
|
2303
|
+
async function resolveRepoTemplate(options, entries, workingDirectory, resolveOptions = {}) {
|
|
2249
2304
|
const { abortSignal } = resolveOptions;
|
|
2305
|
+
const isList = options.repos !== void 0;
|
|
2250
2306
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2307
|
+
const repos = [];
|
|
2308
|
+
const seenDirs = /* @__PURE__ */ new Map();
|
|
2309
|
+
for (const [index, entry] of entries.entries()) {
|
|
2310
|
+
let access;
|
|
2311
|
+
try {
|
|
2312
|
+
access = await entry.getRepositoryAccess(resolveOptions);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2315
|
+
}
|
|
2316
|
+
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2317
|
+
const cloneUrl = access?.cloneUrl;
|
|
2318
|
+
if (!cloneUrl) throw new Error(`Repo template has no clone URL: repository access returned none${isList ? ` (repos[${index}])` : ""}.`);
|
|
2319
|
+
assertCloneUrl(cloneUrl);
|
|
2320
|
+
const repoDir = repoDirName(cloneUrl);
|
|
2321
|
+
const previous = seenDirs.get(repoDir);
|
|
2322
|
+
if (previous) throw new Error(`createDockerRepoTemplate: repositories ${previous} and ${cloneUrl} would both clone into "${repoDir}"`);
|
|
2323
|
+
seenDirs.set(repoDir, cloneUrl);
|
|
2324
|
+
const token = access?.authorization?.token;
|
|
2325
|
+
const sha = await resolveHead(cloneUrl, entry.ref, token, abortSignal);
|
|
2326
|
+
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2327
|
+
if (!sha) throw new Error(`Could not resolve ${entry.ref ?? "HEAD"} of ${cloneUrl} with git ls-remote; check the ref, the credential and network access`);
|
|
2328
|
+
repos.push({
|
|
2329
|
+
cloneUrl,
|
|
2330
|
+
sha,
|
|
2331
|
+
token,
|
|
2332
|
+
tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
|
|
2333
|
+
setupCommand: entry.setupCommand
|
|
2334
|
+
});
|
|
2256
2335
|
}
|
|
2257
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2258
|
-
const cloneUrl = access?.cloneUrl;
|
|
2259
|
-
if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
|
|
2260
|
-
assertCloneUrl(cloneUrl);
|
|
2261
|
-
const token = access?.authorization?.token;
|
|
2262
2336
|
let buildEnv;
|
|
2263
2337
|
try {
|
|
2264
2338
|
buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
|
|
@@ -2266,19 +2340,28 @@ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {
|
|
|
2266
2340
|
throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2267
2341
|
}
|
|
2268
2342
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2269
|
-
const
|
|
2270
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2271
|
-
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`);
|
|
2272
|
-
return buildRepoTemplate({
|
|
2273
|
-
cloneUrl,
|
|
2274
|
-
sha,
|
|
2275
|
-
token,
|
|
2343
|
+
const shared = {
|
|
2276
2344
|
buildEnv,
|
|
2277
|
-
setupCommand: options.setupCommand,
|
|
2278
2345
|
workingDirectory,
|
|
2279
2346
|
baseImage: options.baseImage,
|
|
2280
2347
|
owner: options.owner,
|
|
2281
2348
|
dockerOptions: options.dockerOptions
|
|
2349
|
+
};
|
|
2350
|
+
if (!isList) {
|
|
2351
|
+
const [repo] = repos;
|
|
2352
|
+
return buildRepoTemplate({
|
|
2353
|
+
...shared,
|
|
2354
|
+
cloneUrl: repo.cloneUrl,
|
|
2355
|
+
sha: repo.sha,
|
|
2356
|
+
token: repo.token,
|
|
2357
|
+
setupCommand: repo.setupCommand
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
return buildMultiRepoTemplate({
|
|
2361
|
+
...shared,
|
|
2362
|
+
repos,
|
|
2363
|
+
workspaceSetupCommand: options.workspaceSetupCommand,
|
|
2364
|
+
continueOnSetupFailure: options.continueOnSetupFailure ?? false
|
|
2282
2365
|
});
|
|
2283
2366
|
}
|
|
2284
2367
|
/**
|
|
@@ -2297,12 +2380,12 @@ function buildRepoTemplate(inputs) {
|
|
|
2297
2380
|
if (inputs.baseImage === void 0) template = template.aptInstall(["git", "ca-certificates"]);
|
|
2298
2381
|
if (buildEnv && Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv);
|
|
2299
2382
|
const tokenEnv = token ? BUILD_TOKEN_ENV : void 0;
|
|
2300
|
-
|
|
2383
|
+
template = template.runWithSecrets(cloneAndPin({
|
|
2301
2384
|
cloneUrl,
|
|
2302
2385
|
destination,
|
|
2303
|
-
tokenEnv
|
|
2304
|
-
|
|
2305
|
-
|
|
2386
|
+
tokenEnv,
|
|
2387
|
+
sha
|
|
2388
|
+
}), {
|
|
2306
2389
|
secrets: tokenEnv ? [tokenEnv] : [],
|
|
2307
2390
|
output: destination,
|
|
2308
2391
|
...inputs.owner !== void 0 ? { owner: inputs.owner } : {}
|
|
@@ -2313,6 +2396,60 @@ function buildRepoTemplate(inputs) {
|
|
|
2313
2396
|
return template;
|
|
2314
2397
|
}
|
|
2315
2398
|
/**
|
|
2399
|
+
* List-form assembly: the working directory is the cwd, every repository is
|
|
2400
|
+
* laid out under it (public first) around its own pin, then the workspace
|
|
2401
|
+
* steps and marker. Exported for tests.
|
|
2402
|
+
* @internal
|
|
2403
|
+
*/
|
|
2404
|
+
function buildMultiRepoTemplate(inputs) {
|
|
2405
|
+
const { repos, buildEnv, continueOnSetupFailure } = inputs;
|
|
2406
|
+
const workingDirectory = trimTrailingSlashes(inputs.workingDirectory);
|
|
2407
|
+
const secretValues = {};
|
|
2408
|
+
for (const repo of repos) if (repo.tokenEnv && repo.token) secretValues[repo.tokenEnv] = repo.token;
|
|
2409
|
+
let template = new DockerTemplate({
|
|
2410
|
+
baseImage: inputs.baseImage ?? DEFAULT_BASE_IMAGE,
|
|
2411
|
+
dockerOptions: inputs.dockerOptions,
|
|
2412
|
+
...Object.keys(secretValues).length > 0 ? { secrets: secretValues } : {}
|
|
2413
|
+
});
|
|
2414
|
+
if (inputs.baseImage === void 0) template = template.aptInstall(["git", "ca-certificates"]);
|
|
2415
|
+
if (buildEnv && Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv);
|
|
2416
|
+
template = template.setWorkdir(workingDirectory);
|
|
2417
|
+
const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
|
|
2418
|
+
for (const repo of ordered) {
|
|
2419
|
+
const repoDir = repoDirName(repo.cloneUrl);
|
|
2420
|
+
const destination = `${workingDirectory}/${repoDir}`;
|
|
2421
|
+
const secrets = repo.tokenEnv ? [repo.tokenEnv] : [];
|
|
2422
|
+
const setupCommands = normalizeSetupCommands(repo.setupCommand);
|
|
2423
|
+
template = template.runWithSecrets(cloneAndPin({
|
|
2424
|
+
cloneUrl: repo.cloneUrl,
|
|
2425
|
+
destination,
|
|
2426
|
+
tokenEnv: repo.tokenEnv,
|
|
2427
|
+
sha: repo.sha
|
|
2428
|
+
}), {
|
|
2429
|
+
secrets,
|
|
2430
|
+
output: destination,
|
|
2431
|
+
...inputs.owner !== void 0 ? { owner: inputs.owner } : {}
|
|
2432
|
+
});
|
|
2433
|
+
for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
|
|
2434
|
+
repoDir,
|
|
2435
|
+
command,
|
|
2436
|
+
continueOnFailure: continueOnSetupFailure
|
|
2437
|
+
}));
|
|
2438
|
+
template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands), repoSetupMarkerPath(repoDir)));
|
|
2439
|
+
}
|
|
2440
|
+
const workspaceCommands = normalizeSetupCommands(inputs.workspaceSetupCommand);
|
|
2441
|
+
for (const command of workspaceCommands) template = template.runCmd(command);
|
|
2442
|
+
template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
|
|
2443
|
+
return template;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* A Dockerfile `RUN` only spans lines through `\\` continuation, which the
|
|
2447
|
+
* parser folds away; a bare newline would start a new instruction.
|
|
2448
|
+
*/
|
|
2449
|
+
function assertSingleLineCommands(commands, option) {
|
|
2450
|
+
for (const command of normalizeSetupCommands(commands)) if (/(?<!\\)\n|\r/.test(command)) throw new Error(`createDockerRepoTemplate: ${option} entries cannot contain a bare newline; end the line with \\ or pass separate commands`);
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2316
2453
|
* Resolve `ref` (or the default branch) to a commit sha with `git ls-remote`
|
|
2317
2454
|
* on the host, without cloning. A full sha is returned as is. Any failure
|
|
2318
2455
|
* yields undefined; the caller decides how to surface it.
|
|
@@ -2343,14 +2480,14 @@ async function resolveHead(cloneUrl, ref, token, abortSignal) {
|
|
|
2343
2480
|
});
|
|
2344
2481
|
const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
|
|
2345
2482
|
const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
|
|
2346
|
-
return sha &&
|
|
2483
|
+
return sha && FULL_SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
|
|
2347
2484
|
} catch (error) {
|
|
2348
2485
|
if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2349
2486
|
return;
|
|
2350
2487
|
}
|
|
2351
2488
|
}
|
|
2352
|
-
function
|
|
2353
|
-
return `git ${tokenEnv ? `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)" ` : ""}clone ${shellQuote(cloneUrl)} ${shellQuote(destination)}
|
|
2489
|
+
function cloneAndPin({ cloneUrl, destination, tokenEnv, sha }) {
|
|
2490
|
+
return [`git ${tokenEnv ? `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)" ` : ""}clone ${shellQuote(cloneUrl)} ${shellQuote(destination)}`, `git -C ${shellQuote(destination)} checkout --detach ${shellQuote(sha)}`];
|
|
2354
2491
|
}
|
|
2355
2492
|
function assertCloneUrl(cloneUrl) {
|
|
2356
2493
|
if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) throw new Error(`Invalid cloneUrl '${cloneUrl}': expected an https URL with a plain host and path`);
|