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