232 lines
6.4 KiB
TypeScript
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 };
|
|
}
|