kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import type { PluginContext, TaskTitleChangedEvent } from "../../types";
2import type { GitHubConfig } from "../config";
3import {
4 findExternalLinksByTask,
5 updateExternalLink,
6} from "../services/link-manager";
7import { getGithubApp, getInstallationIdForRepo } from "../utils/github-app";
8
9export async function handleTaskTitleChanged(
10 event: TaskTitleChangedEvent,
11 context: PluginContext,
12): Promise<void> {
13 const githubApp = getGithubApp();
14 if (!githubApp) {
15 return;
16 }
17
18 const config = context.config as GitHubConfig;
19 const { repositoryOwner, repositoryName } = config;
20
21 try {
22 const links = await findExternalLinksByTask(event.taskId);
23 const issueLink = links.find(
24 (link) =>
25 link.integrationId === context.integrationId &&
26 link.resourceType === "issue",
27 );
28
29 if (!issueLink) {
30 return;
31 }
32
33 const metadata = issueLink.metadata ? JSON.parse(issueLink.metadata) : {};
34
35 // LOOP PREVENTION: Check if this update originated from GitHub
36 const lastTitleSync = metadata.lastSync?.title;
37 if (lastTitleSync) {
38 // Skip if value unchanged and last sync was from GitHub
39 if (
40 lastTitleSync.value === event.newTitle &&
41 lastTitleSync.source === "github"
42 ) {
43 console.log("Skipping title sync - already synced from GitHub");
44 return;
45 }
46
47 // Skip if recent sync (within 2 seconds) to prevent rapid loops
48 const timeSinceLastSync =
49 Date.now() - new Date(lastTitleSync.timestamp).getTime();
50 if (timeSinceLastSync < 2000) {
51 console.log(
52 `Skipping title sync - recent sync detected (${timeSinceLastSync}ms ago)`,
53 );
54 return;
55 }
56 }
57
58 let installationId = config.installationId;
59 if (!installationId) {
60 installationId = await getInstallationIdForRepo(
61 repositoryOwner,
62 repositoryName,
63 );
64 }
65
66 const octokit = await githubApp.getInstallationOctokit(installationId);
67 const issueNumber = Number.parseInt(issueLink.externalId, 10);
68
69 await octokit.rest.issues.update({
70 owner: repositoryOwner,
71 repo: repositoryName,
72 issue_number: issueNumber,
73 title: event.newTitle,
74 });
75
76 // Update metadata to track this sync
77 await updateExternalLink(issueLink.id, {
78 title: event.newTitle,
79 metadata: {
80 ...metadata,
81 lastSync: {
82 ...metadata.lastSync,
83 title: {
84 timestamp: new Date().toISOString(),
85 source: "kaneo",
86 value: event.newTitle,
87 },
88 },
89 },
90 });
91
92 console.log(`Synced task title to GitHub issue #${issueNumber}`);
93 } catch (error) {
94 console.error("Failed to update GitHub issue title:", error);
95 }
96}