import {mkdirSync, existsSync, rmSync, writeFileSync} from 'fs' import {join} from 'path' import {simpleGit, SimpleGit} from 'simple-git' import {getCurrentContext} from './context' import pino from 'pino' const logger = pino({ level: process.env.LOG_LEVEL || 'info' }) export interface CheckoutOptions { sparse?: boolean files?: string[] ref?: string } export const checkoutRepo = async ( tempDir: string, options: CheckoutOptions = {} ): Promise => { const ctx = getCurrentContext() if (!ctx) throw new Error('No context available for checkout') // Verify git is available const git = simpleGit() try { await git.raw(['--version']) } catch (e) { throw new Error('git command not found in PATH. Please install git.') } const {owner, repo} = ctx.repo const installationId = ctx.probotContext.payload.installation?.id if (!installationId) { throw new Error('Installation ID not found in payload') } const {token} = (await ctx.probotContext.octokit.auth({ type: 'installation' })) as any if (!token) { throw new Error( 'Failed to create installation access token: token is undefined' ) } // Ensure gh CLI is authenticated for subsequent contexts process.env.GH_TOKEN = token process.env.GITHUB_TOKEN = token // SECURITY: Use token via environment variable, NOT in URL // This prevents token exposure in process listings and logs const cloneUrl = `https://github.com/${owner}/${repo}.git` const isPullRequest = !!ctx.probotContext.payload.pull_request const headBranch = isPullRequest ? ctx.probotContext.payload.pull_request.head.ref : (ctx.probotContext.payload.ref?.replace('refs/heads/', '') || ctx.probotContext.payload.repository?.default_branch || 'main') const headSha = isPullRequest ? ctx.probotContext.payload.pull_request.head.sha : (options.ref || ctx.probotContext.payload.after) // Set up git credential helper for secure token authentication process.env.GIT_ASKPASS = 'echo' process.env.GIT_USERNAME = 'x-access-token' process.env.GIT_PASSWORD = token if (existsSync(tempDir)) { rmSync(tempDir, {recursive: true, force: true}) } mkdirSync(tempDir, {recursive: true}) logger.info({ owner, repo, tempDir }, 'Cloning repository') const repoGit: SimpleGit = simpleGit(tempDir, { timeout: { block: 120000 } // 2 minute timeout for all git operations }) try { if (options.sparse && options.files && options.files.length > 0) { logger.info({ fileCount: options.files.length }, 'Using sparse checkout') // Initialize empty repo and configure sparse checkout await repoGit.init(['--bare']) await repoGit.addRemote('origin', cloneUrl) // Configure sparse checkout await repoGit.raw(['config', 'core.sparseCheckout', 'true']) writeFileSync(join(tempDir, '.git/info/sparse-checkout'), options.files.join('\n')) // Fetch and checkout specific commit await repoGit.fetch(['origin', headSha, '--depth', '1']) await repoGit.checkout(['FETCH_HEAD']) } else { logger.info('Performing full shallow clone') // Clone with depth 1 for efficiency await repoGit.clone(cloneUrl, '.', [ '--depth', '1', '--single-branch', '--branch', headBranch, '--no-checkout' ]) // Fetch and checkout specific SHA await repoGit.fetch(['origin', headSha, '--depth', '1']) await repoGit.checkout([headSha]) } // Configure git identity await repoGit.addConfig('user.name', 'PRIX AI Auditor') await repoGit.addConfig('user.email', 'bot@prix.ai') logger.info({ owner, repo, headSha }, 'Repository cloned successfully') } catch (error) { logger.error({ error, owner, repo }, 'Git operation failed') throw new Error(`Failed to clone repository: ${error}`) } return tempDir }