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,5 +1,5 @@
import { prisma } from "../db";
import { getIssue } from "../lib/github";
import { getIssue } from "../lib/gitea";
const POLL_LIMIT = Number(process.env.GITHUB_SYNC_LIMIT ?? "50");
@@ -13,9 +13,13 @@ export type GithubSyncResult = {
// Used because the panel is Tailscale-only and can't receive webhooks.
export async function runGithubSync(): Promise<GithubSyncResult> {
// Only poll insights that have an issue and could still change state.
const giteaBase = (process.env.GITEA_BASE_URL ?? "https://git.semih.ai").replace(/\/$/, "");
const tracked = await prisma.insight.findMany({
where: {
githubIssueNumber: { not: null },
// Only sync issues hosted on the configured Gitea instance.
// Old GitHub-hosted insights stay frozen until manually re-linked or dismissed.
githubIssueUrl: { startsWith: giteaBase },
status: { notIn: ["validated", "dismissed", "duplicate"] },
},
orderBy: { updatedAt: "asc" },

View File

@@ -1,10 +1,11 @@
// Minimal GitHub REST client for the worker (read-only).
// Outbound only — used because Tailscale-only panel can't receive GitHub webhooks.
// Minimal Gitea REST client for the worker (read-only).
// Outbound only — used because Tailscale-only panel can't receive webhooks.
const TOKEN = process.env.GITHUB_TOKEN ?? "";
const TOKEN = process.env.GITEA_TOKEN ?? "";
const BASE = (process.env.GITEA_BASE_URL ?? "https://git.semih.ai").replace(/\/$/, "");
function repoFor(projectKey: string): { owner: string; name: string } | null {
const env = process.env[`GITHUB_REPO_${projectKey.toUpperCase()}`];
const env = process.env[`GITEA_REPO_${projectKey.toUpperCase()}`];
if (!env) return null;
const [owner, name] = env.split("/");
if (!owner || !name) return null;
@@ -20,11 +21,10 @@ export async function getIssue(
if (!TOKEN) return null;
const repo = repoFor(projectKey);
if (!repo) return null;
const res = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${number}`, {
const res = await fetch(`${BASE}/api/v1/repos/${repo.owner}/${repo.name}/issues/${number}`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
Authorization: `token ${TOKEN}`,
Accept: "application/json",
},
});
if (!res.ok) return null;