nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
3.88 kB
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 }
)
}
}