kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { eq } from "drizzle-orm";
2import db from "../../../database";
3import { taskTable } from "../../../database/schema";
4import { findExternalLink, updateExternalLink } from "../services/link-manager";
5import { findAllIntegrationsByRepo } from "../services/task-service";
6import { formatTaskDescriptionFromIssue } from "../utils/format";
7
8type IssueEditedPayload = {
9 action: string;
10 issue: {
11 number: number;
12 title: string;
13 body: string | null;
14 html_url: string;
15 };
16 changes?: {
17 title?: {
18 from: string;
19 };
20 body?: {
21 from: string;
22 };
23 };
24 repository: {
25 owner: { login: string };
26 name: string;
27 full_name: string;
28 };
29};
30
31export async function handleIssueEdited(payload: IssueEditedPayload) {
32 const { issue, repository, changes } = payload;
33
34 if (!changes?.title && !changes?.body) {
35 console.log(
36 `Issue #${issue.number} edited but no title/body changes detected`,
37 );
38 return;
39 }
40
41 const integrations = await findAllIntegrationsByRepo(
42 repository.owner.login,
43 repository.name,
44 );
45
46 for (const integration of integrations) {
47 const externalLink = await findExternalLink(
48 integration.id,
49 "issue",
50 issue.number.toString(),
51 );
52
53 if (!externalLink) {
54 continue;
55 }
56
57 const task = await db.query.taskTable.findFirst({
58 where: eq(taskTable.id, externalLink.taskId),
59 });
60
61 if (!task) {
62 console.error(`Task ${externalLink.taskId} not found`);
63 continue;
64 }
65
66 const metadata = externalLink.metadata
67 ? JSON.parse(externalLink.metadata)
68 : {};
69
70 const updateData: Record<string, unknown> = {};
71 const updatedMetadata = { ...metadata };
72
73 if (!updatedMetadata.lastSync) {
74 updatedMetadata.lastSync = {};
75 }
76
77 if (changes.title) {
78 const lastTitleSync = metadata.lastSync?.title;
79
80 let shouldUpdateTitle = true;
81
82 if (lastTitleSync) {
83 if (
84 lastTitleSync.value === issue.title &&
85 lastTitleSync.source === "kaneo"
86 ) {
87 console.log("Skipping title update - already synced from Kaneo");
88 shouldUpdateTitle = false;
89 }
90
91 const timeSinceLastSync =
92 Date.now() - new Date(lastTitleSync.timestamp).getTime();
93 if (timeSinceLastSync < 2000 && shouldUpdateTitle) {
94 console.log(
95 `Skipping title update - recent sync detected (${timeSinceLastSync}ms ago)`,
96 );
97 shouldUpdateTitle = false;
98 }
99 }
100
101 if (shouldUpdateTitle) {
102 updateData.title = issue.title;
103 updatedMetadata.lastSync.title = {
104 timestamp: new Date().toISOString(),
105 source: "github",
106 value: issue.title,
107 };
108 console.log(
109 `Updating task title from GitHub: "${changes.title.from}" → "${issue.title}"`,
110 );
111 }
112 }
113
114 if (changes.body) {
115 const lastDescSync = metadata.lastSync?.description;
116 const formattedDescription = formatTaskDescriptionFromIssue(issue.body);
117
118 let shouldUpdateDescription = true;
119
120 if (lastDescSync) {
121 if (
122 lastDescSync.value === formattedDescription &&
123 lastDescSync.source === "kaneo"
124 ) {
125 console.log(
126 "Skipping description update - already synced from Kaneo",
127 );
128 shouldUpdateDescription = false;
129 }
130
131 const timeSinceLastSync =
132 Date.now() - new Date(lastDescSync.timestamp).getTime();
133 if (timeSinceLastSync < 2000 && shouldUpdateDescription) {
134 console.log(
135 `Skipping description update - recent sync detected (${timeSinceLastSync}ms ago)`,
136 );
137 shouldUpdateDescription = false;
138 }
139 }
140
141 if (shouldUpdateDescription) {
142 updateData.description = formattedDescription;
143 updatedMetadata.lastSync.description = {
144 timestamp: new Date().toISOString(),
145 source: "github",
146 value: formattedDescription,
147 };
148 console.log("Updating task description from GitHub");
149 }
150 }
151
152 if (Object.keys(updateData).length > 0) {
153 await db
154 .update(taskTable)
155 .set(updateData)
156 .where(eq(taskTable.id, task.id));
157
158 await updateExternalLink(externalLink.id, {
159 title: issue.title,
160 metadata: updatedMetadata,
161 });
162
163 console.log(
164 `Synced ${Object.keys(updateData).join(", ")} from GitHub issue #${issue.number} to task ${task.id}`,
165 );
166 } else {
167 console.log(
168 `No updates needed for task ${task.id} from issue #${issue.number}`,
169 );
170 }
171
172 return;
173 }
174}