init
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
||||
const crypto = require('node:crypto')
|
||||
const http = require('node:http')
|
||||
const { spawn } = require('node:child_process')
|
||||
|
||||
const PORT = Number(process.env.WEBHOOK_PORT || 3011)
|
||||
const SECRET = process.env.GITHUB_WEBHOOK_SECRET || ''
|
||||
const RESTART_TARGETS = (process.env.PM2_RESTART_TARGETS || 'tssbot-web')
|
||||
.split(',')
|
||||
.map((target) => target.trim())
|
||||
.filter(Boolean)
|
||||
const RESTART_AFTER_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
let deploying = false
|
||||
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
function verifySignature(rawBody, signature) {
|
||||
if (!SECRET) return true
|
||||
if (!signature || !signature.startsWith('sha256=')) return false
|
||||
|
||||
const expected = `sha256=${crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex')}`
|
||||
const signatureBuffer = Buffer.from(signature)
|
||||
const expectedBuffer = Buffer.from(expected)
|
||||
|
||||
return (
|
||||
signatureBuffer.length === expectedBuffer.length &&
|
||||
crypto.timingSafeEqual(signatureBuffer, expectedBuffer)
|
||||
)
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: __dirname,
|
||||
shell: process.platform === 'win32',
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
child.on('error', reject)
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) resolve()
|
||||
else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function deploy() {
|
||||
await run('git', ['pull', '--ff-only'])
|
||||
await run('npm', ['install'])
|
||||
await run('npm', ['run', 'build'])
|
||||
|
||||
for (const target of RESTART_TARGETS) {
|
||||
await run('pm2', ['reload', target, '--update-env'])
|
||||
}
|
||||
}
|
||||
|
||||
http
|
||||
.createServer((req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
json(res, 200, { ok: true, deploying, restart_targets: RESTART_TARGETS })
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method !== 'POST' || req.url !== '/github') {
|
||||
json(res, 404, { error: 'Not found' })
|
||||
return
|
||||
}
|
||||
|
||||
const chunks = []
|
||||
req.on('data', (chunk) => chunks.push(chunk))
|
||||
req.on('end', () => {
|
||||
const rawBody = Buffer.concat(chunks)
|
||||
|
||||
if (!verifySignature(rawBody, req.headers['x-hub-signature-256'])) {
|
||||
json(res, 401, { error: 'Invalid signature' })
|
||||
return
|
||||
}
|
||||
|
||||
if (req.headers['x-github-event'] !== 'push') {
|
||||
json(res, 202, { skipped: true, reason: 'Only push events trigger deploys' })
|
||||
return
|
||||
}
|
||||
|
||||
if (deploying) {
|
||||
json(res, 202, { queued: false, deploying: true })
|
||||
return
|
||||
}
|
||||
|
||||
deploying = true
|
||||
json(res, 202, { accepted: true, restart_targets: RESTART_TARGETS })
|
||||
|
||||
deploy()
|
||||
.then(() => console.log('GitHub push deploy completed'))
|
||||
.catch((error) => console.error('GitHub push deploy failed:', error))
|
||||
.finally(() => {
|
||||
deploying = false
|
||||
})
|
||||
})
|
||||
})
|
||||
.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`tssbot webhook listening on http://localhost:${PORT}/github`)
|
||||
console.log(`restart targets: ${RESTART_TARGETS.join(', ')}`)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('24 hour webhook refresh reached; exiting for PM2 restart')
|
||||
process.exit(0)
|
||||
}, RESTART_AFTER_MS).unref()
|
||||
Reference in New Issue
Block a user