Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 97 additions & 13 deletions apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<number | null> {
try {
const comments = await getIssueComments({ repoFullName, issueNumber })
const marker = `<!-- workflow-run:${workflowId} -->`
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}`,
``,
`<!-- workflow-run:${workflowId} -->`,
]
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,
{
Expand Down Expand Up @@ -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
}
}

Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand All @@ -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}`,
"",
`<!-- workflow-run:${jobId} -->`,
].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
)
}
})
}
}