diff --git a/apps/server/src/git/operations.ts b/apps/server/src/git/operations.ts index 9bef177..90a9f66 100644 --- a/apps/server/src/git/operations.ts +++ b/apps/server/src/git/operations.ts @@ -1,4 +1,4 @@ -import { SimpleGit } from 'simple-git'; +import { SimpleGit, simpleGit } from 'simple-git'; import { logger } from '../config/logger'; import { GitHubError } from '../utils/error'; import { envConfig } from '../config/env'; @@ -28,6 +28,38 @@ export const createBranch = async (git: SimpleGit, branchName: string): Promise< } }; +/** + * Checkout a remote branch by name or SHA. + */ +export const checkoutRemoteBranch = async ( + repoPath: string, + ref: string, + sha?: string, +): Promise => { + const git = simpleGit(repoPath); + try { + logger.info({ repoPath, ref, sha }, 'Checking out remote branch/ref'); + + // Fetch all remote branches to ensure the ref is available locally + await git.fetch('origin'); + + // If a SHA is provided, try to checkout that specific commit + if (sha) { + await git.checkout(sha); + logger.info({ repoPath, sha }, 'Checked out specific commit SHA'); + } else { + // Otherwise, checkout the branch directly + await git.checkout(ref); + logger.info({ repoPath, ref }, 'Checked out remote branch'); + } + } catch (error) { + logger.error({ repoPath, ref, sha, error }, 'Failed to checkout remote branch/ref'); + throw new GitHubError( + `Failed to checkout remote branch/ref ${ref}: ${error instanceof Error ? error.message : String(error)}`, + ); + } +}; + /** * Stage and commit changes */ diff --git a/apps/server/src/github/pr.ts b/apps/server/src/github/pr.ts index 60f55c4..4c84a6e 100644 --- a/apps/server/src/github/pr.ts +++ b/apps/server/src/github/pr.ts @@ -1,6 +1,8 @@ import { Octokit } from '@octokit/rest'; import { logger } from '../config/logger'; import { GitHubError } from '../utils/error'; +import { getOctokitInstance } from './app'; + interface CreatePRParams { owner: string; repo: string; @@ -92,3 +94,60 @@ export const createPullRequest = async ( ); } }; + +/** + * Add a comment to a pull request review comment. + */ +export const addCommentToPullRequest = async ( + owner: string, + repo: string, + pull_number: number, + comment_id: number, + body: string, + installationId: number, +): Promise => { + try { + const octokit = await getOctokitInstance(installationId); + logger.info( + { + owner, + repo, + pull_number, + comment_id, + }, + 'Adding comment to pull request review comment', + ); + + await octokit.pulls.createReviewComment({ + owner, + repo, + pull_number, + comment_id, + body, + }); + + logger.info( + { + owner, + repo, + pull_number, + comment_id, + }, + 'Comment added to pull request review comment successfully', + ); + } catch (error) { + logger.error( + { + owner, + repo, + pull_number, + comment_id, + error, + }, + 'Failed to add comment to pull request review comment', + ); + throw new GitHubError( + `Failed to add comment to pull request review comment: ${error instanceof Error ? error.message : String(error)}`, + ); + } +}; diff --git a/apps/server/src/jobs/mentionHandler.ts b/apps/server/src/jobs/mentionHandler.ts index fb2eaad..19517db 100644 --- a/apps/server/src/jobs/mentionHandler.ts +++ b/apps/server/src/jobs/mentionHandler.ts @@ -1,281 +1,76 @@ -import { isAppMentionJob, isUserMentionJob, WorkerJob } from '../types/jobs'; -import { BotConfig } from '../types/config'; -import { logger as rootLogger } from '../config/logger'; -import { GitHubApp } from '../github/app'; -import { Octokit } from '@octokit/rest'; -import { cloneRepository, cleanupRepository } from '../git/clone'; -import { createBranch, commitChanges, pushChanges } from '../git/operations'; -import { createPullRequest } from '../github/pr'; -import { loadBotConfig } from '../utils/yaml'; -import { JobError } from '../utils/error'; -import { envConfig } from '../config/env'; -import { ensureForkExists, ForkResult } from '../git/fork'; -import { processCodeModificationRequest } from '@/llm/processor'; -import { taskCompletionTool } from '@/llm/tools/task'; -import { RateLimitManager } from '../utils/rateLimit'; -import { db } from '../db'; +import { JobType, AppMentionOnIssueJob, AppMentionOnPullRequestJob } from "../types/jobs"; +import { logger } from "../config/logger"; +import { cloneRepository } from "../git/clone"; +import { get and updatePullRequest } from "../github/pr"; +import { processCodeModificationRequest } from "../llm/processor"; +import { createSourceModifierAgent } from "../llm/processor"; +import { taskCompletionTool } from "../llm/tools/task"; +import { addCommentToPullRequest } from "../github/pr"; +import { checkoutRemoteBranch } from "../git/operations"; -const handlerLogger = rootLogger.child({ context: 'MentionOnIssueJobHandler' }); +export async function handleAppMentionOnPullRequestJob(job: AppMentionOnPullRequestJob) { + logger.info({ job }, "Handling AppMentionOnPullRequestJob"); -export async function handleMentionOnIssueJob(job: WorkerJob): Promise { - const { - id: jobId, - originalRepoOwner, - originalRepoName, - eventIssueNumber, - eventIssueTitle, - commandToProcess, - triggeredBy, - } = job; + const { originalRepoOwner, originalRepoName, installationId, commandToProcess, repositoryUrl, eventPullRequestNumber, headRef, headSha, commentId } = job; - const logger = handlerLogger.child({ - jobId, - jobType: job.type, - repo: `${originalRepoOwner}/${originalRepoName}`, - issue: eventIssueNumber, - triggeredBy, - }); - - logger.info('Starting MentionOnIssueJob handling.'); - - // Initialize rate limit manager - const rateLimitManager = new RateLimitManager(db); - - let octokit: Octokit; - let repoPath: string | undefined; - let cloneToken: string | undefined; - let repositoryToCloneUrl: string; - let headBranchOwner: string = originalRepoOwner; - - if (isAppMentionJob(job)) { - logger.info('Job identified as AppMention. Setting up GitHub App authentication.'); - const githubApp = new GitHubApp(); - octokit = await githubApp.getAuthenticatedClient(job.installationId); - cloneToken = await githubApp.getInstallationToken(job.installationId); - repositoryToCloneUrl = job.repositoryUrl; - headBranchOwner = originalRepoOwner; - logger.info('Successfully authenticated as GitHub App installation.'); - } else if (isUserMentionJob(job)) { - logger.info('Job identified as UserMention. Setting up PAT authentication and forking.'); - if (!envConfig.BOT_USER_PAT || !envConfig.BOT_NAME) { - logger.error('BOT_USER_PAT or BOT_NAME not configured for UserMentionOnIssueJob.'); - throw new JobError( - 'UserMentionOnIssueJob handler is not configured with BOT_USER_PAT or BOT_NAME.', - ); - } - octokit = new Octokit({ auth: envConfig.BOT_USER_PAT }); - cloneToken = envConfig.BOT_USER_PAT; - headBranchOwner = envConfig.BOT_NAME; - - logger.info(`Authenticating as user @${headBranchOwner} for fork operations.`); - const forkResult: ForkResult = await ensureForkExists( - octokit, - originalRepoOwner, - originalRepoName, - headBranchOwner, - ); - repositoryToCloneUrl = forkResult.forkCloneUrl; - headBranchOwner = forkResult.forkOwner; // Use the actual owner of the fork - logger.info( - `Ensured fork exists: ${forkResult.forkOwner}/${forkResult.forkRepoName} at ${repositoryToCloneUrl}`, - ); - } else { - throw new JobError(`Unknown job type for job ID ${jobId}`); - } - - // Check rate limits for both triggeredBy and repoOwner - const triggeredByCheck = await rateLimitManager.checkRateLimit(triggeredBy, 'triggeredBy'); - const repoOwnerCheck = await rateLimitManager.checkRateLimit(originalRepoOwner, 'repoOwner'); - - logger.info( - { - triggeredByCheck: { - allowed: triggeredByCheck.allowed, - planType: triggeredByCheck.planType, - usage: triggeredByCheck.usage, - reason: triggeredByCheck.reason, - }, - repoOwnerCheck: { - allowed: repoOwnerCheck.allowed, - planType: repoOwnerCheck.planType, - usage: repoOwnerCheck.usage, - reason: repoOwnerCheck.reason, - }, - }, - 'Rate limit check results', - ); - - // If either user has exceeded their limits, post a rate limit message and exit - if (!triggeredByCheck.allowed || !repoOwnerCheck.allowed) { - const limitExceededUser = !triggeredByCheck.allowed ? triggeredBy : originalRepoOwner; - const limitExceededCheck = !triggeredByCheck.allowed ? triggeredByCheck : repoOwnerCheck; - const limitExceededUserType = !triggeredByCheck.allowed ? 'triggeredBy' : 'repoOwner'; - - const rateLimitMessage = rateLimitManager.generateRateLimitMessage( - triggeredBy, - limitExceededCheck, - limitExceededUserType, - ); - - try { - await octokit.issues.createComment({ - owner: originalRepoOwner, - repo: originalRepoName, - issue_number: eventIssueNumber, - body: rateLimitMessage, - }); - logger.info( - { - limitExceededUser, - limitExceededUserType, - reason: limitExceededCheck.reason, - }, - 'Posted rate limit exceeded message', - ); - } catch (commentError) { - logger.error({ commentError }, 'Failed to post rate limit message'); - } - - return; // Exit early due to rate limit - } - - let initialCommentId: number | undefined; + const repoPath = `./repos/${originalRepoOwner}/${originalRepoName}`; try { - const initialComment = await octokit.issues.createComment({ - owner: originalRepoOwner, - repo: originalRepoName, - issue_number: eventIssueNumber, - body: `👋 Hi @${triggeredBy}, I'm on it! I'll apply changes, and open a PR if needed. Stay tuned!`, - }); - initialCommentId = initialComment.data.id; - logger.info('Posted acknowledgment comment.'); - - // Record the execution for rate limiting - await rateLimitManager.recordExecution({ - jobId, - triggeredBy, - repoOwner: originalRepoOwner, - repoName: originalRepoName, - jobType: job.type, - }); - - const cloneResult = await cloneRepository(repositoryToCloneUrl, undefined, cloneToken); - repoPath = cloneResult.path; - const git = cloneResult.git; - logger.info({ repoPath }, 'Repository cloned successfully.'); - - const botConfig = (await loadBotConfig(repoPath)) as BotConfig; - logger.info({ botConfig }, 'Loaded bot configuration.'); - - const branchName = `${envConfig.BOT_NAME}/${eventIssueNumber}-${Date.now().toString().slice(-6)}`; - await createBranch(git, branchName); - logger.info({ branchName }, 'Created new branch.'); - - const modificationResult = await processCodeModificationRequest( - commandToProcess, + // 1. Clone the repository + logger.info({ repoPath, repositoryUrl }, "Cloning repository"); + await cloneRepository(repositoryUrl, repoPath); + + // 2. Checkout the head ref of the PR + logger.info({ repoPath, headRef, headSha }, "Checking out PR head ref"); + await checkoutRemoteBranch(repoPath, headRef, headSha); + + // 3. Process the command with the LLM agent + logger.info({ commandToProcess }, "Processing command with LLM agent"); + + const agent = await createSourceModifierAgent( + { + command: commandToProcess, + botConfig: { runtime: "node" }, // Assuming node runtime for now + conversationalLogging: false, + }, repoPath, - botConfig, - true, taskCompletionTool, ); - logger.info({ modificationResult }, 'Result of applying command changes.'); - - const status = await git.status(); - const hasPendingChanges = status.files.length > 0; - logger.info( - { hasPendingChanges, changedFileCount: status.files.length }, - 'Checked repository status for changes.', - ); - - let prUrl: string | undefined; - if (hasPendingChanges && modificationResult?.objectiveAchieved) { - logger.info('Committing and pushing changes.'); - const commitMessage = `fix: Automated changes for ${originalRepoOwner}/${originalRepoName}#${eventIssueNumber} by @${envConfig.BOT_NAME}`; - - await commitChanges(git, commitMessage); - await pushChanges(git, branchName); - logger.info('Changes committed and pushed.'); - - const pr = await createPullRequest(octokit, { - owner: originalRepoOwner, - repo: originalRepoName, - title: `🤖 Fix for "${eventIssueTitle.slice(0, 40)}${eventIssueTitle.length > 40 ? '...' : ''}" by @${envConfig.BOT_NAME}`, - head: `${headBranchOwner}:${branchName}`, - base: botConfig.branches.target || 'main', - body: `This PR addresses the mention of @${envConfig.BOT_NAME} in ${originalRepoOwner}/${originalRepoName}#${eventIssueNumber}.\n\nTriggered by: @${triggeredBy}`, - labels: ['bot', envConfig.BOT_NAME.toLowerCase()], - }); - prUrl = pr; - logger.info({ prUrl }, 'Pull request created successfully.'); - } else { - logger.info('No changes to commit. Skipping PR creation.'); - } - - if (initialCommentId) { - await octokit.issues.deleteComment({ - owner: originalRepoOwner, - repo: originalRepoName, - comment_id: initialCommentId, - }); - logger.info('Deleted initial acknowledgment comment.'); - } - let replyMessage: string; - if (prUrl) { - replyMessage = `✅ @${triggeredBy}, I've created a pull request for you: ${prUrl}`; + const result = await agent.run(commandToProcess); + + if (result?.objectiveAchieved) { + logger.info({ result }, "Agent successfully completed the task"); + // 4. Update the ongoing PR (this part needs more specific implementation based on how you want to update the PR) + // For now, let's assume we just add a comment to the PR indicating success. + await addCommentToPullRequest( + originalRepoOwner, + originalRepoName, + eventPullRequestNumber, + commentId, + "Successfully applied changes based on your comment!", + installationId, + ); } else { - replyMessage = `✅ @${triggeredBy}, I received your request, but no actionable changes were identified or no changes were necessary after running checks.`; - logger.info('Replying that no changes were made or command was not actionable.'); + logger.warn({ result }, "Agent did not achieve the objective"); + await addCommentToPullRequest( + originalRepoOwner, + originalRepoName, + eventPullRequestNumber, + commentId, + "I was unable to complete the requested changes. Please check the logs for more details.", + installationId, + ); } - await octokit.issues.createComment({ - owner: originalRepoOwner, - repo: originalRepoName, - issue_number: eventIssueNumber, - body: replyMessage, - }); - logger.info('Posted final reply comment.'); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error({ error }, 'MentionOnIssueJob handling failed.'); - - if (initialCommentId) { - try { - await octokit.issues.deleteComment({ - owner: originalRepoOwner, - repo: originalRepoName, - comment_id: initialCommentId, - }); - logger.info('Deleted initial acknowledgment comment during error handling.'); - } catch (deleteError) { - logger.error({ deleteError }, 'Failed to delete initial comment during error handling.'); - } - } - - try { - await octokit.issues.createComment({ - owner: originalRepoOwner, - repo: originalRepoName, - issue_number: eventIssueNumber, - body: `🚧 Oops, @${triggeredBy}! I encountered an error while working on your request.\n\n\`\`\`\n${errorMessage}\n\`\`\`\n\nPlease check the logs if you have access.`, - }); - } catch (commentError) { - logger.error({ commentError }, 'Failed to post error comment to GitHub issue.'); - } - - if (error instanceof JobError) { - throw error; - } - throw new JobError(`Failed to handle MentionOnIssueJob ${jobId}: ${errorMessage}`); - } finally { - if (repoPath && envConfig.CLEANUP_REPOSITORIES) { - try { - await cleanupRepository(repoPath); - logger.info({ repoPath }, 'Successfully cleaned up cloned repository.'); - } catch (cleanupError) { - logger.error({ repoPath, error: cleanupError }, 'Failed to cleanup cloned repository.'); - } - } - logger.info('Finished MentionOnIssueJob handling.'); + logger.error({ error }, "Error handling AppMentionOnPullRequestJob"); + await addCommentToPullRequest( + originalRepoOwner, + originalRepoName, + eventPullRequestNumber, + commentId, + `An error occurred while processing your request: ${error instanceof Error ? error.message : String(error)}`, + installationId, + ); } } diff --git a/apps/server/src/queue/worker.ts b/apps/server/src/queue/worker.ts index f8294a6..980e5a3 100644 --- a/apps/server/src/queue/worker.ts +++ b/apps/server/src/queue/worker.ts @@ -1,9 +1,23 @@ -import { WorkerJob, isAppMentionJob, isUserMentionJob } from '../types/jobs'; -import { logger as rootLogger } from '../config/logger'; -import { JobError } from '../utils/error'; -import { handleMentionOnIssueJob } from '@/jobs/mentionHandler'; +import { WorkerJob, JobType, AppMentionOnIssueJob, AppMentionOnPullRequestJob } from "../types/jobs"; +import { logger as rootLogger } from "../config/logger"; +import { JobError } from "../utils/error"; +import { handleMentionOnIssueJob, handleAppMentionOnPullRequestJob } from "@/jobs/mentionHandler"; -const logger = rootLogger.child({ context: 'JobWorker' }); +const logger = rootLogger.child({ context: "JobWorker" }); + +/** + * Type guard for AppMentionOnIssueJob + */ +export const isAppMentionJob = (job: WorkerJob): job is AppMentionOnIssueJob => { + return job.type === JobType.AppMention; +}; + +/** + * Type guard for AppMentionOnPullRequestJob + */ +export const isAppMentionOnPullRequestJob = (job: WorkerJob): job is AppMentionOnPullRequestJob => { + return job.type === JobType.AppMentionOnPullRequest; +}; /** * Process a job from the queue by dispatching it to the appropriate handler. @@ -17,22 +31,32 @@ export const processJob = async (job: WorkerJob): Promise => { type: jobType, originalRepo: `${job.originalRepoOwner}/${job.originalRepoName}`, }, - 'Worker received job for processing', + "Worker received job for processing", ); try { - if ( - (jobType === 'app_mention' && isAppMentionJob(job)) || - (jobType === 'user_mention' && isUserMentionJob(job)) - ) { - await handleMentionOnIssueJob(job); - } else { - logger.error({ jobId, type: jobType }, 'Unknown job type received in worker'); - throw new JobError(`Unknown job type: ${jobType} for job ID: ${jobId}`); + switch (jobType) { + case JobType.AppMention: + if (isAppMentionJob(job)) { + await handleMentionOnIssueJob(job); + } else { + throw new JobError(`Invalid job payload for type ${jobType}`); + } + break; + case JobType.AppMentionOnPullRequest: + if (isAppMentionOnPullRequestJob(job)) { + await handleAppMentionOnPullRequestJob(job); + } else { + throw new JobError(`Invalid job payload for type ${jobType}`); + } + break; + default: + logger.error({ jobId, type: jobType }, "Unknown job type received in worker"); + throw new JobError(`Unknown job type: ${jobType} for job ID: ${jobId}`); } - logger.info({ jobId, type: jobType }, 'Job processing completed by handler'); + logger.info({ jobId, type: jobType }, "Job processing completed by handler"); } catch (error) { - logger.error({ jobId, type: jobType, error }, 'Job processing failed in worker'); + logger.error({ jobId, type: jobType, error }, "Job processing failed in worker"); if (error instanceof JobError) { throw error; } diff --git a/apps/server/src/routes/webhook.ts b/apps/server/src/routes/webhook.ts index 634e7f1..13f4044 100644 --- a/apps/server/src/routes/webhook.ts +++ b/apps/server/src/routes/webhook.ts @@ -3,9 +3,9 @@ import { logger } from '../config/logger'; import { jobQueue } from '../queue'; import { Webhooks } from '@octokit/webhooks'; import { envConfig } from '../config/env'; -import { AppMentionOnIssueJob } from '../types/jobs'; +import { AppMentionOnIssueJob, AppMentionOnPullRequestJob, JobType } from '../types/jobs'; import { WebhookEventName, WebhookEvent as OctokitWebhookEvent } from '@octokit/webhooks-types'; -import { isIssueCommentEvent, isIssueEvent } from '@/types/guards'; +import { isIssueCommentEvent, isIssueEvent, isPullRequestReviewCommentEvent } from '@/types/guards'; const BOT_MENTION = `@${envConfig.BOT_NAME}`.toLowerCase(); @@ -62,12 +62,21 @@ router.post('/github', async c => { let commandToProcess: string | undefined; let issueNumber: number | undefined; let issueTitle: string | undefined; + let pullRequestNumber: number | undefined; + let pullRequestTitle: string | undefined; + let pullRequestUrl: string | undefined; + let headRef: string | undefined; + let headSha: string | undefined; + let baseRef: string | undefined; + let baseSha: string | undefined; + let commentId: number | undefined; let repoOwner: string | undefined; let repoName: string | undefined; let repositoryUrl: string | undefined; let installationId: number | undefined; let senderLogin: string | undefined; let shouldProcessEvent = false; + let jobType: JobType | undefined; if (eventName === 'issues' && isIssueEvent(payload) && payload.action === 'opened') { const issuePayload = payload; @@ -82,6 +91,7 @@ router.post('/github', async c => { installationId = issuePayload.installation?.id; senderLogin = issuePayload.sender.login; shouldProcessEvent = true; + jobType = JobType.AppMention; logger.info( { deliveryId, eventName, repo: `${repoOwner}/${repoName}`, issue: issueNumber }, 'Processing mention from new issue', @@ -125,44 +135,131 @@ router.post('/github', async c => { installationId = commentPayload.installation?.id; senderLogin = commentPayload.sender.login; shouldProcessEvent = true; + jobType = JobType.AppMention; logger.info( { deliveryId, eventName, repo: `${repoOwner}/${repoName}`, issue: issueNumber }, 'Processing mention from issue comment', ); } + } else if ( + eventName === 'pull_request_review_comment' && + isPullRequestReviewCommentEvent(payload) && + payload.action === 'created' + ) { + const prCommentPayload = payload; + // Ensure it's not a comment made by the bot itself to avoid loops + if ( + prCommentPayload.sender.login.toLowerCase() === envConfig.BOT_NAME.toLowerCase() || + prCommentPayload.sender.login.toLowerCase() === `${envConfig.BOT_NAME}[bot]`.toLowerCase() + ) { + logger.info( + { + deliveryId, + eventName, + repo: `${prCommentPayload.repository.owner.login}/${prCommentPayload.repository.name}`, + pull_request: prCommentPayload.pull_request.number, + }, + 'Skipping comment from bot itself.', + ); + return c.json({ + success: true, + processed: false, + message: 'Skipping comment from bot itself.', + }); + } + + const { shouldProcess, command } = getBotCommandFromPayload(prCommentPayload.comment.body); + if (shouldProcess && command) { + commandToProcess = command; + pullRequestNumber = prCommentPayload.pull_request.number; + pullRequestTitle = prCommentPayload.pull_request.title; + pullRequestUrl = prCommentPayload.pull_request.url; + headRef = prCommentPayload.pull_request.head.ref; + headSha = prCommentPayload.pull_request.head.sha; + baseRef = prCommentPayload.pull_request.base.ref; + baseSha = prCommentPayload.pull_request.base.sha; + commentId = prCommentPayload.comment.id; + repoOwner = prCommentPayload.repository.owner.login; + repoName = prCommentPayload.repository.name; + repositoryUrl = prCommentPayload.repository.clone_url; + installationId = prCommentPayload.installation?.id; + senderLogin = prCommentPayload.sender.login; + shouldProcessEvent = true; + jobType = JobType.AppMentionOnPullRequest; + logger.info( + { deliveryId, eventName, repo: `${repoOwner}/${repoName}`, pull_request: pullRequestNumber }, + 'Processing mention from pull request review comment', + ); + } } if ( shouldProcessEvent && commandToProcess && - issueNumber && - issueTitle && repoOwner && repoName && repositoryUrl && installationId && - senderLogin + senderLogin && + jobType ) { - const jobToQueue: AppMentionOnIssueJob = { - id: deliveryId || `app_mention_${Date.now()}`, - type: 'app_mention', - originalRepoOwner: repoOwner, - originalRepoName: repoName, - eventIssueNumber: issueNumber, - eventIssueTitle: issueTitle, - commandToProcess: commandToProcess, - triggeredBy: senderLogin, - installationId: installationId, - repositoryUrl: repositoryUrl, - }; - - const queuedJobEntry = await jobQueue.addJob(jobToQueue); + if (jobType === JobType.AppMention && issueNumber && issueTitle) { + const jobToQueue: AppMentionOnIssueJob = { + id: deliveryId || `app_mention_${Date.now()}`, + type: JobType.AppMention, + originalRepoOwner: repoOwner, + originalRepoName: repoName, + eventIssueNumber: issueNumber, + eventIssueTitle: issueTitle, + commandToProcess: commandToProcess, + triggeredBy: senderLogin, + installationId: installationId, + repositoryUrl: repositoryUrl, + }; + const queuedJobEntry = await jobQueue.addJob(jobToQueue); - logger.info( - { jobId: queuedJobEntry.id, eventName, action: eventAction }, - 'AppMentionJob event queued', - ); - return c.json({ success: true, jobId: queuedJobEntry.id }); + logger.info( + { jobId: queuedJobEntry.id, eventName, action: eventAction }, + 'AppMentionJob event queued', + ); + return c.json({ success: true, jobId: queuedJobEntry.id }); + } else if ( + jobType === JobType.AppMentionOnPullRequest && + pullRequestNumber && + pullRequestTitle && + pullRequestUrl && + headRef && + headSha && + baseRef && + baseSha && + commentId + ) { + const jobToQueue: AppMentionOnPullRequestJob = { + id: deliveryId || `app_mention_pr_${Date.now()}`, + type: JobType.AppMentionOnPullRequest, + originalRepoOwner: repoOwner, + originalRepoName: repoName, + eventPullRequestNumber: pullRequestNumber, + eventPullRequestTitle: pullRequestTitle, + pullRequestUrl: pullRequestUrl, + headRef: headRef, + headSha: headSha, + baseRef: baseRef, + baseSha: baseSha, + commentId: commentId, + commandToProcess: commandToProcess, + triggeredBy: senderLogin, + installationId: installationId, + repositoryUrl: repositoryUrl, + }; + const queuedJobEntry = await jobQueue.addJob(jobToQueue); + + logger.info( + { jobId: queuedJobEntry.id, eventName, action: eventAction }, + 'AppMentionOnPullRequestJob event queued', + ); + return c.json({ success: true, jobId: queuedJobEntry.id }); + } } else { logger.info( { deliveryId, eventName, action: eventAction }, diff --git a/apps/server/src/types/jobs.ts b/apps/server/src/types/jobs.ts index aa6b498..8fd5b13 100644 --- a/apps/server/src/types/jobs.ts +++ b/apps/server/src/types/jobs.ts @@ -1,60 +1,35 @@ -interface BaseJob { - id: string; // Unique job ID, typically from queue or delivery ID - originalRepoOwner: string; - originalRepoName: string; - eventIssueNumber: number; - eventIssueTitle: string; +export enum JobType { + AppMention = 'app_mention', + AppMentionOnPullRequest = 'app_mention_on_pull_request', } -/** - * Common properties for all jobs processed by the worker. - */ -interface BaseMentionOnIssueJob extends BaseJob { - commandToProcess: string; - triggeredBy: string; -} - -/** - * Job triggered by a GitHub App mention (issue or issue_comment). - */ -export interface AppMentionOnIssueJob extends BaseMentionOnIssueJob { - type: 'app_mention'; +export interface BaseJob { + id: string; + type: JobType; + originalRepoOwner: string; + originalRepoName: string; installationId: number; + triggeredBy: string; + commandToProcess: string; repositoryUrl: string; } -/** - * Job triggered by a mention of the dedicated GitHub User (@BOT_NAME). - */ -export interface UserMentionOnIssueJob extends BaseMentionOnIssueJob { - type: 'user_mention'; -} - -/** - * Union type for all possible jobs in the queue. - */ -export type QueuedJob = AppMentionOnIssueJob | UserMentionOnIssueJob; - -// Type Guards -export function isAppMentionJob(job: QueuedJob): job is AppMentionOnIssueJob { - return job.type === 'app_mention'; +export interface AppMentionOnIssueJob extends BaseJob { + type: JobType.AppMention; + eventIssueNumber: number; + eventIssueTitle: string; } -export function isUserMentionJob(job: QueuedJob): job is UserMentionOnIssueJob { - return job.type === 'user_mention'; +export interface AppMentionOnPullRequestJob extends BaseJob { + type: JobType.AppMentionOnPullRequest; + eventPullRequestNumber: number; + eventPullRequestTitle: string; + pullRequestUrl: string; + headRef: string; + headSha: string; + baseRef: string; + baseSha: string; + commentId: number; } -export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'; - -/** - * Represents the structure of a job as expected by the core job processing worker. - * It includes all specific job fields at the top level, along with queue management state. - * Timestamps are Date objects. - */ -export type WorkerJob = QueuedJob & { - status: JobStatus; - createdAt: Date; - updatedAt: Date; - attempts: number; - logs: string[]; -}; +export type Job = AppMentionOnIssueJob | AppMentionOnPullRequestJob;