c9ab80ab94
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { createServer, type Server } from "node:http";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
type HTTPGitFixture = {
|
|
remoteURL: string;
|
|
checkoutPath: string;
|
|
/**
|
|
* Backend-only config for fixture consumers whose Git path honors global
|
|
* configuration. Managed workspace clones deliberately sanitize it.
|
|
*/
|
|
backendEnv: Record<string, string>;
|
|
/**
|
|
* Test-executor-only Git configuration that rewrites the trusted public
|
|
* origin to this fixture's bridge-reachable HTTP server.
|
|
*/
|
|
gitConfigEnvVars: Array<{ key: string; value: string }>;
|
|
close: () => Promise<void>;
|
|
};
|
|
|
|
export type HTTPGitFixtureOptions = {
|
|
onListening?: (server: Server, port: number) => void;
|
|
writeBackendGitConfig?: (file: string, content: string) => void;
|
|
closeServer?: (server: Server) => Promise<void>;
|
|
};
|
|
|
|
/**
|
|
* Serves a disposable bare repository from the Docker bridge gateway. This
|
|
* exercises the same HTTP clone path used by Docker and SSH executors without
|
|
* relying on an external provider or developer checkout.
|
|
*/
|
|
export async function startHTTPGitFixture(
|
|
root: string,
|
|
name: string,
|
|
options: HTTPGitFixtureOptions = {},
|
|
): Promise<HTTPGitFixture> {
|
|
const remoteDir = path.join(root, "fixture", `${name}.git`);
|
|
const checkout = path.join(root, `${name}-checkout`);
|
|
fs.mkdirSync(checkout, { recursive: true });
|
|
execFileSync("git", ["init", "--bare", "-b", "main", remoteDir]);
|
|
execFileSync("git", ["init", "-b", "main"], { cwd: checkout });
|
|
fs.writeFileSync(path.join(checkout, "remote-source.txt"), `${name} fixture\n`);
|
|
execFileSync("git", ["add", "."], { cwd: checkout });
|
|
execFileSync(
|
|
"git",
|
|
["-c", "user.name=E2E Test", "-c", "user.email=e2e@test.local", "commit", "-m", "fixture"],
|
|
{ cwd: checkout },
|
|
);
|
|
execFileSync("git", ["remote", "add", "origin", remoteDir], { cwd: checkout });
|
|
execFileSync("git", ["push", "origin", "main"], { cwd: checkout });
|
|
execFileSync("git", ["--git-dir", remoteDir, "update-server-info"]);
|
|
|
|
const server = createStaticGitServer(root);
|
|
const port = await listen(server);
|
|
try {
|
|
options.onListening?.(server, port);
|
|
const fixtureOrigin = `http://${dockerBridgeGateway()}:${port}/`;
|
|
const remoteURL = `https://gitlab.com/fixture/${name}.git`;
|
|
execFileSync("git", ["remote", "set-url", "origin", remoteURL], { cwd: checkout });
|
|
const backendGitConfigPath = path.join(root, "fixture", `${name}.gitconfig`);
|
|
const config = `[url "${fixtureOrigin}fixture/${name}.git"]\n\tinsteadOf = ${remoteURL}\n`;
|
|
(options.writeBackendGitConfig ?? fs.writeFileSync)(backendGitConfigPath, config);
|
|
return {
|
|
// The source endpoint must receive the real GitLab identity so the
|
|
// production trusted-origin validation remains exercised. Disposable test
|
|
// executor profiles and their isolated backend fixture rewrite Git's clone
|
|
// transport to this local HTTP server.
|
|
remoteURL,
|
|
checkoutPath: checkout,
|
|
backendEnv: { GIT_CONFIG_GLOBAL: backendGitConfigPath },
|
|
gitConfigEnvVars: [
|
|
{ key: "GIT_CONFIG_COUNT", value: "1" },
|
|
{ key: "GIT_CONFIG_KEY_0", value: `url.${fixtureOrigin}.insteadOf` },
|
|
{ key: "GIT_CONFIG_VALUE_0", value: "https://gitlab.com/" },
|
|
],
|
|
// Leave the config under the backend fixture root until that fixture has
|
|
// released its environment and stopped its process, then removes the root.
|
|
close: () => closeServer(server),
|
|
};
|
|
} catch (setupError) {
|
|
try {
|
|
await (options.closeServer ?? closeServer)(server);
|
|
} catch (closeError) {
|
|
throw new AggregateError(
|
|
[setupError, closeError],
|
|
"HTTP Git fixture setup failed and its server did not close",
|
|
);
|
|
}
|
|
throw setupError;
|
|
}
|
|
}
|
|
|
|
function createStaticGitServer(root: string): Server {
|
|
return createServer((request, response) => {
|
|
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://fixture").pathname);
|
|
const relative = pathname.replace(/^\/+/, "");
|
|
const file = path.resolve(root, relative);
|
|
if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
|
|
response.writeHead(400).end();
|
|
return;
|
|
}
|
|
try {
|
|
const stat = fs.statSync(file);
|
|
if (!stat.isFile()) throw new Error("not a file");
|
|
response.writeHead(200, { "content-length": stat.size });
|
|
fs.createReadStream(file).pipe(response);
|
|
} catch {
|
|
response.writeHead(404).end();
|
|
}
|
|
});
|
|
}
|
|
|
|
function dockerBridgeGateway(): string {
|
|
const result = spawnSync(
|
|
"docker",
|
|
["network", "inspect", "bridge", "-f", "{{(index .IPAM.Config 0).Gateway}}"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const gateway = result.status === 0 ? result.stdout.trim() : "";
|
|
if (!gateway) throw new Error(`Could not determine Docker bridge gateway: ${result.stderr}`);
|
|
return gateway;
|
|
}
|
|
|
|
function listen(server: Server): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "0.0.0.0", () => {
|
|
server.off("error", reject);
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
reject(new Error("HTTP Git fixture did not receive a TCP port"));
|
|
return;
|
|
}
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
}
|
|
|
|
function closeServer(server: Server): Promise<void> {
|
|
return new Promise((resolve, reject) =>
|
|
server.close((error) => (error ? reject(error) : resolve())),
|
|
);
|
|
}
|