@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/dist/index.js
CHANGED
|
@@ -1575,6 +1575,11 @@ function renderSecretRun(command, secrets) {
|
|
|
1575
1575
|
const prefix = names.length > 0 ? [`export ${exports.join(" ")}`] : [];
|
|
1576
1576
|
return `RUN ${[...mounts, ""].join(" ")}${[...prefix, ...toCommandList(command)].join(" && ")}`;
|
|
1577
1577
|
}
|
|
1578
|
+
const OWNER_PATTERN = /^[A-Za-z0-9_.-]+(:[A-Za-z0-9_.-]+)?$/;
|
|
1579
|
+
/** Throws unless `owner` is a `user[:group]` or `uid[:gid]` usable in `COPY --chown`. */
|
|
1580
|
+
function assertValidOwner(owner) {
|
|
1581
|
+
if (typeof owner !== "string" || !OWNER_PATTERN.test(owner)) throw new TypeError(`owner must be user[:group] or uid[:gid], got ${JSON.stringify(owner)}`);
|
|
1582
|
+
}
|
|
1578
1583
|
function synthesizeDockerfile(definition) {
|
|
1579
1584
|
const lines = [];
|
|
1580
1585
|
let mainIndex = 0;
|
|
@@ -1596,13 +1601,14 @@ function synthesizeDockerfile(definition) {
|
|
|
1596
1601
|
lines.push(`RUN ${toCommandList(operation.args[0]).join(" && ")}`);
|
|
1597
1602
|
break;
|
|
1598
1603
|
case "runWithSecrets": {
|
|
1599
|
-
const [command, { secrets, output }] = operation.args;
|
|
1604
|
+
const [command, { secrets, output, owner }] = operation.args;
|
|
1605
|
+
if (owner !== void 0) assertValidOwner(owner);
|
|
1600
1606
|
const snapshot = mainStageName(mainIndex);
|
|
1601
1607
|
const stage = secretStageName(index);
|
|
1602
1608
|
lines.push(`FROM ${snapshot} AS ${stage}`, renderSecretRun(command, secrets));
|
|
1603
1609
|
mainIndex += 1;
|
|
1604
1610
|
openMain(snapshot);
|
|
1605
|
-
lines.push(`COPY --from=${stage} ${output} ${output}`);
|
|
1611
|
+
lines.push(`COPY ${owner !== void 0 ? `--chown=${owner} ` : ""}--from=${stage} ${output} ${output}`);
|
|
1606
1612
|
break;
|
|
1607
1613
|
}
|
|
1608
1614
|
case "aptInstall":
|
|
@@ -1633,12 +1639,13 @@ function canonicalOperation(operation) {
|
|
|
1633
1639
|
args: [sortedEntries(operation.args[0])]
|
|
1634
1640
|
};
|
|
1635
1641
|
case "runWithSecrets": {
|
|
1636
|
-
const [command, { secrets, output }] = operation.args;
|
|
1642
|
+
const [command, { secrets, output, owner }] = operation.args;
|
|
1637
1643
|
return {
|
|
1638
1644
|
method: "runWithSecrets",
|
|
1639
1645
|
args: [command, {
|
|
1640
1646
|
secrets: [...secrets].sort(),
|
|
1641
|
-
output
|
|
1647
|
+
output,
|
|
1648
|
+
...owner !== void 0 ? { owner } : {}
|
|
1642
1649
|
}]
|
|
1643
1650
|
};
|
|
1644
1651
|
}
|
|
@@ -1784,11 +1791,17 @@ var DockerTemplate = class DockerTemplate {
|
|
|
1784
1791
|
});
|
|
1785
1792
|
const output = validateString(options.output, "output");
|
|
1786
1793
|
if (!output.startsWith("/")) throw new TypeError("output must be an absolute path");
|
|
1794
|
+
let owner;
|
|
1795
|
+
if (options.owner !== void 0) {
|
|
1796
|
+
owner = validateString(options.owner, "owner");
|
|
1797
|
+
assertValidOwner(owner);
|
|
1798
|
+
}
|
|
1787
1799
|
return this.#append({
|
|
1788
1800
|
method: "runWithSecrets",
|
|
1789
1801
|
args: [validateStringOrStrings(command, "command"), {
|
|
1790
1802
|
secrets,
|
|
1791
|
-
output
|
|
1803
|
+
output,
|
|
1804
|
+
...owner !== void 0 ? { owner } : {}
|
|
1792
1805
|
}]
|
|
1793
1806
|
});
|
|
1794
1807
|
}
|
|
@@ -2136,6 +2149,8 @@ function isImageNotFoundError(error) {
|
|
|
2136
2149
|
}
|
|
2137
2150
|
//#endregion
|
|
2138
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";
|
|
2139
2154
|
/**
|
|
2140
2155
|
* Setup completion marker shared by repo templates and their consumers.
|
|
2141
2156
|
*
|
|
@@ -2145,8 +2160,29 @@ function isImageNotFoundError(error) {
|
|
|
2145
2160
|
* booted from the image tell whether the setup it is about to run already
|
|
2146
2161
|
* happened. Relative to the template's build cwd, which is also the runtime
|
|
2147
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}).
|
|
2148
2166
|
*/
|
|
2149
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
|
+
}
|
|
2150
2186
|
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
2151
2187
|
function normalizeSetupCommands(setupCommand) {
|
|
2152
2188
|
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
@@ -2156,8 +2192,26 @@ function setupMarkerContent(setupCommand) {
|
|
|
2156
2192
|
return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
|
|
2157
2193
|
}
|
|
2158
2194
|
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
2159
|
-
function setupMarkerCommand(content) {
|
|
2160
|
-
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, `'\\''`)}'`;
|
|
2161
2215
|
}
|
|
2162
2216
|
//#endregion
|
|
2163
2217
|
//#region src/template/repo-template.ts
|
|
@@ -2173,7 +2227,11 @@ function setupMarkerCommand(content) {
|
|
|
2173
2227
|
* the template identity. A moved branch therefore yields a fresh image on the
|
|
2174
2228
|
* next new sandbox, and an unmoved one reuses the cached image. When the head
|
|
2175
2229
|
* cannot be resolved the resolver rejects: an unpinned clone cached under a
|
|
2176
|
-
* 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.
|
|
2177
2235
|
*
|
|
2178
2236
|
* The clone runs in a throwaway build stage; the credential is passed by value
|
|
2179
2237
|
* to that stage only and never enters the template identity or the image.
|
|
@@ -2189,38 +2247,67 @@ function setupMarkerCommand(content) {
|
|
|
2189
2247
|
* ```
|
|
2190
2248
|
*/
|
|
2191
2249
|
const execFileAsync = promisify(execFile);
|
|
2192
|
-
/** Env var the build's clone reads the credential from (see `
|
|
2250
|
+
/** Env var the build's clone reads the credential from (see `cloneAndPin`). */
|
|
2193
2251
|
const BUILD_TOKEN_ENV = "GH_TOKEN";
|
|
2194
2252
|
const DEFAULT_BASE_IMAGE = "node:22-slim";
|
|
2195
2253
|
const DEFAULT_WORKING_DIRECTORY = "/workspace";
|
|
2196
2254
|
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2197
|
-
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
2198
2255
|
const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
|
|
2199
2256
|
const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
|
|
2200
2257
|
const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
|
|
2201
2258
|
/** Refs interpolate into shell too; git ref names are already restricted, so allowlist tightly. */
|
|
2202
2259
|
const REF_PATTERN = /^[\w./-]+$/;
|
|
2203
2260
|
function createDockerRepoTemplate(options) {
|
|
2204
|
-
if (
|
|
2205
|
-
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");
|
|
2206
2274
|
const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
|
|
2207
2275
|
if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
|
|
2208
|
-
return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
|
|
2276
|
+
return (resolveOptions) => resolveRepoTemplate(options, entries, workingDirectory, resolveOptions);
|
|
2209
2277
|
}
|
|
2210
|
-
async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
|
|
2278
|
+
async function resolveRepoTemplate(options, entries, workingDirectory, resolveOptions = {}) {
|
|
2211
2279
|
const { abortSignal } = resolveOptions;
|
|
2280
|
+
const isList = options.repos !== void 0;
|
|
2212
2281
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
+
});
|
|
2218
2310
|
}
|
|
2219
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2220
|
-
const cloneUrl = access?.cloneUrl;
|
|
2221
|
-
if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
|
|
2222
|
-
assertCloneUrl(cloneUrl);
|
|
2223
|
-
const token = access?.authorization?.token;
|
|
2224
2311
|
let buildEnv;
|
|
2225
2312
|
try {
|
|
2226
2313
|
buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
|
|
@@ -2228,18 +2315,28 @@ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {
|
|
|
2228
2315
|
throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2229
2316
|
}
|
|
2230
2317
|
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2231
|
-
const
|
|
2232
|
-
throwIfAborted(abortSignal, "resolve Docker repository template");
|
|
2233
|
-
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`);
|
|
2234
|
-
return buildRepoTemplate({
|
|
2235
|
-
cloneUrl,
|
|
2236
|
-
sha,
|
|
2237
|
-
token,
|
|
2318
|
+
const shared = {
|
|
2238
2319
|
buildEnv,
|
|
2239
|
-
setupCommand: options.setupCommand,
|
|
2240
2320
|
workingDirectory,
|
|
2241
2321
|
baseImage: options.baseImage,
|
|
2322
|
+
owner: options.owner,
|
|
2242
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
|
|
2243
2340
|
});
|
|
2244
2341
|
}
|
|
2245
2342
|
/**
|
|
@@ -2258,14 +2355,15 @@ function buildRepoTemplate(inputs) {
|
|
|
2258
2355
|
if (inputs.baseImage === void 0) template = template.aptInstall(["git", "ca-certificates"]);
|
|
2259
2356
|
if (buildEnv && Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv);
|
|
2260
2357
|
const tokenEnv = token ? BUILD_TOKEN_ENV : void 0;
|
|
2261
|
-
|
|
2358
|
+
template = template.runWithSecrets(cloneAndPin({
|
|
2262
2359
|
cloneUrl,
|
|
2263
2360
|
destination,
|
|
2264
|
-
tokenEnv
|
|
2265
|
-
|
|
2266
|
-
|
|
2361
|
+
tokenEnv,
|
|
2362
|
+
sha
|
|
2363
|
+
}), {
|
|
2267
2364
|
secrets: tokenEnv ? [tokenEnv] : [],
|
|
2268
|
-
output: destination
|
|
2365
|
+
output: destination,
|
|
2366
|
+
...inputs.owner !== void 0 ? { owner: inputs.owner } : {}
|
|
2269
2367
|
}).setWorkdir(destination);
|
|
2270
2368
|
const setupCommands = normalizeSetupCommands(inputs.setupCommand);
|
|
2271
2369
|
for (const command of setupCommands) template = template.runCmd(command);
|
|
@@ -2273,6 +2371,60 @@ function buildRepoTemplate(inputs) {
|
|
|
2273
2371
|
return template;
|
|
2274
2372
|
}
|
|
2275
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
|
+
/**
|
|
2276
2428
|
* Resolve `ref` (or the default branch) to a commit sha with `git ls-remote`
|
|
2277
2429
|
* on the host, without cloning. A full sha is returned as is. Any failure
|
|
2278
2430
|
* yields undefined; the caller decides how to surface it.
|
|
@@ -2303,14 +2455,14 @@ async function resolveHead(cloneUrl, ref, token, abortSignal) {
|
|
|
2303
2455
|
});
|
|
2304
2456
|
const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
|
|
2305
2457
|
const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
|
|
2306
|
-
return sha &&
|
|
2458
|
+
return sha && FULL_SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
|
|
2307
2459
|
} catch (error) {
|
|
2308
2460
|
if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
|
|
2309
2461
|
return;
|
|
2310
2462
|
}
|
|
2311
2463
|
}
|
|
2312
|
-
function
|
|
2313
|
-
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)}`];
|
|
2314
2466
|
}
|
|
2315
2467
|
function assertCloneUrl(cloneUrl) {
|
|
2316
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`);
|