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 cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 129 lines 3.6 kB view raw
1import { and, eq } from "drizzle-orm"; 2import db from "../../../database"; 3import { externalLinkTable, taskTable } from "../../../database/schema"; 4import { publishEvent } from "../../../events"; 5import { updateExternalLink } from "../../github/services/link-manager"; 6import { updateTaskStatus } from "../../github/services/task-service"; 7import { 8 findAllIntegrationsByGiteaRepo, 9 repoOwnerLogin, 10} from "../services/integration-lookup"; 11import { 12 OUTBOUND_STATE_ECHO_WINDOW_MS, 13 parseIssueUpdatedAtMs, 14} from "../utils/outbound-echo"; 15import { resolveTargetStatus } from "../utils/resolve-column"; 16import { baseUrlFromRepositoryHtmlUrl } from "../utils/webhook-repo"; 17 18type IssueClosedPayload = { 19 action: string; 20 issue: { 21 number: number; 22 title: string; 23 html_url: string; 24 state: string; 25 updated_at?: string; 26 }; 27 repository: { 28 owner: { login?: string; username?: string }; 29 name: string; 30 html_url: string; 31 }; 32}; 33 34export async function handleGiteaIssueClosed(payload: IssueClosedPayload) { 35 if (payload.action !== "closed") { 36 return; 37 } 38 39 const { issue, repository } = payload; 40 41 const baseUrl = baseUrlFromRepositoryHtmlUrl(repository.html_url); 42 if (!baseUrl) return; 43 44 const owner = repoOwnerLogin(repository); 45 const integrations = await findAllIntegrationsByGiteaRepo( 46 baseUrl, 47 owner, 48 repository.name, 49 ); 50 51 for (const integration of integrations) { 52 const externalLink = await db.query.externalLinkTable.findFirst({ 53 where: and( 54 eq(externalLinkTable.integrationId, integration.id), 55 eq(externalLinkTable.resourceType, "issue"), 56 eq(externalLinkTable.externalId, issue.number.toString()), 57 ), 58 }); 59 60 if (!externalLink) { 61 continue; 62 } 63 64 const task = await db.query.taskTable.findFirst({ 65 where: eq(taskTable.id, externalLink.taskId), 66 }); 67 68 if (!task) { 69 continue; 70 } 71 72 let existingMetadata: Record<string, unknown> = {}; 73 if (externalLink.metadata) { 74 try { 75 existingMetadata = JSON.parse(externalLink.metadata) as Record< 76 string, 77 unknown 78 >; 79 } catch (error) { 80 console.warn("Failed to parse Gitea issue metadata for close sync", { 81 externalLinkId: externalLink.id, 82 metadata: externalLink.metadata, 83 error, 84 }); 85 } 86 } 87 88 const lastOutbound = existingMetadata.lastOutboundStateSyncAt; 89 if (typeof lastOutbound === "number" && Number.isFinite(lastOutbound)) { 90 const eventMs = parseIssueUpdatedAtMs(issue); 91 if ( 92 eventMs !== null && 93 Math.abs(eventMs - lastOutbound) <= OUTBOUND_STATE_ECHO_WINDOW_MS 94 ) { 95 continue; 96 } 97 } 98 99 const targetStatus = await resolveTargetStatus( 100 task.projectId, 101 "issue_closed", 102 "done", 103 ); 104 105 const statusResult = await updateTaskStatus(task.id, targetStatus); 106 if ( 107 statusResult.applied && 108 statusResult.before.status !== statusResult.after.status 109 ) { 110 await publishEvent("task.status_changed", { 111 taskId: statusResult.after.id, 112 projectId: statusResult.after.projectId, 113 userId: null, 114 oldStatus: statusResult.before.status, 115 newStatus: statusResult.after.status, 116 title: statusResult.after.title, 117 assigneeId: statusResult.after.userId, 118 type: "status_changed", 119 }); 120 } 121 122 await updateExternalLink(externalLink.id, { 123 metadata: { 124 ...existingMetadata, 125 state: "closed", 126 }, 127 }); 128 } 129}