kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
at main 101 lines 3.0 kB view raw
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}