diff --git a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts index 739c4f473..4283736b6 100644 --- a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts +++ b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts @@ -5,6 +5,7 @@ import { EventBusAdapter } from "shared/adapters/ioredis/EventBusAdapter" import { createNeo4jDataSource } from "shared/adapters/neo4j/dataSource" import { makeSettingsReaderAdapter } from "shared/adapters/neo4j/repositories/SettingsReaderAdapter" import { setAccessToken } from "shared/auth" +import { createIssueComment, getIssueComments, updateIssueComment } from "shared/lib/github/issues" import { getPrivateKeyFromFile } from "shared/services/fs" import { autoResolveIssue as autoResolveIssueWorkflow } from "shared/usecases/workflows/autoResolveIssue" @@ -40,6 +41,62 @@ export type AutoResolveJobData = { githubInstallationId: string } +function getAppBaseUrl(): string { + return process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000" +} + +function buildWorkflowRunLink(id: string): string { + return `${getAppBaseUrl()}/workflow-runs/${id}` +} + +async function findStatusCommentId( + repoFullName: string, + issueNumber: number, + workflowId: string +): Promise { + try { + const comments = await getIssueComments({ repoFullName, issueNumber }) + const marker = `` + const match = comments.find((c) => typeof c.body === "string" && c.body.includes(marker)) + return match?.id ?? null + } catch (e) { + console.warn("Failed to list issue comments to find status marker:", e) + return null + } +} + +function renderStatusComment(workflowId: string, status: "queued" | "running" | "completed" | "failed", extra?: string): string { + const link = buildWorkflowRunLink(workflowId) + const lines = [ + `[Issue to PR] Workflow status for this issue`, + ``, + `Status: ${status}`, + `Details: ${link}`, + ``, + ``, + ] + if (extra && extra.trim().length > 0) { + lines.splice(3, 0, `Note: ${extra}`) + } + return lines.join("\n") +} + +async function upsertStatusComment( + repoFullName: string, + issueNumber: number, + workflowId: string, + status: "queued" | "running" | "completed" | "failed", + extra?: string +) { + const body = renderStatusComment(workflowId, status, extra) + const commentId = await findStatusCommentId(repoFullName, issueNumber, workflowId) + if (commentId) { + await updateIssueComment({ commentId, repoFullName, comment: body }) + } else { + await createIssueComment({ repoFullName, issueNumber, comment: body }) + } +} + export async function autoResolveIssue( jobId: string, { @@ -107,19 +164,46 @@ export async function autoResolveIssue( await publishJobStatus(jobId, "Fetching issue and running LLM") - const result = await autoResolveIssueWorkflow( - { - issueNumber, - repoFullName, - login: githubLogin, - branch, - }, - { - settings: settingsAdapter, - eventBus: eventBusAdapter, + // Update GitHub status comment to running (create if missing) + try { + await upsertStatusComment(repoFullName, issueNumber, jobId, "running") + } catch (e) { + console.warn("Failed to upsert running status comment:", e) + } + + try { + const result = await autoResolveIssueWorkflow( + { + issueNumber, + repoFullName, + login: githubLogin, + branch, + jobId, // ensure workflow run id matches job id so URLs align + }, + { + settings: settingsAdapter, + eventBus: eventBusAdapter, + } + ) + + // Update GitHub status comment to completed + try { + await upsertStatusComment(repoFullName, issueNumber, jobId, "completed") + } catch (e) { + console.warn("Failed to upsert completed status comment:", e) } - ) - // Handler will publish the completion status - return result.messages + // Handler will publish the completion status + return result.messages + } catch (error) { + // Update GitHub status comment to failed + try { + const extra = error instanceof Error ? error.message : String(error) + await upsertStatusComment(repoFullName, issueNumber, jobId, "failed", extra) + } catch (e) { + console.warn("Failed to upsert failed status comment:", e) + } + throw error + } } + diff --git a/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler.ts b/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler.ts index d5079d619..10ab48d9b 100644 --- a/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler.ts +++ b/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler.ts @@ -1,12 +1,19 @@ import { QueueEnum, WORKFLOW_JOBS_QUEUE } from "shared/entities/Queue" import { addJob } from "shared/services/job" +import { createIssueComment } from "@/lib/github/issues" +import { runWithInstallationId } from "@/lib/utils/utils-server" import type { IssuesPayload } from "@/lib/webhook/github/types" +function getAppBaseUrl(): string { + return process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000" +} + /** * Handler: Issue labeled with "I2PR: Resolve Issue" * - Enqueues the autoResolveIssue job onto the workflow-jobs queue * - Includes installation id and labeler login in job data + * - Posts a GitHub issue comment with a link to the workflow run and updates it as the job progresses */ export async function handleIssueLabelAutoResolve({ payload, @@ -30,7 +37,8 @@ export async function handleIssueLabelAutoResolve({ const queue: QueueEnum = WORKFLOW_JOBS_QUEUE - await addJob( + // Enqueue job and capture the job id + const jobId = await addJob( queue, { name: "autoResolveIssue", @@ -44,4 +52,34 @@ export async function handleIssueLabelAutoResolve({ {}, redisUrl ) + + // Create initial status comment linking to the workflow run page if we have a job id + if (jobId) { + const link = `${getAppBaseUrl()}/workflow-runs/${jobId}` + const body = [ + `[Issue to PR] Workflow queued for auto-resolve`, + "", + `Status: queued`, + `Details: ${link}`, + "", + ``, + ].join("\n") + + // Use installation context to authenticate GitHub App API requests + await runWithInstallationId(String(installationId), async () => { + try { + await createIssueComment({ + issueNumber, + repoFullName, + comment: body, + }) + } catch (e) { + console.error( + "Failed to post initial workflow status comment:", + e + ) + } + }) + } } +