kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import {
2 createExternalLink,
3 findExternalLink,
4} from "../../github/services/link-manager";
5import {
6 findTaskByNumber,
7 isTaskInFinalState,
8 updateTaskStatus,
9} from "../../github/services/task-service";
10import type { GiteaConfig } from "../config";
11import {
12 findAllIntegrationsByGiteaRepo,
13 repoOwnerLogin,
14} from "../services/integration-lookup";
15import { extractTaskNumberGitea } from "../utils/branch-matcher";
16import { resolveTargetStatus } from "../utils/resolve-column";
17import { baseUrlFromRepositoryHtmlUrl } from "../utils/webhook-repo";
18
19type PROpenedPayload = {
20 action: string;
21 pull_request: {
22 number: number;
23 title: string;
24 body: string | null;
25 html_url: string;
26 state: string;
27 draft?: boolean;
28 merged?: boolean;
29 head: {
30 ref: string;
31 };
32 user: { login?: string; username?: string } | null;
33 };
34 repository: {
35 owner: { login?: string; username?: string };
36 name: string;
37 html_url: string;
38 };
39};
40
41export async function handleGiteaPullRequestOpened(payload: PROpenedPayload) {
42 const { pull_request, repository } = payload;
43
44 const baseUrl = baseUrlFromRepositoryHtmlUrl(repository.html_url);
45 if (!baseUrl) return;
46
47 const owner = repoOwnerLogin(repository);
48 const integrations = await findAllIntegrationsByGiteaRepo(
49 baseUrl,
50 owner,
51 repository.name,
52 );
53
54 for (const integration of integrations) {
55 if (!integration.project) {
56 continue;
57 }
58
59 let config: GiteaConfig;
60 try {
61 config = JSON.parse(integration.config) as GiteaConfig;
62 } catch (error) {
63 console.error("Invalid Gitea config for integration", {
64 integrationId: integration.id,
65 error,
66 });
67 continue;
68 }
69 const projectSlug = integration.project.slug;
70 const branchName = pull_request.head.ref;
71
72 const taskNumber = extractTaskNumberGitea(
73 branchName,
74 pull_request.title,
75 pull_request.body ?? undefined,
76 config,
77 projectSlug,
78 );
79
80 if (!taskNumber) {
81 continue;
82 }
83
84 const task = await findTaskByNumber(integration.projectId, taskNumber);
85
86 if (!task) {
87 continue;
88 }
89
90 const existingLink = await findExternalLink(
91 integration.id,
92 "pull_request",
93 pull_request.number.toString(),
94 );
95
96 if (existingLink) {
97 continue;
98 }
99
100 await createExternalLink({
101 taskId: task.id,
102 integrationId: integration.id,
103 resourceType: "pull_request",
104 externalId: pull_request.number.toString(),
105 url: pull_request.html_url,
106 title: pull_request.title,
107 metadata: {
108 state: pull_request.state,
109 draft: pull_request.draft,
110 merged: pull_request.merged,
111 branch: branchName,
112 author: pull_request.user?.login ?? pull_request.user?.username,
113 },
114 });
115
116 const targetStatus = await resolveTargetStatus(
117 integration.projectId,
118 "pr_opened",
119 config.statusTransitions?.onPROpen || "in-review",
120 );
121
122 const isTaskFinal = await isTaskInFinalState(task);
123
124 if (task.status !== targetStatus && !isTaskFinal) {
125 await updateTaskStatus(task.id, targetStatus);
126 }
127
128 return;
129 }
130}