kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { createOrUpdateExternalLink } from "../../github/services/link-manager";
2import {
3 findTaskByNumber,
4 isTaskInFinalState,
5 updateTaskStatus,
6} from "../../github/services/task-service";
7import type { GiteaConfig } from "../config";
8import {
9 findAllIntegrationsByGiteaRepo,
10 repoOwnerLogin,
11} from "../services/integration-lookup";
12import { extractTaskNumberFromBranchGitea } from "../utils/branch-matcher";
13import { resolveTargetStatus } from "../utils/resolve-column";
14import { baseUrlFromRepositoryHtmlUrl } from "../utils/webhook-repo";
15
16type PushPayload = {
17 ref: string;
18 head_commit?: {
19 id: string;
20 message: string;
21 author?: { name: string };
22 timestamp: string;
23 };
24 commits?: Array<{
25 id: string;
26 message: string;
27 author?: { name: string; username?: string };
28 timestamp?: string;
29 }>;
30 repository: {
31 owner: { login?: string; username?: string };
32 name: string;
33 html_url: string;
34 };
35};
36
37const PROTECTED_BRANCHES = [
38 "main",
39 "master",
40 "develop",
41 "staging",
42 "production",
43];
44
45export async function handleGiteaPush(payload: PushPayload) {
46 const { ref, repository } = payload;
47
48 if (!ref.startsWith("refs/heads/")) {
49 console.log(`[Gitea Push] Skipping non-branch ref: ${ref}`);
50 return;
51 }
52
53 const branchName = ref.slice("refs/heads/".length);
54 console.log(`[Gitea Push] Processing branch: ${branchName}`);
55
56 if (PROTECTED_BRANCHES.includes(branchName)) {
57 console.log(`[Gitea Push] Skipping protected branch: ${branchName}`);
58 return;
59 }
60
61 const origin = baseUrlFromRepositoryHtmlUrl(repository.html_url);
62 if (!origin) {
63 return;
64 }
65 const owner = repoOwnerLogin(repository);
66 const integrations = await findAllIntegrationsByGiteaRepo(
67 origin,
68 owner,
69 repository.name,
70 );
71
72 if (integrations.length === 0) {
73 return;
74 }
75
76 const headCommit =
77 payload.head_commit ?? payload.commits?.[payload.commits.length - 1];
78
79 for (const integration of integrations) {
80 if (!integration.project) {
81 continue;
82 }
83
84 let config: GiteaConfig;
85 try {
86 config = JSON.parse(integration.config) as GiteaConfig;
87 } catch (error) {
88 console.error("Invalid Gitea integration config for push webhook", {
89 integrationId: integration.id,
90 error,
91 });
92 continue;
93 }
94 const projectSlug = integration.project.slug;
95
96 const taskNumber = extractTaskNumberFromBranchGitea(
97 branchName,
98 config,
99 projectSlug,
100 );
101
102 if (!taskNumber) {
103 continue;
104 }
105
106 const task = await findTaskByNumber(integration.projectId, taskNumber);
107
108 if (!task) {
109 continue;
110 }
111
112 const treeUrl = `${repository.html_url}/src/branch/${branchName}`;
113
114 await createOrUpdateExternalLink({
115 taskId: task.id,
116 integrationId: integration.id,
117 resourceType: "branch",
118 externalId: branchName,
119 url: treeUrl,
120 title: branchName,
121 metadata: {
122 lastCommit: headCommit
123 ? {
124 sha: headCommit.id,
125 message: headCommit.message,
126 author: headCommit.author?.name,
127 timestamp:
128 "timestamp" in headCommit ? headCommit.timestamp : undefined,
129 }
130 : null,
131 },
132 });
133
134 const targetStatus = await resolveTargetStatus(
135 integration.projectId,
136 "branch_push",
137 config.statusTransitions?.onBranchPush || "in-progress",
138 );
139
140 const isTaskFinal = await isTaskInFinalState(task);
141
142 if (task.status !== targetStatus && !isTaskFinal) {
143 await updateTaskStatus(task.id, targetStatus);
144 }
145 }
146}