diff --git a/packages/adapters/src/runtime/build-pipeline.ts b/packages/adapters/src/runtime/build-pipeline.ts index 752bb4108..a67b2643a 100644 --- a/packages/adapters/src/runtime/build-pipeline.ts +++ b/packages/adapters/src/runtime/build-pipeline.ts @@ -319,7 +319,7 @@ export async function runBuildPipeline( await exec( gitShellCommand( gitInvocation, - `clone --progress --depth 50 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)}`, + `clone --progress --depth 50 --recurse-submodules --shallow-submodules --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)}`, ), ); const commitPresent = await exec( @@ -343,11 +343,17 @@ export async function runBuildPipeline( `-c advice.detachedHead=false checkout ${sq(config.commitSha)}`, )}`, ); + await exec( + `cd ${sq(env.projectDir)} && ${gitShellCommand( + gitInvocation, + "submodule update --init --recursive", + )}`, + ); } else { await exec( gitShellCommand( gitInvocation, - `clone --progress --depth 1 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)}`, + `clone --progress --depth 1 --recurse-submodules --shallow-submodules --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)}`, ), ); } diff --git a/packages/adapters/src/runtime/cloud.ts b/packages/adapters/src/runtime/cloud.ts index 140762183..48c0b9bb9 100644 --- a/packages/adapters/src/runtime/cloud.ts +++ b/packages/adapters/src/runtime/cloud.ts @@ -1502,10 +1502,10 @@ export class CloudRuntime implements MultiServiceRuntimeAdapter { // See fetchCommand above for env-var rationale; --progress keeps // the clone visible in the streamed log even though stdout/stderr // are pipes, not a tty. - `GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/echo git -c credential.helper= clone --progress ${depthArgs}--branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(cloneTarget)}`, + `GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/echo git -c credential.helper= clone --progress ${depthArgs}--recurse-submodules --shallow-submodules --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(cloneTarget)}`, ].join("\n"); const checkoutCommand = config.commitSha - ? `cd ${sq(cloneTarget)} && git -c credential.helper= -c advice.detachedHead=false checkout ${sq(config.commitSha)}` + ? `cd ${sq(cloneTarget)} && git -c credential.helper= -c advice.detachedHead=false checkout ${sq(config.commitSha)} && GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/echo git -c credential.helper= submodule update --init --recursive` : ""; // No name-based pruning: a fresh clone already contains only git-tracked // files (gitignored output was never committed), so pruning by name here @@ -1513,7 +1513,7 @@ export class CloudRuntime implements MultiServiceRuntimeAdapter { // A tracked `.dockerignore` still applies at `docker build` on the worker. const prepareCommand = [ "set -e", - `rm -rf ${sq(joinWorkspacePath(cloneTarget, ".git"))}`, + `find ${sq(cloneTarget)} -name .git -prune -exec rm -rf {} +`, ...prepareContextCommands, 'echo "Dockerfile context prepared."', ].join("\n"); diff --git a/packages/adapters/src/runtime/docker.ts b/packages/adapters/src/runtime/docker.ts index bad142ad5..6c4202dca 100644 --- a/packages/adapters/src/runtime/docker.ts +++ b/packages/adapters/src/runtime/docker.ts @@ -1743,6 +1743,16 @@ export class DockerRuntime implements RuntimeAdapter { destDir: remoteContextDir, onLog: (entry) => log.log(entry.message, parseLogLevel(entry.message)), }); + // Check for submodules. If present, the tarball is missing submodule contents. + const hasSubmodules = await executor + .exec(`test -f ${sq(`${remoteContextDir}/.gitmodules`)}`) + .then( + () => true, + () => false, + ); + if (hasSubmodules) { + throw new Error("Repository contains submodules; tarball download is insufficient"); + } // A tarball has no .git, but strip defensively in case a repo tracks one. await executor.exec(`rm -rf ${sq(`${remoteContextDir}/.git`)}`).catch(() => {}); return; @@ -1791,7 +1801,7 @@ export class DockerRuntime implements RuntimeAdapter { log.log(`Cloning ${config.repoUrl} on the server → ${remoteContextDir} (${authLabel})...\n`); await executor.exec(`rm -rf ${dir} && mkdir -p ${dir}`); - const run = async (operation: "clone" | "fetch" | "checkout", cmd: string) => { + const run = async (operation: "clone" | "fetch" | "checkout" | "submodule", cmd: string) => { const { code } = await executor.streamExec(cmd, (entry) => log.log(entry.message, parseLogLevel(entry.message)), ); @@ -1808,7 +1818,7 @@ export class DockerRuntime implements RuntimeAdapter { "clone", gitShellCommand( gitInvocation, - `clone --progress --depth 50 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${dir}`, + `clone --progress --depth 50 --recurse-submodules --shallow-submodules --branch ${sq(config.branch)} ${sq(cloneUrl)} ${dir}`, ), ); const commitPresent = await executor @@ -1834,17 +1844,23 @@ export class DockerRuntime implements RuntimeAdapter { `-c advice.detachedHead=false checkout ${sq(config.commitSha)}`, )}`, ); + await run( + "submodule", + `cd ${dir} && ${gitShellCommand(gitInvocation, "submodule update --init --recursive")}`, + ); } else { await run( "clone", gitShellCommand( gitInvocation, - `clone --progress --depth 1 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${dir}`, + `clone --progress --depth 1 --recurse-submodules --shallow-submodules --branch ${sq(config.branch)} ${sq(cloneUrl)} ${dir}`, ), ); } - // Never ship .git into the build image. - await executor.exec(`rm -rf ${sq(`${remoteContextDir}/.git`)}`).catch(() => {}); + // Never ship .git into the build image. Submodules may create .git files/dirs within the tree. + await executor + .exec(`find ${sq(remoteContextDir)} -name .git -prune -exec rm -rf {} +`) + .catch(() => {}); } finally { await sshMaterial?.cleanup(); }