77aebbef89ead5ef0a366ff1ec26564bbe6156e5 diff --git a/scripts/p620/forgejo.container b/scripts/p620/forgejo.container index 79004bb22c2a1915d5a79c77959e214233a6a766..f326bd88e14f9623e0d94894bb28af1e53547c69 100644 --- a/scripts/p620/forgejo.container +++ b/scripts/p620/forgejo.container @@ -22,10 +22,13 @@ Environment=USER_UID=1000 Environment=USER_GID=1000 # App URL en domain — Forgejo gebruikt deze voor link-generatie (clone URLs, -# emails, web links). Cloudflare termineert TLS, Forgejo zelf praat HTTP. -Environment=FORGEJO__server__ROOT_URL=https://git.tdd.md/ -Environment=FORGEJO__server__DOMAIN=git.tdd.md -Environment=FORGEJO__server__SSH_DOMAIN=git.tdd.md +# emails, web links). Onze Bun-server op tdd.md proxiet git-protocol paths +# door naar Forgejo, dus de canonical URLs zijn tdd.md (geen git.tdd.md). +# git.tdd.md blijft technisch werken via cloudflare route maar wordt niet +# meer gelinkt vanuit Forgejo's UI/API. +Environment=FORGEJO__server__ROOT_URL=https://tdd.md/ +Environment=FORGEJO__server__DOMAIN=tdd.md +Environment=FORGEJO__server__SSH_DOMAIN=tdd.md Environment=FORGEJO__server__HTTP_PORT=3000 # SQLite — geen aparte db container nodig. diff --git a/src/forgejo.ts b/src/forgejo.ts index d915ae84dd125e13df389ac2cf9283d4ba1ab7f5..c71cdaa7176bb571101bbce3680d5dcb575e9b78 100644 --- a/src/forgejo.ts +++ b/src/forgejo.ts @@ -191,10 +191,11 @@ export const registerAgent = async (params: { }); } + const baseUrl = process.env.BASE_URL ?? "https://tdd.md"; return { username: params.username, pushToken, - repoCloneUrl: `https://git.tdd.md/${params.username}/${kata}.git`, + repoCloneUrl: `${baseUrl}/${params.username}/${kata}.git`, isNew, }; }; diff --git a/src/server.ts b/src/server.ts index b0ccf2b1e2b0f30a4246d109523a57ae87575236..363d809b101d3be5d4d00ffb873b3ac10edcc27b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -134,6 +134,141 @@ const timingSafeEqual = (a: string, b: string): boolean => { return r === 0; }; +// Forward git protocol + Forgejo API/asset requests to Forgejo via the host +// network. Lets us serve everything under tdd.md (GitHub-style) without +// exposing git.tdd.md externally. +const FORGEJO_INTERNAL = process.env.FORGEJO_URL ?? "https://git.tdd.md"; + +const HOP_BY_HOP = [ + "host", + "connection", + "keep-alive", + "transfer-encoding", + "upgrade", + "proxy-authorization", + "proxy-connection", + "te", + "trailer", +]; + +const proxyToForgejo = async (req: Request, pathAndQuery: string): Promise => { + const upstream = `${FORGEJO_INTERNAL}${pathAndQuery}`; + const headers = new Headers(req.headers); + for (const h of HOP_BY_HOP) headers.delete(h); + headers.set("X-Forwarded-Host", "tdd.md"); + headers.set("X-Forwarded-Proto", "https"); + headers.set("X-Forwarded-For", req.headers.get("cf-connecting-ip") ?? "0.0.0.0"); + + let body: ArrayBuffer | undefined; + if (req.method !== "GET" && req.method !== "HEAD") { + body = await req.arrayBuffer(); + } + + const upstreamRes = await fetch(upstream, { + method: req.method, + headers, + body, + redirect: "manual", + }); + + const responseHeaders = new Headers(upstreamRes.headers); + for (const h of HOP_BY_HOP) responseHeaders.delete(h); + + return new Response(upstreamRes.body, { + status: upstreamRes.status, + statusText: upstreamRes.statusText, + headers: responseHeaders, + }); +}; + +const isGitProtocol = (pathname: string, search: URLSearchParams): boolean => { + if (pathname.includes(".git/") || pathname.endsWith(".git")) return true; + if ( + pathname.endsWith("/info/refs") && + (search.get("service") === "git-upload-pack" || search.get("service") === "git-receive-pack") + ) { + return true; + } + if (pathname.endsWith("/git-upload-pack") || pathname.endsWith("/git-receive-pack")) { + return true; + } + return false; +}; + +interface ForgejoRepoSummary { + description: string; + clone_url: string; + empty: boolean; +} + +interface ForgejoCommit { + sha: string; + commit: { message: string; author: { name: string; date: string } }; +} + +const renderRepoView = async (owner: string, repo: string): Promise => { + const repoApi = `${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + const repoRes = await fetch(repoApi); + if (repoRes.status === 404) { + const html = await renderNotFound(`/${owner}/${repo}`); + return htmlResponse(html, 404); + } + if (!repoRes.ok) { + const html = await renderPage({ + title: `${owner}/${repo} — tdd.md`, + bodyMarkdown: `# ${owner}/${repo}\n\n> repository unavailable`, + }); + return htmlResponse(html, 502); + } + const info = (await repoRes.json()) as ForgejoRepoSummary; + + let commitList = "> No commits yet."; + if (!info.empty) { + const commitsRes = await fetch(`${repoApi}/commits?limit=8&stat=false`); + if (commitsRes.ok) { + const commits = (await commitsRes.json()) as ForgejoCommit[]; + if (commits.length > 0) { + commitList = commits + .map((c) => { + const sha = c.sha.slice(0, 7); + const subject = c.commit.message.split("\n")[0]!; + const escaped = subject.replace(/\|/g, "\\|"); + return `| \`${sha}\` | ${escaped} | ${c.commit.author.name} |`; + }) + .join("\n"); + commitList = `| sha | message | author |\n|---|---|---|\n${commitList}`; + } + } + } + + const cloneUrl = info.clone_url || `https://tdd.md/${owner}/${repo}.git`; + + const body = `# ${owner}/${repo} + +${info.description ? `> ${info.description}\n` : ""} + +## clone + +\`\`\` +git clone ${cloneUrl} +\`\`\` + +## recent commits + +${commitList} + +[← agents/${owner}](/agents/${owner}) +`; + + const html = await renderPage({ + title: `${owner}/${repo} — tdd.md`, + bodyMarkdown: body, + ogPath: `https://tdd.md/${owner}/${repo}`, + active: "agents", + }); + return htmlResponse(html); +}; + const port = Number(process.env.PORT ?? 3000); const server = Bun.serve({ @@ -279,6 +414,20 @@ When you push, the judge replays your commits and posts the verdict at [/agents/ async fetch(req) { const url = new URL(req.url); + + // Git smart-HTTP and dumb-HTTP — proxy raw to Forgejo. + if (isGitProtocol(url.pathname, url.searchParams)) { + return proxyToForgejo(req, url.pathname + url.search); + } + + // Bare repo URL: // — render Bun-native view via Forgejo API. + // Two segments only, no trailing path. Reserved top-level paths are + // already matched by explicit routes above, so they never reach here. + const repoMatch = url.pathname.match(/^\/([A-Za-z0-9][A-Za-z0-9-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*)\/?$/); + if (repoMatch) { + return renderRepoView(repoMatch[1]!, repoMatch[2]!); + } + const html = await renderNotFound(url.pathname); return htmlResponse(html, 404); },