@mastra/docker 0.9.3 → 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/README.md +2 -0
- package/dist/index.cjs +192 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +192 -40
- package/dist/index.js.map +1 -1
- package/dist/template/dockerfile.d.ts +7 -0
- package/dist/template/dockerfile.d.ts.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 +85 -4
- package/dist/template/repo-template.d.ts.map +1 -1
- package/dist/template/template.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ const sandbox = new DockerSandbox({
|
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
If `baseImage` runs as a non-root `USER`, set `owner` to that user (for example `owner: 'node'` or `owner: '1000:1000'`). The checkout is then copied with `COPY --chown`, so git and setup commands can write to it. Without `owner`, the checkout is owned by root.
|
|
173
|
+
|
|
172
174
|
## Documentation
|
|
173
175
|
|
|
174
176
|
- [Docker Sandbox integration guide](https://mastra.ai/integrations/sandboxes/docker)
|
package/dist/index.cjs
CHANGED
|
@@ -1600,6 +1600,11 @@ function renderSecretRun(command, secrets) {
|
|
|
1600
1600
|
const prefix = names.length > 0 ? [`export ${exports.join(" ")}`] : [];
|
|
1601
1601
|
return `RUN ${[...mounts, ""].join(" ")}${[...prefix, ...toCommandList(command)].join(" && ")}`;
|
|
1602
1602
|
}
|
|
1603
|
+
const OWNER_PATTERN = /^[A-Za-z0-9_.-]+(:[A-Za-z0-9_.-]+)?$/;
|
|
1604
|
+
/** Throws unless `owner` is a `user[:group]` or `uid[:gid]` usable in `COPY --chown`. */
|
|
1605
|
+
function assertValidOwner(owner) {
|
|
1606
|
+
if (typeof owner !== "string" || !OWNER_PATTERN.test(owner)) throw new TypeError(`owner must be user[:group] or uid[:gid], got ${JSON.stringify(owner)}`);
|
|
1607
|
+
}
|
|
1603
1608
|
function synthesizeDockerfile(definition) {
|
|
1604
1609
|
const lines = [];
|
|
1605
1610
|
let mainIndex = 0;
|
|
@@ -1621,13 +1626,14 @@ function synthesizeDockerfile(definition) {
|
|
|
1621
1626
|
lines.push(`RUN ${toCommandList(operation.args[0]).join(" && ")}`);
|
|
1622
1627
|
break;
|
|
1623
1628
|
case "runWithSecrets": {
|
|
1624
|
-
const [command, { secrets, output }] = operation.args;
|
|
1629
|
+
const [command, { secrets, output, owner }] = operation.args;
|
|
1630
|
+
if (owner !== void 0) assertValidOwner(owner);
|
|
1625
1631
|
const snapshot = mainStageName(mainIndex);
|
|
1626
1632
|
const stage = secretStageName(index);
|
|
1627
1633
|
lines.push(`FROM ${snapshot} AS ${stage}`, renderSecretRun(command, secrets));
|
|
1628
1634
|
mainIndex += 1;
|
|
1629
1635
|
openMain(snapshot);
|
|
1630
|
-
lines.push(`COPY --from=${stage} ${output} ${output}`);
|
|
1636
|
+
lines.push(`COPY ${owner !== void 0 ? `--chown=${owner} ` : ""}--from=${stage} ${output} ${output}`);
|
|
1631
1637
|
break;
|
|
1632
1638
|
}
|
|
1633
1639
|
case "aptInstall":
|
|
@@ -1658,12 +1664,13 @@ function canonicalOperation(operation) {
|
|
|
1658
1664
|
args: [sortedEntries(operation.args[0])]
|
|
1659
1665
|
};
|
|
1660
1666
|
case "runWithSecrets": {
|
|
1661
|
-
const [command, { secrets, output }] = operation.args;
|
|
1667
|
+
const [command, { secrets, output, owner }] = operation.args;
|
|
1662
1668
|
return {
|
|
1663
1669
|
method: "runWithSecrets",
|
|
1664
1670
|
args: [command, {
|
|
1665
1671
|
secrets: [...secrets].sort(),
|
|
1666
|
-
output
|
|
1672
|
+
output,
|
|
1673
|
+
...owner !== void 0 ? { owner } : {}
|
|
1667
1674
|
}]
|
|
1668
1675
|
};
|
|
1669
1676
|
}
|
|
@@ -1809,11 +1816,17 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1809
1816
|
});
|
|
1810
1817
|
const output = validateString(options.output, "output");
|
|
1811
1818
|
if (!output.startsWith("/")) throw new TypeError("output must be an absolute path");
|
|
1819
|
+
let owner;
|
|
1820
|
+
if (options.owner !== void 0) {
|
|
1821
|
+
owner = validateString(options.owner, "owner");
|
|
1822
|
+
assertValidOwner(owner);
|
|
1823
|
+
}
|
|
1812
1824
|
return this.#append({
|
|
1813
1825
|
method: "runWithSecrets",
|
|
1814
1826
|
args: [validateStringOrStrings(command, "command"), {
|
|
1815
1827
|
secrets,
|
|
1816
|
-
output
|
|
1828
|
+
output,
|
|
1829
|
+
...owner !== void 0 ? { owner } : {}
|
|
1817
1830
|
}]
|
|
1818
1831
|
});
|
|
1819
1832
|
}
|
|
@@ -2161,6 +2174,8 @@ function isImageNotFoundError(error) {
|
|
|
2161
2174
|
}
|
|
2162
2175
|
//#endregion
|
|
2163
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";
|
|
2164
2179
|
/**
|
|
2165
2180
|
* Setup completion marker shared by repo templates and their consumers.
|
|
2166
2181
|
*
|
|
@@ -2170,8 +2185,29 @@ function isImageNotFoundError(error) {
|
|
|
2170
2185
|
* booted from the image tell whether the setup it is about to run already
|
|
2171
2186
|
* happened. Relative to the template's build cwd, which is also the runtime
|
|
2172
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}).
|
|
2173
2191
|
*/
|
|
2174
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
|
+
}
|
|
2175
2211
|
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
2176
2212
|
function normalizeSetupCommands(setupCommand) {
|
|
2177
2213
|
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
@@ -2181,8 +2217,26 @@ function setupMarkerContent(setupCommand) {
|
|
|
2181
2217
|
return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
|
|
2182
2218
|
}
|
|
2183
2219
|
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
2184
|
-
function setupMarkerCommand(content) {
|
|
2185
|
-
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, `'\\''`)}'`;
|
|
2186
2240
|
}
|
|
2187
2241
|
//#endregion
|
|
2188
2242
|
//#region src/template/repo-template.ts
|
|
@@ -2198,7 +2252,11 @@ function setupMarkerCommand(content) {
|
|
|
2198
2252
|
* the template identity. A moved branch therefore yields a fresh image on the
|
|
2199
2253
|
* next new sandbox, and an unmoved one reuses the cached image. When the head
|
|
2200
2254
|
* cannot be resolved the resolver rejects: an unpinned clone cached under a
|
|
2201
|
-
* 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.
|
|
2202
2260
|
*
|
|
2203
2261
|
* The clone runs in a throwaway build stage; the credential is passed by value
|
|
2204
2262
|
* to that stage only and never enters the template identity or the image.
|
|
@@ -2214,38 +2272,67 @@ function setupMarkerCommand(content) {
|
|
|
2214
2272
|
* ```
|
|
2215
2273
|
*/
|
|
2216
2274
|
const execFileAsync = (0, util.promisify)(child_process.execFile);
|
|
2217
|
-
/** Env var the build's clone reads the credential from (see `
|
|
2275
|
+
/** Env var the build's clone reads the credential from (see `cloneAndPin`). */
|
|
2218
2276
|
const BUILD_TOKEN_ENV = "GH_TOKEN";
|
|
2219
2277
|
const DEFAULT_BASE_IMAGE = "node:22-slim";
|
|
2220
2278
|
const DEFAULT_WORKING_DIRECTORY = "/workspace";
|
|
2221
2279
|
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2222
|
-
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2223
2280
|
const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
|
|
2224
2281
|
const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
|
|
2225
2282
|
const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
|
|
2226
2283
|
/** Refs interpolate into shell too; git ref names are already restricted, so allowlist tightly. */
|
|
2227
2284
|
const REF_PATTERN = /^[\w./-]+$/;
|
|
2228
2285
|
function createDockerRepoTemplate(options) {
|
|
2229
|
-
if (
|
|
2230
|
-
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");
|
|
2231
2299
|
const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
|
|
2232
2300
|
if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
|
|
2233
|
-
return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
|
|
2301
|
+
return (resolveOptions) => resolveRepoTemplate(options, entries, workingDirectory, resolveOptions);
|
|
2234
2302
|
}
|
|
2235
|
-
async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
|
|
2303
|
+
async function resolveRepoTemplate(options, entries, workingDirectory, resolveOptions = {}) {
|
|
2236
2304
|
const { abortSignal } = resolveOptions;
|
|
2305
|
+
const isList = options.repos !== void 0;
|
|
2237
2306
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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
|
+
});
|
|
2243
2335
|
}
|
|
2244
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2245
|
-
const cloneUrl = access?.cloneUrl;
|
|
2246
|
-
if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
|
|
2247
|
-
assertCloneUrl(cloneUrl);
|
|
2248
|
-
const token = access?.authorization?.token;
|
|
2249
2336
|
let buildEnv;
|
|
2250
2337
|
try {
|
|
2251
2338
|
buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
|
|
@@ -2253,18 +2340,28 @@ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {
|
|
|
2253
2340
|
throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2254
2341
|
}
|
|
2255
2342
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2256
|
-
const
|
|
2257
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2258
|
-
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`);
|
|
2259
|
-
return buildRepoTemplate({
|
|
2260
|
-
cloneUrl,
|
|
2261
|
-
sha,
|
|
2262
|
-
token,
|
|
2343
|
+
const shared = {
|
|
2263
2344
|
buildEnv,
|
|
2264
|
-
setupCommand: options.setupCommand,
|
|
2265
2345
|
workingDirectory,
|
|
2266
2346
|
baseImage: options.baseImage,
|
|
2347
|
+
owner: options.owner,
|
|
2267
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
|
|
2268
2365
|
});
|
|
2269
2366
|
}
|
|
2270
2367
|
/**
|
|
@@ -2283,14 +2380,15 @@ function buildRepoTemplate(inputs) {
|
|
|
2283
2380
|
if (inputs.baseImage === void 0) template = template.aptInstall(["git", "ca-certificates"]);
|
|
2284
2381
|
if (buildEnv && Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv);
|
|
2285
2382
|
const tokenEnv = token ? BUILD_TOKEN_ENV : void 0;
|
|
2286
|
-
|
|
2383
|
+
template = template.runWithSecrets(cloneAndPin({
|
|
2287
2384
|
cloneUrl,
|
|
2288
2385
|
destination,
|
|
2289
|
-
tokenEnv
|
|
2290
|
-
|
|
2291
|
-
|
|
2386
|
+
tokenEnv,
|
|
2387
|
+
sha
|
|
2388
|
+
}), {
|
|
2292
2389
|
secrets: tokenEnv ? [tokenEnv] : [],
|
|
2293
|
-
output: destination
|
|
2390
|
+
output: destination,
|
|
2391
|
+
...inputs.owner !== void 0 ? { owner: inputs.owner } : {}
|
|
2294
2392
|
}).setWorkdir(destination);
|
|
2295
2393
|
const setupCommands = normalizeSetupCommands(inputs.setupCommand);
|
|
2296
2394
|
for (const command of setupCommands) template = template.runCmd(command);
|
|
@@ -2298,6 +2396,60 @@ function buildRepoTemplate(inputs) {
|
|
|
2298
2396
|
return template;
|
|
2299
2397
|
}
|
|
2300
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
|
+
/**
|
|
2301
2453
|
* Resolve `ref` (or the default branch) to a commit sha with `git ls-remote`
|
|
2302
2454
|
* on the host, without cloning. A full sha is returned as is. Any failure
|
|
2303
2455
|
* yields undefined; the caller decides how to surface it.
|
|
@@ -2328,14 +2480,14 @@ async function resolveHead(cloneUrl, ref, token, abortSignal) {
|
|
|
2328
2480
|
});
|
|
2329
2481
|
const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
|
|
2330
2482
|
const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
|
|
2331
|
-
return sha &&
|
|
2483
|
+
return sha && FULL_SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
|
|
2332
2484
|
} catch (error) {
|
|
2333
2485
|
if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2334
2486
|
return;
|
|
2335
2487
|
}
|
|
2336
2488
|
}
|
|
2337
|
-
function
|
|
2338
|
-
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)}`];
|
|
2339
2491
|
}
|
|
2340
2492
|
function assertCloneUrl(cloneUrl) {
|
|
2341
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`);
|