kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { and, eq, sql } from "drizzle-orm";
2import db from "../../database";
3import {
4 externalLinkTable,
5 integrationTable,
6 taskTable,
7} from "../../database/schema";
8import { defaultGitHubConfig } from "./config";
9
10async function tableExists(tableName: string): Promise<boolean> {
11 try {
12 const result = await db.execute(sql`
13 SELECT EXISTS (
14 SELECT FROM information_schema.tables
15 WHERE table_schema = 'public'
16 AND table_name = ${tableName}
17 );
18 `);
19 return (result.rows[0] as { exists: boolean })?.exists === true;
20 } catch {
21 return false;
22 }
23}
24
25export async function migrateGitHubIntegration() {
26 const oldTableExists = await tableExists("github_integration");
27
28 if (!oldTableExists) {
29 console.log("No old github_integration table found, skipping migration");
30 return;
31 }
32
33 console.log("🔄 Starting GitHub integration migration...");
34
35 try {
36 const oldIntegrations = await db.query.githubIntegrationTable.findMany();
37
38 if (oldIntegrations.length === 0) {
39 console.log("No old integrations to migrate");
40 await dropOldTable();
41 return;
42 }
43
44 let migratedCount = 0;
45
46 for (const old of oldIntegrations) {
47 const existingIntegration = await db.query.integrationTable.findFirst({
48 where: and(
49 eq(integrationTable.projectId, old.projectId),
50 eq(integrationTable.type, "github"),
51 ),
52 });
53
54 if (existingIntegration) {
55 continue;
56 }
57
58 await db.insert(integrationTable).values({
59 projectId: old.projectId,
60 type: "github",
61 config: JSON.stringify({
62 repositoryOwner: old.repositoryOwner,
63 repositoryName: old.repositoryName,
64 installationId: old.installationId,
65 ...defaultGitHubConfig,
66 }),
67 isActive: old.isActive ?? true,
68 createdAt: old.createdAt,
69 updatedAt: old.updatedAt,
70 });
71
72 migratedCount++;
73 }
74
75 console.log(`✓ Migrated ${migratedCount} integrations`);
76
77 await migrateTaskLinks();
78
79 await dropOldTable();
80
81 console.log("✅ GitHub integration migration complete!");
82 } catch (error) {
83 console.error("Failed to migrate GitHub integration:", error);
84 throw error;
85 }
86}
87
88async function migrateTaskLinks() {
89 console.log("🔄 Migrating task links from descriptions...");
90
91 const tasks = await db.query.taskTable.findMany();
92
93 let linksCreated = 0;
94 let descriptionsUpdated = 0;
95
96 for (const task of tasks) {
97 if (!task.description) continue;
98
99 const linkMatch = task.description.match(
100 /(Linked to|Created from) GitHub issue: (https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+))/,
101 );
102
103 if (!linkMatch) continue;
104
105 const linkType = linkMatch[1];
106 const url = linkMatch[2];
107 const owner = linkMatch[3];
108 const repo = linkMatch[4];
109 const issueNumber = linkMatch[5];
110
111 if (!url || !owner || !repo || !issueNumber) continue;
112
113 const integration = await db.query.integrationTable.findFirst({
114 where: and(
115 eq(integrationTable.projectId, task.projectId),
116 eq(integrationTable.type, "github"),
117 ),
118 });
119
120 if (!integration) continue;
121
122 const config = JSON.parse(integration.config);
123 if (config.repositoryOwner !== owner || config.repositoryName !== repo) {
124 continue;
125 }
126
127 const existingLink = await db.query.externalLinkTable.findFirst({
128 where: and(
129 eq(externalLinkTable.taskId, task.id),
130 eq(externalLinkTable.integrationId, integration.id),
131 eq(externalLinkTable.resourceType, "issue"),
132 ),
133 });
134
135 if (!existingLink) {
136 await db.insert(externalLinkTable).values({
137 taskId: task.id,
138 integrationId: integration.id,
139 resourceType: "issue",
140 externalId: issueNumber,
141 url: url,
142 title: null,
143 metadata: JSON.stringify({
144 migrated: true,
145 createdFrom: linkType === "Created from" ? "github" : "kaneo",
146 }),
147 });
148 linksCreated++;
149 }
150
151 const cleanedDescription = task.description
152 .replace(/\n\n---\n\n\*.*GitHub issue:.*\*/g, "")
153 .replace(/\n---\n<sub>Task:.*<\/sub>/g, "")
154 .trim();
155
156 if (cleanedDescription !== task.description) {
157 await db
158 .update(taskTable)
159 .set({ description: cleanedDescription || null })
160 .where(eq(taskTable.id, task.id));
161 descriptionsUpdated++;
162 }
163 }
164
165 console.log(`✓ Created ${linksCreated} external links`);
166 console.log(`✓ Cleaned ${descriptionsUpdated} task descriptions`);
167}
168
169async function dropOldTable() {
170 console.log("🗑️ Dropping old github_integration table...");
171 await db.execute(sql`DROP TABLE IF EXISTS github_integration CASCADE`);
172 console.log("✓ Dropped github_integration table");
173}