Files

232 lines
6.4 KiB
TypeScript

import { execFileSync, execSync } from "node:child_process";
import { mkdtempSync, readdirSync, realpathSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ShellPermission } from "../external.ts";
import type { ToolState } from "../toolState.ts";
import { log } from "./cli.ts";
import type { Gitea } from "./gitea.ts";
import { isInsideDocker } from "./globals.ts";
import { $ } from "./shell.ts";
export interface SetupOptions {
tempDir: string;
}
export function createTempDirectory(): string {
const sharedTempDir = mkdtempSync(join(tmpdir(), "shockbot-"));
process.env.SHOCKBOT_TEMP_DIR = sharedTempDir;
// also set PULLFROG_TEMP_DIR for compatibility with files that haven't been updated
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
log.info(`» created temp dir at ${sharedTempDir}`);
return sharedTempDir;
}
export function wipeRunnerLeakSurface(): void {
const runnerTemp = process.env.RUNNER_TEMP;
if (!runnerTemp) return;
const preserve = new Set<string>();
for (const envVar of [
"GITHUB_OUTPUT",
"GITHUB_ENV",
"GITHUB_PATH",
"GITHUB_STATE",
"GITHUB_STEP_SUMMARY",
]) {
const path = process.env[envVar];
if (!path) continue;
try {
preserve.add(realpathSync(path));
} catch {
preserve.add(path);
}
}
const wiped: string[] = [];
const tryUnlink = (path: string): void => {
let resolved = path;
try {
resolved = realpathSync(path);
} catch {}
if (preserve.has(resolved) || preserve.has(path)) return;
try {
unlinkSync(path);
wiped.push(path);
} catch {}
};
const listDir = (dir: string): string[] => {
try {
return readdirSync(dir);
} catch {
return [];
}
};
const fileCommandsDir = join(runnerTemp, "_runner_file_commands");
for (const entry of listDir(fileCommandsDir)) {
tryUnlink(join(fileCommandsDir, entry));
}
for (const entry of listDir(runnerTemp)) {
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
tryUnlink(join(runnerTemp, entry));
}
}
if (wiped.length > 0) {
log.info(`» wiped ${wiped.length} leak-surface file(s) from $RUNNER_TEMP`);
log.debug(`» wiped paths: ${wiped.join(", ")}`);
}
}
function envScopedToRepo(): NodeJS.ProcessEnv {
const scoped = { ...process.env };
for (const key of Object.keys(scoped)) {
if (key.startsWith("GIT_")) delete scoped[key];
}
return scoped;
}
export function removeIncludeIfEntries(repoDir: string): void {
const env = envScopedToRepo();
let configOutput: string;
try {
configOutput = execSync("git config --local --get-regexp -z ^includeif\\.", {
cwd: repoDir,
encoding: "utf-8",
stdio: "pipe",
env,
});
} catch {
log.debug("» no includeIf credential entries to remove");
return;
}
const seen = new Set<string>();
for (const entry of configOutput.split("\0")) {
if (!entry) continue;
const nl = entry.indexOf("\n");
const key = nl === -1 ? entry : entry.slice(0, nl);
if (!key || seen.has(key)) continue;
seen.add(key);
try {
execFileSync("git", ["config", "--local", "--unset-all", key], {
cwd: repoDir,
stdio: "pipe",
env,
});
} catch (error) {
log.debug(
`» failed to unset ${key}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
if (seen.size > 0)
log.info(
`» removed ${seen.size} includeIf credential ${seen.size === 1 ? "entry" : "entries"}`
);
}
export interface GitContext {
gitToken: string;
owner: string;
name: string;
gitea: Gitea;
toolState: ToolState;
shell: ShellPermission;
postCheckoutScript: string | null;
}
export type SetupGitParams = GitContext;
export async function setupGit(params: SetupGitParams): Promise<void> {
const repoDir = process.cwd();
log.info("» setting up git configuration...");
try {
let currentEmail = "";
try {
currentEmail = execSync("git config user.email", {
cwd: repoDir,
stdio: "pipe",
encoding: "utf-8",
}).trim();
} catch {}
const shouldSetDefaults =
!currentEmail ||
currentEmail.includes("github-actions") ||
currentEmail.includes("pullfrog");
if (shouldSetDefaults) {
execSync('git config --local user.email "shockbot@git.shockvpn.com"', {
cwd: repoDir,
stdio: "pipe",
});
execSync('git config --local user.name "shockbot"', {
cwd: repoDir,
stdio: "pipe",
});
log.debug("» git user configured (shockbot defaults)");
} else {
log.debug(`» git user already configured (${currentEmail}), skipping`);
}
if (params.shell === "disabled") {
execSync("git config --local core.hooksPath /dev/null", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("» git hooks disabled (shell=disabled)");
}
} catch (error) {
log.info(`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`);
}
// Remove credential helpers set by actions/checkout
for (const header of [
"http.https://github.com/.extraheader",
"http.https://git.shockvpn.com/.extraheader",
]) {
try {
execSync(`git config --local --unset-all ${header}`, {
cwd: repoDir,
stdio: "pipe",
});
log.info(`» removed existing authentication headers (${header})`);
} catch {
log.debug(`» no existing authentication headers to remove (${header})`);
}
}
removeIncludeIfEntries(repoDir);
const giteaUrl = (process.env.GITEA_URL ?? "https://git.shockvpn.com").replace(/\/$/, "");
const originUrl = `${giteaUrl}/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
params.toolState.pushUrl = originUrl;
$("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
params.toolState.initialHead = captureInitialHead(repoDir);
log.info("» git authentication configured");
}
export function captureInitialHead(
repoDir: string
): { kind: "branch"; name: string } | { kind: "detached"; sha: string } {
try {
const name = $("git", ["symbolic-ref", "--short", "HEAD"], {
cwd: repoDir,
log: false,
}).trim();
if (name) return { kind: "branch", name };
} catch {}
const sha = $("git", ["rev-parse", "HEAD"], { cwd: repoDir, log: false }).trim();
return { kind: "detached", sha };
}