refactor(integrations): migrate issue tracker GitHub → Gitea

- apps/web/src/lib/gitea.ts: createIssue / getIssue / verifyWebhookSignature / buildIssueBody
  * Endpoint: git.semih.ai/api/v1 (configurable via GITEA_BASE_URL)
  * Auth: 'Authorization: token <PAT>' (Gitea convention)
  * Labels: Gitea expects numeric IDs not strings → ensureLabels() resolves/creates
    with color coding (P0/P1 red, P2 yellow, P3 green, type-* grey, default blue)
  * Webhook signature: X-Gitea-Signature (hex, no sha256= prefix)
- apps/worker/src/lib/gitea.ts: read-only getIssue() for sync polling
- _actions.ts + github-sync.ts now import from /lib/gitea
- Removed old apps/{web,worker}/.../lib/github.ts + /api/webhooks/github route
  (the receiver was already dead — sp.semih.ai is Tailscale-only)
- UI: 'GitHub' label → 'Gitea' on insight detail card
- github-sync job filters by githubIssueUrl.startsWith(GITEA_BASE_URL) so legacy
  GitHub-hosted insights (semihyesilyurt/sase.tr#20) stay frozen rather than
  collide with same-numbered Gitea issues at root/sase.tr.

Env migration (Coolify, panel-web + panel-worker):
- removed: GITHUB_TOKEN, GITHUB_REPO_SASE, GITHUB_WEBHOOK_SECRET
- added:   GITEA_TOKEN, GITEA_REPO_SASE=root/sase.tr, GITEA_BASE_URL=https://git.semih.ai

Provisioned Gitea PAT 'super-panel-insights' (scopes: write:repository + write:issue),
stored in Bitwarden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-05-14 09:36:57 +00:00
parent d0c497d12d
commit dfc19c8f13
7 changed files with 101 additions and 149 deletions

View File

@@ -1,95 +0,0 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { verifyWebhookSignature } from "@/lib/github";
import { revalidatePath } from "next/cache";
export const dynamic = "force-dynamic";
export async function POST(req: Request) {
const payload = await req.text();
const signature = req.headers.get("x-hub-signature-256");
if (!verifyWebhookSignature(payload, signature)) {
return NextResponse.json({ ok: false, error: "bad_signature" }, { status: 401 });
}
let event: any;
try {
event = JSON.parse(payload);
} catch {
return NextResponse.json({ ok: false, error: "bad_json" }, { status: 400 });
}
const ghEvent = req.headers.get("x-github-event");
if (ghEvent !== "issues" && ghEvent !== "pull_request" && ghEvent !== "ping") {
return NextResponse.json({ ok: true, ignored: ghEvent });
}
if (ghEvent === "ping") return NextResponse.json({ ok: true, pong: true });
if (ghEvent === "issues") {
const action = event.action;
const issue = event.issue;
if (!issue?.id) return NextResponse.json({ ok: true, no_issue: true });
const insight = await prisma.insight.findFirst({
where: { githubIssueId: BigInt(issue.id) },
});
if (!insight) {
return NextResponse.json({ ok: true, not_tracked: true, issue_id: issue.id });
}
let nextStatus: string | null = null;
let extra: Record<string, unknown> = {};
switch (action) {
case "closed":
nextStatus = "shipped";
extra = { shippedAt: new Date(), validationStartedAt: new Date() };
break;
case "reopened":
nextStatus = "in_progress";
extra = { shippedAt: null, validationStartedAt: null, regressionDetected: false };
break;
case "opened":
nextStatus = "in_backlog";
break;
case "edited":
case "labeled":
case "unlabeled":
// ignore body/label changes
break;
}
if (nextStatus) {
await prisma.insight.update({
where: { id: insight.id },
data: { status: nextStatus, githubIssueState: issue.state, ...extra },
});
} else {
await prisma.insight.update({
where: { id: insight.id },
data: { githubIssueState: issue.state },
});
}
revalidatePath("/insights");
revalidatePath(`/insights/i/${insight.id}`);
return NextResponse.json({ ok: true, insight_id: insight.id, status: nextStatus });
}
if (ghEvent === "pull_request") {
// Map linked PR opening to in_progress. GitHub auto-links issues via #N in PR body.
const pr = event.pull_request;
const action = event.action;
if (!pr?.body) return NextResponse.json({ ok: true, no_pr: true });
// Find linked issues by #N pattern
const refs = Array.from(String(pr.body).matchAll(/#(\d+)/g)).map((m) => Number(m[1]));
if (!refs.length) return NextResponse.json({ ok: true, no_refs: true });
// Look up by issue number (we stored id not number — need to match via API or store number)
// For now match against githubIssueId — but PR refs use issue number. Skip safely.
return NextResponse.json({ ok: true, pr_refs: refs, action });
}
return NextResponse.json({ ok: true });
}