kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 39e2dfae265f26c8d6d888a560f50ab2d5d58b3f 187 lines 5.4 kB view raw
1import { and, eq } from "drizzle-orm"; 2import db from "../../../database"; 3import { columnTable, projectTable, taskTable } from "../../../database/schema"; 4import getNextTaskNumber from "../../../task/controllers/get-next-task-number"; 5import type { GitHubConfig } from "../config"; 6import { createExternalLink, findExternalLink } from "../services/link-manager"; 7import { findAllIntegrationsByRepo } from "../services/task-service"; 8import { 9 extractIssuePriority, 10 extractIssueStatus, 11} from "../utils/extract-priority"; 12import { formatTaskDescriptionFromIssue } from "../utils/format"; 13import { getGithubApp } from "../utils/github-app"; 14import { addLabelsToIssue } from "../utils/labels"; 15import { resolveTargetStatus } from "../utils/resolve-column"; 16 17type IssueOpenedPayload = { 18 action: string; 19 issue: { 20 number: number; 21 title: string; 22 body: string | null; 23 html_url: string; 24 labels?: Array<string | { name?: string }>; 25 user: { login: string } | null; 26 }; 27 repository: { 28 owner: { login: string }; 29 name: string; 30 full_name: string; 31 }; 32}; 33 34export async function handleIssueOpened(payload: IssueOpenedPayload) { 35 const githubApp = getGithubApp(); 36 if (!githubApp) { 37 return; 38 } 39 40 const { issue, repository } = payload; 41 42 const integrations = await findAllIntegrationsByRepo( 43 repository.owner.login, 44 repository.name, 45 ); 46 47 if (integrations.length === 0) { 48 return; 49 } 50 51 for (const integration of integrations) { 52 const config = JSON.parse(integration.config) as GitHubConfig; 53 const projectId = integration.projectId; 54 55 const priority = extractIssuePriority(issue.labels); 56 const status = extractIssueStatus(issue.labels); 57 58 const existingLink = await findExternalLink( 59 integration.id, 60 "issue", 61 issue.number.toString(), 62 ); 63 64 if (existingLink) { 65 console.log( 66 `Issue #${issue.number} already linked to task ${existingLink.taskId} in project ${projectId}, skipping`, 67 ); 68 continue; 69 } 70 71 const nextTaskNumber = await getNextTaskNumber(projectId); 72 73 const resolvedStatus = await resolveTargetStatus( 74 projectId, 75 "issue_opened", 76 status || "to-do", 77 ); 78 79 const targetStatus = resolvedStatus; 80 const targetColumn = await db.query.columnTable.findFirst({ 81 where: and( 82 eq(columnTable.projectId, projectId), 83 eq(columnTable.slug, targetStatus), 84 ), 85 }); 86 87 const taskValues: typeof taskTable.$inferInsert = { 88 projectId, 89 userId: null, 90 title: issue.title, 91 description: formatTaskDescriptionFromIssue(issue.body), 92 status: targetStatus, 93 columnId: targetColumn?.id ?? null, 94 priority: null, 95 number: nextTaskNumber + 1, 96 }; 97 98 if (priority) taskValues.priority = priority; 99 100 const [createdTask] = await db 101 .insert(taskTable) 102 .values(taskValues) 103 .returning(); 104 105 if (!createdTask) { 106 console.error("Failed to create task from GitHub issue"); 107 continue; 108 } 109 110 await createExternalLink({ 111 taskId: createdTask.id, 112 integrationId: integration.id, 113 resourceType: "issue", 114 externalId: issue.number.toString(), 115 url: issue.html_url, 116 title: issue.title, 117 metadata: { 118 state: "open", 119 createdFrom: "github", 120 author: issue.user?.login, 121 }, 122 }); 123 124 const project = await db.query.projectTable.findFirst({ 125 where: eq(projectTable.id, projectId), 126 }); 127 128 if (!project) { 129 console.error("Project not found for task linking comment"); 130 continue; 131 } 132 133 const clientUrl = process.env.KANEO_CLIENT_URL || "http://localhost:5173"; 134 const taskUrl = `${clientUrl}/dashboard/workspace/${project.workspaceId}/project/${projectId}/task/${createdTask.id}`; 135 const taskIdentifier = `${project.slug.toUpperCase()}-${createdTask.number}`; 136 137 try { 138 let installationId = config.installationId; 139 if (!installationId) { 140 const { data: installation } = 141 await githubApp.octokit.rest.apps.getRepoInstallation({ 142 owner: repository.owner.login, 143 repo: repository.name, 144 }); 145 installationId = installation.id; 146 } 147 148 const octokit = await githubApp.getInstallationOctokit(installationId); 149 150 const existingLabels = 151 issue.labels 152 ?.map((label) => (typeof label === "string" ? label : label.name)) 153 .filter(Boolean) || []; 154 155 const labelsToAdd: string[] = []; 156 157 if (priority && !existingLabels.includes(`priority:${priority}`)) { 158 labelsToAdd.push(`priority:${priority}`); 159 } 160 161 if (status && !existingLabels.includes(`status:${status}`)) { 162 labelsToAdd.push(`status:${status}`); 163 } 164 165 if (labelsToAdd.length > 0) { 166 await addLabelsToIssue( 167 octokit, 168 repository.owner.login, 169 repository.name, 170 issue.number, 171 labelsToAdd, 172 ); 173 } 174 175 if (config.commentTaskLinkOnGitHubIssue !== false) { 176 await octokit.rest.issues.createComment({ 177 owner: repository.owner.login, 178 repo: repository.name, 179 issue_number: issue.number, 180 body: `[${taskIdentifier}](${taskUrl})`, 181 }); 182 } 183 } catch (error) { 184 console.error("Failed to process GitHub issue:", error); 185 } 186 } 187}