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