import { Router } from 'express' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import fs from 'node:fs/promises' import path from 'node:path' import { config } from '../../config.js' import { upsertWebsiteProjectByRepo } from '../../services/appwriteAdmin.js' const execFileAsync = promisify(execFile) const router = Router() function verifyWebhookToken(req) { const token = config.gitea.webhookToken if (!token) return false const queryToken = req.query.token const headerToken = req.get('X-Gitea-Token') || req.get('X-Gogs-Signature') return queryToken === token || headerToken === token } async function fetchPreviewConfig(repoFullName, branch) { const [owner, repo] = repoFullName.split('/') if (!owner || !repo || !config.gitea.apiToken) { return null } const url = `${config.gitea.baseUrl}/api/v1/repos/${owner}/${repo}/contents/.webklar-preview.json?ref=${encodeURIComponent(branch)}` const response = await fetch(url, { headers: { Authorization: `token ${config.gitea.apiToken}` }, }) if (!response.ok) return null const data = await response.json() const content = Buffer.from(data.content || '', 'base64').toString('utf8') return JSON.parse(content) } function defaultPreviewConfig(repoFullName) { const repoName = repoFullName.split('/').pop() || 'preview' return { enabled: true, type: 'static', branch: 'main', displayName: repoName, subdomain: repoName, } } async function cloneRepo(cloneUrl, targetDir, branch) { await fs.rm(targetDir, { recursive: true, force: true }) await fs.mkdir(targetDir, { recursive: true }) await execFileAsync('git', [ 'clone', '--depth', '1', '--branch', branch, cloneUrl, targetDir, ]) } async function deployStatic(sourceDir, targetDir, indexSubdir = '') { const webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir await fs.rm(targetDir, { recursive: true, force: true }) await fs.mkdir(targetDir, { recursive: true }) await execFileAsync('cp', ['-R', `${webroot}/.`, targetDir]) } async function deployNodeBuild(sourceDir, targetDir) { await execFileAsync('npm', ['ci'], { cwd: sourceDir }) await execFileAsync('npm', ['run', 'build'], { cwd: sourceDir }) const distDir = path.join(sourceDir, 'dist') await deployStatic(distDir, targetDir) } async function runDeploy(repoFullName, branch, previewConfig) { if (!config.preview.deployRoot) { return { deployed: false, reason: 'PREVIEW_DEPLOY_ROOT nicht gesetzt' } } const subdomain = previewConfig.subdomain || repoFullName.split('/').pop() const cloneUrl = `${config.gitea.baseUrl}/${repoFullName}.git` const workDir = path.join(process.cwd(), 'preview-data', repoFullName.replace(/\//g, '_')) const targetDir = path.join(config.preview.deployRoot, subdomain) await cloneRepo(cloneUrl, workDir, branch) if (previewConfig.type === 'node_build') { await deployNodeBuild(workDir, targetDir) } else { await deployStatic(workDir, targetDir, previewConfig.index || '') } return { deployed: true, subdomain, targetDir } } router.post('/gitea', async (req, res) => { if (!verifyWebhookToken(req)) { return res.status(401).json({ error: 'Unauthorized' }) } const payload = req.body || {} const ref = payload.ref || '' const repo = payload.repository || {} const repoFullName = repo.full_name || '' if (!repoFullName) { return res.status(400).json({ error: 'repository.full_name fehlt' }) } const branch = ref.replace('refs/heads/', '') || 'main' try { let previewConfig = await fetchPreviewConfig(repoFullName, branch) if (!previewConfig) { previewConfig = defaultPreviewConfig(repoFullName) } if (!previewConfig.enabled) { return res.json({ ok: true, skipped: true, reason: 'preview disabled' }) } const configBranch = previewConfig.branch || 'main' if (branch !== configBranch) { return res.json({ ok: true, skipped: true, reason: `branch ${branch} != ${configBranch}` }) } const subdomain = previewConfig.subdomain || repoFullName.split('/').pop() const previewUrl = `https://${subdomain}.${config.preview.baseHost}` let deployResult = { deployed: false } try { deployResult = await runDeploy(repoFullName, branch, previewConfig) } catch (deployErr) { console.error('[webhook] deploy failed:', deployErr.message) deployResult = { deployed: false, error: deployErr.message } } const project = await upsertWebsiteProjectByRepo(repoFullName, { projectName: previewConfig.displayName || subdomain, templateName: previewConfig.templateName || 'webklar-preview-template', giteaRepoUrl: repo.html_url || `${config.gitea.baseUrl}/${repoFullName}`, giteaRepoName: repo.name || subdomain, repoFullName, subdomain, previewUrl, status: deployResult.deployed ? 'deployed' : 'pending', provisioningStatus: deployResult.deployed ? 'ready' : 'deploy_failed', }) return res.json({ ok: true, repoFullName, subdomain, previewUrl, deploy: deployResult, projectId: project.$id, }) } catch (err) { console.error('[webhook]', err) return res.status(500).json({ error: err.message || 'Webhook-Verarbeitung fehlgeschlagen' }) } }) export default router