import { Router } from 'express'; import express from 'express'; import { spawn } from 'child_process'; import { createHmac, timingSafeEqual } from 'crypto'; import { EmailService } from '../email/EmailService'; const router = Router(); const DEPLOY_SECRET = process.env.DEPLOY_WEBHOOK_SECRET ?? ''; const APP_DIR = process.env.APP_DIR ?? '/opt/roject'; const SERVICE_NAME = process.env.SERVICE_NAME ?? 'roject'; function verifySignature( rawBody: Buffer, signature: string ): boolean { if ( !DEPLOY_SECRET ) return false; const expected = createHmac( 'sha256', DEPLOY_SECRET ).update( rawBody ).digest( 'hex' ); try { return timingSafeEqual( Buffer.from( signature ), Buffer.from( expected ) ); } catch { return false; } } function triggerDeploy() { const cmd = `git -C ${APP_DIR} pull && npm --prefix ${APP_DIR} run build && systemctl restart ${SERVICE_NAME}`; const child = spawn( 'bash', [ '-c', cmd ], { detached: true, stdio: 'ignore' } ); child.unref(); } router.post( '/', express.raw( { type: 'application/json' } ), ( req, res ) => { const signature = req.headers[ 'x-gitea-signature' ] as string; if ( !DEPLOY_SECRET ) { console.error( '[deploy] DEPLOY_WEBHOOK_SECRET is not set' ); res.status( 500 ).json( { error: 'Deploy not configured' } ); return; } if ( !signature || !verifySignature( req.body as Buffer, signature ) ) { res.status( 401 ).json( { error: 'Invalid signature' } ); return; } const payload = JSON.parse( ( req.body as Buffer ).toString() ); if ( payload.ref !== 'refs/heads/main' ) { res.json( { message: 'Not main branch, skipping' } ); return; } const pusher = payload.pusher?.login ?? 'unknown'; const time = new Date().toISOString(); console.log( `[deploy] triggered by push from ${pusher}` ); EmailService.sendEmail( EmailService.reportEmail, 'Roject — deploy requested', `Deploy triggered at ${time} by ${pusher}.` ).catch( () => {} ); res.json( { message: 'Deploy triggered' } ); triggerDeploy(); } ); export default router;