import {getCurrentContext} from './context' /** * File-based checkout using GitHub Contents API instead of git clone * Much faster, cheaper, and more reliable */ export interface FileInfo { path: string content: string sha: string } export class ApiCheckout { private octokit: any private owner: string private repo: string private commitSha: string private branchName?: string private cache: Map = new Map() constructor( probotContext?: any, options?: {owner?: string; repo?: string; ref?: string; branch?: string} ) { // Use passed probotContext or get from async context const ctx = probotContext || getCurrentContext() if (!ctx && !options) throw new Error('No context available and no options provided') this.octokit = ctx?.probotContext?.octokit || ctx?.octokit this.owner = options?.owner || ctx?.repo?.owner this.repo = options?.repo || ctx?.repo?.repo // Primary source: options if (options?.ref) { this.commitSha = options.ref } else { // Fallback: pull_request payload const pullRequest = ctx?.probotContext?.payload?.pull_request || ctx?.payload?.pull_request if (pullRequest?.head?.sha) { this.commitSha = pullRequest.head.sha } else { throw new Error( 'No commit SHA (ref) available for checkout. For issue_comment events, please provide ref in options.' ) } } this.branchName = options?.branch || ctx?.probotContext?.payload?.pull_request?.head?.ref || ctx?.payload?.pull_request?.head?.ref } /** * Get file content via GitHub API */ async getFile(path: string): Promise { // Check cache first if (this.cache.has(path)) { return this.cache.get(path)! } try { const response = await this.octokit.repos.getContent({ owner: this.owner, repo: this.repo, path: path, ref: this.commitSha }) // Handle both file and directory responses if (Array.isArray(response.data)) { throw new Error(`Path ${path} is a directory, not a file`) } const content = Buffer.from(response.data.content, 'base64').toString( 'utf-8' ) const fileInfo: FileInfo = { path, content, sha: response.data.sha } // Cache the result this.cache.set(path, fileInfo) return fileInfo } catch (error) { throw new Error(`Failed to get file ${path}: ${error}`) } } /** * Update file content via GitHub API */ async updateFile( path: string, newContent: string, message: string ): Promise { const currentFile = await this.getFile(path) try { const response = await this.octokit.repos.createOrUpdateFileContents({ owner: this.owner, repo: this.repo, path: path, message: message, content: Buffer.from(newContent).toString('base64'), sha: currentFile.sha, branch: this.branchName || this.commitSha }) // Update cache const updatedFile: FileInfo = { path, content: newContent, sha: response.data.commit.sha } this.cache.set(path, updatedFile) return response.data.commit.sha } catch (error) { throw new Error(`Failed to update file ${path}: ${error}`) } } /** * Create a new file via GitHub API */ async createFile( path: string, content: string, message: string ): Promise { try { const response = await this.octokit.repos.createOrUpdateFileContents({ owner: this.owner, repo: this.repo, path: path, message: message, content: Buffer.from(content).toString('base64'), branch: this.branchName || this.commitSha }) // Update cache const newFile: FileInfo = { path, content, sha: response.data.commit.sha } this.cache.set(path, newFile) return response.data.commit.sha } catch (error) { throw new Error(`Failed to create file ${path}: ${error}`) } } /** * Get directory listing */ async getDirectory(path: string): Promise { try { const response = await this.octokit.repos.getContent({ owner: this.owner, repo: this.repo, path: path, ref: this.commitSha }) if (!Array.isArray(response.data)) { throw new Error(`Path ${path} is not a directory`) } return response.data .filter((item: any) => item.type === 'file') .map((item: any) => item.path) } catch (error) { throw new Error(`Failed to get directory ${path}: ${error}`) } } /** * Get repository tree structure */ async getTree(): Promise { try { const response = await this.octokit.git.getTree({ owner: this.owner, repo: this.repo, tree_sha: this.commitSha, recursive: true }) return response.data.tree .filter((item: any) => item.type === 'blob') .map((item: any) => item.path) } catch (error) { throw new Error(`Failed to get repository tree: ${error}`) } } }