kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { eq } from "drizzle-orm";
2import db from "../../../database";
3import { externalLinkTable } from "../../../database/schema";
4import type { GiteaConfig } from "../config";
5import { createGiteaClient } from "./gitea-api";
6
7const namedColorToHex: Record<string, string> = {
8 red: "EF4444",
9 orange: "F97316",
10 amber: "F59E0B",
11 yellow: "EAB308",
12 lime: "84CC16",
13 green: "22C55E",
14 emerald: "10B981",
15 teal: "14B8A6",
16 cyan: "06B6D4",
17 sky: "0EA5E9",
18 blue: "3B82F6",
19 indigo: "6366F1",
20 violet: "8B5CF6",
21 purple: "A855F7",
22 fuchsia: "D946EF",
23 pink: "EC4899",
24 rose: "F43F5E",
25 gray: "6B7280",
26 slate: "64748B",
27 zinc: "71717A",
28 neutral: "737373",
29 stone: "78716C",
30};
31
32function toHexColor(color: string): string {
33 const lower = color.toLowerCase().replace(/^#/, "");
34 if (namedColorToHex[lower]) {
35 return namedColorToHex[lower];
36 }
37 if (/^[0-9a-f]{6}$/i.test(lower)) {
38 return lower;
39 }
40 if (/^[0-9a-f]{3}$/i.test(lower)) {
41 const [r, g, b] = lower.split("");
42 return `${r}${r}${g}${g}${b}${b}`;
43 }
44 return "6B7280";
45}
46
47async function getGiteaIssueContext(taskId: string) {
48 const externalLinks = await db.query.externalLinkTable.findMany({
49 where: eq(externalLinkTable.taskId, taskId),
50 with: {
51 integration: true,
52 },
53 });
54
55 const externalLink = externalLinks.find(
56 (link) =>
57 link.resourceType === "issue" && link.integration?.type === "gitea",
58 );
59
60 if (!externalLink) {
61 return null;
62 }
63
64 const integration = externalLink.integration;
65 if (!integration) {
66 return null;
67 }
68
69 let config: GiteaConfig;
70 try {
71 config = JSON.parse(integration.config) as GiteaConfig;
72 } catch {
73 return null;
74 }
75
76 if (!config.accessToken || !config.baseUrl) {
77 return null;
78 }
79
80 const client = createGiteaClient(config);
81 const issueNumber = Number.parseInt(externalLink.externalId, 10);
82 if (Number.isNaN(issueNumber)) {
83 console.warn("Invalid Gitea issue externalId for label sync", {
84 externalLinkId: externalLink.id,
85 externalId: externalLink.externalId,
86 taskId,
87 });
88 return null;
89 }
90
91 return {
92 client,
93 config,
94 issueNumber,
95 };
96}
97
98export async function syncLabelToGitea(
99 taskId: string,
100 labelName: string,
101 labelColor: string,
102) {
103 const ctx = await getGiteaIssueContext(taskId);
104 if (!ctx) return;
105
106 const { client, config, issueNumber } = ctx;
107 const color = toHexColor(labelColor);
108
109 const labels = await client.listLabels(
110 config.repositoryOwner,
111 config.repositoryName,
112 );
113 let label = labels.find((l) => l.name === labelName);
114
115 if (!label) {
116 try {
117 label = await client.createLabel(
118 config.repositoryOwner,
119 config.repositoryName,
120 labelName,
121 color,
122 );
123 } catch (error) {
124 console.error(`Failed to create label "${labelName}" in Gitea:`, error);
125 return;
126 }
127 }
128
129 try {
130 const issue = await client.getIssue(
131 config.repositoryOwner,
132 config.repositoryName,
133 issueNumber,
134 );
135 const existingIds = (issue.labels ?? []).map((l) => l.id);
136 if (existingIds.includes(label.id)) {
137 return;
138 }
139 await client.addLabelsToIssue(
140 config.repositoryOwner,
141 config.repositoryName,
142 issueNumber,
143 [label.id],
144 );
145 } catch (error) {
146 console.error(`Failed to add label "${labelName}" to Gitea issue:`, error);
147 }
148}
149
150export async function removeLabelFromGitea(taskId: string, labelName: string) {
151 const ctx = await getGiteaIssueContext(taskId);
152 if (!ctx) return;
153
154 const { client, config, issueNumber } = ctx;
155
156 const labels = await client.listLabels(
157 config.repositoryOwner,
158 config.repositoryName,
159 );
160 const label = labels.find((l) => l.name === labelName);
161 if (!label) return;
162
163 try {
164 await client.removeLabelFromIssue(
165 config.repositoryOwner,
166 config.repositoryName,
167 issueNumber,
168 label.id,
169 );
170 } catch (error) {
171 console.error(
172 `Failed to remove label "${labelName}" from Gitea issue:`,
173 error,
174 );
175 }
176}