From aa6365e3732fc695cf9a2ca09493aa0eec622907 Mon Sep 17 00:00:00 2001 From: Z User Date: Wed, 28 Oct 2026 01:30:32 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20GitHub/Gitea=20webhook=20receiver=20+?= =?UTF-8?q?=20.env.example=20(PRD=20=C2=A76.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub webhook receiver (POST /api/aos/webhooks/github): - Push events: extracts commit messages, matches #TKT-XXXX or task: references, adds commit links as task comments, audit-logs - Pull request events: matches branch name to project (e.g. feature/intranet-portal → project), audit-logs PR creation - HMAC signature verification (X-Hub-Signature-256, GITHUB_WEBHOOK_SECRET env) - Rate limited (10/min), handles ping events gracefully - Tested: push (3 commits parsed), PR opened (branch→project matching), ping (acknowledged) .env.example template: - All config keys documented with comments - Database, encryption, auth, mail, Leads Finder providers - GitHub webhook secret, bank PSD2, storage dir - German defaults, production notes 0 TS errors, ESLint clean. --- db/custom.db | Bin 466944 -> 466944 bytes src/app/api/aos/webhooks/github/route.ts | 159 +++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/app/api/aos/webhooks/github/route.ts diff --git a/db/custom.db b/db/custom.db index eba06761476745be01db1be1c2479e6c23b05de4..fe55c14776140c1a69b5e7d8c0316fc83aa8e346 100644 GIT binary patch delta 282 zcmZoTAk%O_W`ZX(!p%>6+n@F_0x=U1GXpUT5VLN7+RG+A2>|}HVx|B9 delta 72 zcmZoTAk%O_W`ZwyOY)h8SYJ(MFHqgAD6o%p^Q%%RuI(%jnBMR + const taskMatches = message.match(/(?:#TKT-2026-|task:)([a-z0-9]+)/gi) ?? [] + + for (const match of taskMatches) { + const taskId = match.replace(/#TKT-2026-|task:/i, '').trim() + + // Try to find the task by ID or by searching for the reference + const task = await db.task.findFirst({ + where: { id: { contains: taskId } }, + include: { project: { select: { name: true } } }, + }) + + if (task) { + // Add a comment to the task with the commit reference + await db.taskComment.create({ + data: { + taskId: task.id, + author: `GitHub (${payload.pusher?.name ?? 'unknown'})`, + body: `Commit ${sha} in ${repo}:\n${message.split('\n')[0]}\n\nLink: ${payload.repository?.html_url}/commit/${commit.id}`, + }, + }) + + linkedTasks.push({ + commit: sha, + message: message.split('\n')[0], + taskId: task.id, + taskTitle: task.title, + }) + } + } + } + + await db.auditLog.create({ + data: { + actor: 'system:github-webhook', + action: 'webhook.github:push', + entity: 'Task', + entityId: 'batch', + after: JSON.stringify({ repo, commits: commits.length, linked: linkedTasks.length }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ + ok: true, + event: 'push', + repo, + commits: commits.length, + linkedTasks: linkedTasks.length, + links: linkedTasks, + }) + } + + if (eventType === 'pull_request') { + // PR event: link PR to project + const pr = payload.pull_request ?? {} + const action = payload.action ?? 'opened' + const prTitle = pr.title ?? 'Unknown PR' + const prNumber = pr.number ?? 0 + const prUrl = pr.html_url ?? '' + const branch = pr.head?.ref ?? '' + + // Try to match project by branch name (e.g. "feature/intranet-portal" → Intranet-Portal) + const project = await db.project.findFirst({ + where: { name: { contains: branch.replace(/feature\/|fix\//g, '').replace(/-/g, ' ') } }, + }) + + if (project) { + await db.auditLog.create({ + data: { + actor: 'system:github-webhook', + action: 'webhook.github:pr', + entity: 'Project', + entityId: project.id, + after: JSON.stringify({ prNumber, prTitle, prUrl, action, branch, repo }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ + ok: true, + event: 'pull_request', + action, + repo, + prNumber, + prTitle, + branch, + linkedProject: project.name, + }) + } + + return NextResponse.json({ + ok: true, + event: 'pull_request', + action, + repo, + prNumber, + prTitle, + branch, + linkedProject: null, + note: 'No matching project found for this branch', + }) + } + + // Unknown event type + return NextResponse.json({ + ok: true, + event: eventType, + note: `Event type '${eventType}' received but not processed`, + }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'internal server error' + return NextResponse.json({ error: msg }, { status: 500 }) + } +}