kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
at main 156 lines 3.4 kB view raw
1import { eq } from "drizzle-orm"; 2import db from "../../../database"; 3import { externalLinkTable } from "../../../database/schema"; 4import { getInstallationOctokit } from "./github-app"; 5 6const namedColorToHex: Record<string, string> = { 7 red: "EF4444", 8 orange: "F97316", 9 amber: "F59E0B", 10 yellow: "EAB308", 11 lime: "84CC16", 12 green: "22C55E", 13 emerald: "10B981", 14 teal: "14B8A6", 15 cyan: "06B6D4", 16 sky: "0EA5E9", 17 blue: "3B82F6", 18 indigo: "6366F1", 19 violet: "8B5CF6", 20 purple: "A855F7", 21 fuchsia: "D946EF", 22 pink: "EC4899", 23 rose: "F43F5E", 24 gray: "6B7280", 25 slate: "64748B", 26 zinc: "71717A", 27 neutral: "737373", 28 stone: "78716C", 29}; 30 31function toHexColor(color: string): string { 32 const lower = color.toLowerCase().replace(/^#/, ""); 33 if (namedColorToHex[lower]) { 34 return namedColorToHex[lower]; 35 } 36 if (/^[0-9a-f]{6}$/i.test(lower)) { 37 return lower; 38 } 39 if (/^[0-9a-f]{3}$/i.test(lower)) { 40 const [r, g, b] = lower.split(""); 41 return `${r}${r}${g}${g}${b}${b}`; 42 } 43 return "6B7280"; 44} 45 46async function getGitHubContext(taskId: string) { 47 const externalLink = await db.query.externalLinkTable.findFirst({ 48 where: eq(externalLinkTable.taskId, taskId), 49 with: { 50 integration: true, 51 }, 52 }); 53 54 if (!externalLink || externalLink.resourceType !== "issue") { 55 return null; 56 } 57 58 const integration = externalLink.integration; 59 if (!integration || integration.type !== "github") { 60 return null; 61 } 62 63 let config: { 64 repositoryOwner: string; 65 repositoryName: string; 66 installationId?: number; 67 }; 68 try { 69 config = JSON.parse(integration.config); 70 } catch { 71 return null; 72 } 73 74 if (!config.installationId) { 75 return null; 76 } 77 78 const octokit = await getInstallationOctokit(config.installationId); 79 if (!octokit) { 80 return null; 81 } 82 83 return { 84 octokit, 85 owner: config.repositoryOwner, 86 repo: config.repositoryName, 87 issueNumber: Number.parseInt(externalLink.externalId, 10), 88 }; 89} 90 91export async function syncLabelToGitHub( 92 taskId: string, 93 labelName: string, 94 labelColor: string, 95) { 96 const ctx = await getGitHubContext(taskId); 97 if (!ctx) return; 98 99 const { octokit, owner, repo, issueNumber } = ctx; 100 const color = toHexColor(labelColor); 101 102 try { 103 await octokit.rest.issues.getLabel({ 104 owner, 105 repo, 106 name: labelName, 107 }); 108 } catch { 109 try { 110 await octokit.rest.issues.createLabel({ 111 owner, 112 repo, 113 name: labelName, 114 color, 115 }); 116 } catch (createError) { 117 console.error( 118 `Failed to create label "${labelName}" in GitHub:`, 119 createError, 120 ); 121 return; 122 } 123 } 124 125 try { 126 await octokit.rest.issues.addLabels({ 127 owner, 128 repo, 129 issue_number: issueNumber, 130 labels: [labelName], 131 }); 132 } catch (error) { 133 console.error(`Failed to add label "${labelName}" to GitHub issue:`, error); 134 } 135} 136 137export async function removeLabelFromGitHub(taskId: string, labelName: string) { 138 const ctx = await getGitHubContext(taskId); 139 if (!ctx) return; 140 141 const { octokit, owner, repo, issueNumber } = ctx; 142 143 try { 144 await octokit.rest.issues.removeLabel({ 145 owner, 146 repo, 147 issue_number: issueNumber, 148 name: labelName, 149 }); 150 } catch (error) { 151 console.error( 152 `Failed to remove label "${labelName}" from GitHub issue:`, 153 error, 154 ); 155 } 156}