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:
@@ -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 });
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { headers } from "next/headers";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
import { createIssue, buildIssueBody } from "@/lib/github";
|
||||
import { createIssue, buildIssueBody } from "@/lib/gitea";
|
||||
import { pipelineQueue } from "@/lib/queue";
|
||||
|
||||
const ALLOWED_STATUS = new Set([
|
||||
|
||||
@@ -32,7 +32,7 @@ export function GithubActions({
|
||||
if (issueUrl) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border bg-muted/30 p-3 text-sm">
|
||||
<span className="text-muted-foreground">GitHub:</span>
|
||||
<span className="text-muted-foreground">Gitea:</span>
|
||||
<a href={issueUrl} target="_blank" rel="noreferrer" className="font-mono underline">
|
||||
#{issueNumber} ({issueState})
|
||||
</a>
|
||||
@@ -43,7 +43,7 @@ export function GithubActions({
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="default" disabled={pending} onClick={fire}>
|
||||
Create GitHub Issue
|
||||
Create Gitea Issue
|
||||
</Button>
|
||||
{flash && <span className="text-xs text-muted-foreground">{flash}</span>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user