Files
shockbot/utils/setup.ts
T
2025-11-25 16:02:08 -05:00

211 lines
6.3 KiB
TypeScript

import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import type { Payload } from "../external.ts";
import { log } from "./cli.ts";
import type { RepoContext } from "./github.ts";
import { $ } from "./shell.ts";
// Store original remote URL for cleanup (only thing we need to restore)
let originalRemoteUrl: string | null = null;
export interface SetupOptions {
tempDir: string;
repoUrl?: string;
forceClean?: boolean;
}
/**
* Setup the test repository for running actions
*/
export function setupTestRepo(options: SetupOptions): void {
const {
tempDir,
repoUrl = "git@github.com:pullfrogai/scratch.git",
forceClean = false,
} = options;
if (existsSync(tempDir)) {
if (forceClean) {
log.info("🗑️ Removing existing .temp directory...");
rmSync(tempDir, { recursive: true, force: true });
log.info("📦 Cloning pullfrogai/scratch into .temp...");
$("git", ["clone", repoUrl, tempDir]);
} else {
log.info("📦 Resetting existing .temp repository...");
execSync("git reset --hard HEAD && git clean -fd", {
cwd: tempDir,
stdio: "inherit",
});
}
} else {
log.info("📦 Cloning pullfrogai/scratch into .temp...");
$("git", ["clone", repoUrl, tempDir]);
}
}
/**
* Setup git configuration to avoid identity errors
* Uses --local flag to scope config to the current repo only
*/
export function setupGitConfig(): void {
const repoDir = process.cwd();
log.info("🔧 Setting up git configuration...");
try {
// Use --local to scope config to this repo only, preventing leakage to user's global config
execSync('git config --local user.email "action@pullfrog.ai"', {
cwd: repoDir,
stdio: "pipe",
});
execSync('git config --local user.name "Pullfrog Action"', {
cwd: repoDir,
stdio: "pipe",
});
log.debug("setupGitConfig: ✓ Git configuration set successfully (scoped to repo)");
} catch (error) {
// If git config fails, log warning but don't fail the action
// This can happen if we're not in a git repo or git isn't available
log.warning(
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Setup git authentication using GitHub installation token
* Always uses the installation token, scoped to the current repo only
*/
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
const repoDir = process.cwd();
// Store original remote URL for cleanup
try {
originalRemoteUrl =
execSync("git config --local --get remote.origin.url", {
cwd: repoDir,
stdio: "pipe",
encoding: "utf-8",
}).trim() || null;
} catch {
originalRemoteUrl = null;
}
log.info("🔐 Setting up git authentication...");
// Remove existing git auth headers that actions/checkout might have set
// Use --local to scope to this repo only
try {
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
cwd: repoDir,
stdio: "pipe",
});
log.info("✓ Removed existing authentication headers");
} catch {
log.debug("No existing authentication headers to remove");
}
// Update remote URL to embed the token
// This is scoped to the repo's .git/config, not the user's global config
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
}
/**
* Setup git branch based on payload event context
* Automatically checks out the appropriate branch before agent execution
*/
export function setupGitBranch(payload: Payload): void {
const branch = payload.event.branch;
const repoDir = process.cwd();
if (!branch) {
log.debug("No branch specified in payload, using default branch");
return;
}
log.info(`🌿 Setting up git branch: ${branch}`);
try {
// Fetch the branch from origin
log.debug(`Fetching branch from origin: ${branch}`);
execSync(`git fetch origin ${branch}`, {
cwd: repoDir,
stdio: "pipe",
});
// Checkout the branch, creating local tracking branch
log.debug(`Checking out branch: ${branch}`);
execSync(`git checkout -B ${branch} origin/${branch}`, {
cwd: repoDir,
stdio: "pipe",
});
log.info(`✓ Successfully checked out branch: ${branch}`);
} catch (error) {
// If git operations fail, log warning but don't fail the action
// The agent might still be able to work with the default branch
log.warning(
`Failed to checkout branch ${branch}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Clean up local git configuration after action completes
* Removes the --local config entries we added so the repo returns to its original state
* Only runs in local development (not in GitHub Actions)
*/
export function restoreGitConfig(): void {
if (process.env.GITHUB_ACTIONS) {
return;
}
const repoDir = process.cwd();
log.info("🔄 Cleaning up git configuration...");
try {
try {
execSync("git config --local --unset user.email", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("✓ Removed local user.email");
} catch {
// Ignore if unset fails (config might not exist)
}
try {
execSync("git config --local --unset user.name", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("✓ Removed local user.name");
} catch {
// Ignore if unset fails (config might not exist)
}
// Restore original remote URL if we stored it
if (originalRemoteUrl !== null) {
try {
$("git", ["remote", "set-url", "origin", originalRemoteUrl], { cwd: repoDir });
log.debug("✓ Restored original remote URL");
} catch (error) {
log.warning(
`Failed to restore remote URL: ${error instanceof Error ? error.message : String(error)}`
);
}
}
log.info("✓ Git configuration cleanup completed");
} catch (error) {
// Log warning but don't fail - this is cleanup
log.warning(
`Failed to clean up git config: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
// Clear stored remote URL
originalRemoteUrl = null;
}
}