Spaces:
Runtime error
Runtime error
| import { NextResponse } from 'next/server' | |
| import { execFileSync } from 'child_process' | |
| import { readFileSync } from 'fs' | |
| import { join } from 'path' | |
| import { requireRole } from '@/lib/auth' | |
| import { getDatabase } from '@/lib/db' | |
| import { APP_VERSION } from '@/lib/version' | |
| const UPDATE_TIMEOUT = 5 * 60 * 1000 // 5 minutes | |
| const MAX_BUFFER = 10 * 1024 * 1024 // 10 MB | |
| const EXEC_OPTS = { | |
| timeout: UPDATE_TIMEOUT, | |
| maxBuffer: MAX_BUFFER, | |
| encoding: 'utf-8' as const, | |
| } | |
| function git(args: string[], cwd: string): string { | |
| return execFileSync('git', args, { ...EXEC_OPTS, cwd }).trim() | |
| } | |
| function pnpm(args: string[], cwd: string): string { | |
| return execFileSync('pnpm', args, { ...EXEC_OPTS, cwd }).trim() | |
| } | |
| export async function POST(request: Request) { | |
| const auth = requireRole(request, 'admin') | |
| if (auth.error) { | |
| return NextResponse.json({ error: auth.error }, { status: auth.status }) | |
| } | |
| const user = auth.user! | |
| const cwd = process.cwd() | |
| const steps: { step: string; output: string }[] = [] | |
| try { | |
| // Parse target version from request body | |
| const body = await request.json().catch(() => ({})) | |
| const targetVersion: string | undefined = body.targetVersion | |
| if (!targetVersion) { | |
| return NextResponse.json( | |
| { error: 'Missing targetVersion in request body' }, | |
| { status: 400 } | |
| ) | |
| } | |
| // Normalize to tag format (e.g. "1.2.0" -> "v1.2.0") | |
| const tag = targetVersion.startsWith('v') ? targetVersion : `v${targetVersion}` | |
| // 1. Check for uncommitted changes | |
| const status = git(['status', '--porcelain'], cwd) | |
| if (status) { | |
| return NextResponse.json( | |
| { | |
| error: 'Working tree has uncommitted changes. Please commit or stash them before updating.', | |
| dirty: true, | |
| files: status.split('\n').slice(0, 20), | |
| }, | |
| { status: 409 } | |
| ) | |
| } | |
| // 2. Fetch tags and release code from origin | |
| const fetchOut = git(['fetch', 'origin', '--tags', '--force'], cwd) | |
| steps.push({ step: 'git fetch', output: fetchOut || 'OK' }) | |
| // 3. Verify the tag exists | |
| try { | |
| git(['rev-parse', '--verify', `refs/tags/${tag}`], cwd) | |
| } catch { | |
| return NextResponse.json( | |
| { error: `Release tag ${tag} not found in remote` }, | |
| { status: 404 } | |
| ) | |
| } | |
| // 4. Checkout the release tag | |
| const checkoutOut = git(['checkout', tag], cwd) | |
| steps.push({ step: `git checkout ${tag}`, output: checkoutOut }) | |
| // 5. Install dependencies | |
| const installOut = pnpm(['install', '--frozen-lockfile'], cwd) | |
| steps.push({ step: 'pnpm install', output: installOut }) | |
| // 6. Build | |
| const buildOut = pnpm(['build'], cwd) | |
| steps.push({ step: 'pnpm build', output: buildOut }) | |
| // 7. Read new version from package.json | |
| const newPkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8')) | |
| const newVersion: string = newPkg.version ?? targetVersion | |
| // 8. Log to audit_log | |
| try { | |
| const db = getDatabase() | |
| db.prepare( | |
| 'INSERT INTO audit_log (action, actor, detail) VALUES (?, ?, ?)' | |
| ).run( | |
| 'system.update', | |
| user.username, | |
| JSON.stringify({ | |
| previousVersion: APP_VERSION, | |
| newVersion, | |
| tag, | |
| }) | |
| ) | |
| } catch { | |
| // Non-critical -- don't fail the update if audit logging fails | |
| } | |
| return NextResponse.json({ | |
| success: true, | |
| previousVersion: APP_VERSION, | |
| newVersion, | |
| tag, | |
| steps, | |
| restartRequired: true, | |
| }) | |
| } catch (err: any) { | |
| const message = | |
| err?.stderr?.toString?.()?.trim() || | |
| err?.stdout?.toString?.()?.trim() || | |
| err?.message || | |
| 'Unknown error during update' | |
| return NextResponse.json( | |
| { | |
| error: 'Update failed', | |
| detail: message, | |
| steps, | |
| }, | |
| { status: 500 } | |
| ) | |
| } | |
| } | |