Initial commit: management

This commit is contained in:
jtricerolph 2026-07-01 12:09:54 +00:00
commit ab329d497b
8 changed files with 243 additions and 0 deletions

15
updater/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM node:20-alpine
RUN apk add --no-cache openssh-client
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY src/ ./src/
COPY deploy-map.js ./
EXPOSE 9000
CMD ["node", "src/index.js"]

14
updater/deploy-map.js Normal file
View file

@ -0,0 +1,14 @@
// Maps Forgejo repo names → target LXC SSH details.
// Keys are the plain repo names Forgejo sends in the webhook (repository.name).
// The updater only pulls + rebuilds existing LXCs — provisioning is done on the
// Proxmox host with proxmox-helpers/add-app.sh, which appends new apps here.
export const deployMap = {
'noticeboard': { host: '10.10.10.112', path: '/opt/noticeboard' },
'kitchen': { host: '10.10.10.110', path: '/opt/kitchen' },
'cashup': { host: '10.10.10.111', path: '/opt/cashup' },
'housekeeping': { host: '10.10.10.114', path: '/opt/housekeeping' },
'forecasting': { host: '10.10.10.113', path: '/opt/forecasting' },
'rates': { host: '10.10.10.115', path: '/opt/rates' },
'portal': { host: '10.10.10.102', path: '/opt/portal' },
'auth': { host: '10.10.10.101', path: '/opt/auth' },
}

9
updater/package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "hnf-updater",
"version": "1.0.0",
"type": "module",
"scripts": { "start": "node src/index.js" },
"dependencies": {
"fastify": "^4.28.1"
}
}

72
updater/src/index.js Normal file
View file

@ -0,0 +1,72 @@
import Fastify from 'fastify'
import crypto from 'crypto'
import { execSync } from 'child_process'
import { deployMap } from '../deploy-map.js'
const app = Fastify({ logger: true })
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519'
const deployLog = []
function verifySignature(body, signature) {
if (!WEBHOOK_SECRET) return true // disabled in dev
const expected = `sha256=${crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')}`
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || ''))
}
async function deploy(target, repoName) {
const { host, path } = target
const cmd = `ssh -i ${SSH_KEY} -o StrictHostKeyChecking=no root@${host} "cd ${path} && git pull && docker compose up -d --build"`
const entry = { repo: repoName, host, started: new Date().toISOString(), status: 'running' }
deployLog.unshift(entry)
if (deployLog.length > 50) deployLog.pop()
try {
const out = execSync(cmd, { timeout: 300_000, encoding: 'utf8' })
entry.status = 'success'
entry.output = out.slice(-500)
app.log.info(`Deploy ${repoName} → success`)
} catch (err) {
entry.status = 'failed'
entry.error = err.message.slice(-500)
app.log.error(`Deploy ${repoName} → failed: ${err.message}`)
}
entry.finished = new Date().toISOString()
}
app.post('/webhook', {
config: { rawBody: true }
}, async (request, reply) => {
const sig = request.headers['x-hub-signature-256']
const rawBody = request.rawBody || JSON.stringify(request.body)
if (!verifySignature(rawBody, sig)) {
return reply.status(401).send({ error: 'Invalid signature' })
}
const event = request.headers['x-gitea-event'] || request.headers['x-github-event']
if (event !== 'push') return reply.status(200).send({ ok: true, skipped: 'not a push event' })
const repoName = request.body?.repository?.name
const ref = request.body?.ref || ''
if (!ref.endsWith('/main') && !ref.endsWith('/master')) {
return reply.status(200).send({ ok: true, skipped: 'not main branch' })
}
const target = deployMap[repoName]
if (!target) {
app.log.warn(`No deploy target for repo: ${repoName}`)
return reply.status(200).send({ ok: true, skipped: 'no deploy target' })
}
// Kick off deploy async — don't block the webhook response
deploy(target, repoName).catch(() => {})
return reply.status(202).send({ ok: true, deploying: repoName })
})
app.get('/health', async () => ({ status: 'healthy' }))
app.get('/deploys', async () => deployLog)
await app.listen({ port: 9000, host: '0.0.0.0' })