From ca574601d1eb2ffa5445a4ceb8024a38ac580c5d Mon Sep 17 00:00:00 2001 From: Heidi Date: Fri, 15 May 2026 00:27:08 +0100 Subject: [PATCH] meow --- .env.example | 6 +++++ README.md | 11 ++++++++ server.js | 74 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index fb886be..af18faa 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,9 @@ WEBHOOK_SECRET= # Optional. Override if pm2 is not available as "pm2" on PATH. PM2_BIN= + +# Optional. Override if npm is not available as "npm" on PATH. +NPM_BIN= + +# Optional. Override if git is not available as "git" on PATH. +GIT_BIN= diff --git a/README.md b/README.md index 68e7cfc..eccee3b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,15 @@ The app name is `linkweb`. The GitHub webhook receiver is: POST /webhook/github ``` +On a valid webhook request, the server runs: + +```sh +git pull --ff-only +npm install +npm run build +pm2 restart linkweb +``` + Set `WEBHOOK_SECRET` in `ecosystem.config.cjs` if you want GitHub `x-hub-signature-256` verification: ```js @@ -36,3 +45,5 @@ env: { WEBHOOK_SECRET: 'your-secret', } ``` + +You can also set `PM2_BIN`, `NPM_BIN`, or `GIT_BIN` if a command is not available on the production `PATH`. diff --git a/server.js b/server.js index fed2489..72671ba 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,10 @@ const webhookPath = process.env.WEBHOOK_PATH || '/webhook/github'; const webhookSecret = process.env.WEBHOOK_SECRET || ''; const pm2AppName = process.env.PM2_APP_NAME || 'linkweb'; const pm2Command = process.env.PM2_BIN || (process.platform === 'win32' ? 'pm2.cmd' : 'pm2'); +const npmCommand = process.env.NPM_BIN || (process.platform === 'win32' ? 'npm.cmd' : 'npm'); +const gitCommand = process.env.GIT_BIN || 'git'; + +let deployInProgress = false; function verifyGitHubSignature(req) { if (!webhookSecret) { @@ -33,27 +37,67 @@ function verifyGitHubSignature(req) { return received.length === expected.length && crypto.timingSafeEqual(received, expected); } +function runStep(label, command, args) { + console.log(`[deploy] ${label}`); + + return new Promise((resolve, reject) => { + execFile( + command, + args, + { + cwd: __dirname, + maxBuffer: 1024 * 1024 * 10, + windowsHide: true, + }, + (error, stdout, stderr) => { + if (stdout) { + console.log(stdout); + } + + if (stderr) { + console.error(stderr); + } + + if (error) { + reject(error); + return; + } + + resolve(); + }, + ); + }); +} + +async function runDeploy() { + if (deployInProgress) { + console.log('[deploy] Webhook ignored because a deploy is already running.'); + return; + } + + deployInProgress = true; + + try { + await runStep('Pulling latest changes', gitCommand, ['pull', '--ff-only']); + await runStep('Installing dependencies', npmCommand, ['install']); + await runStep('Building site', npmCommand, ['run', 'build']); + await runStep(`Restarting PM2 app "${pm2AppName}"`, pm2Command, ['restart', pm2AppName]); + console.log('[deploy] Complete.'); + } catch (error) { + console.error('[deploy] Failed:', error.message); + } finally { + deployInProgress = false; + } +} + app.post(webhookPath, express.raw({ type: '*/*' }), (req, res) => { if (!verifyGitHubSignature(req)) { res.status(401).json({ ok: false, error: 'Invalid webhook signature' }); return; } - res.status(202).json({ ok: true, restarting: pm2AppName }); - - execFile(pm2Command, ['restart', pm2AppName], (error, stdout, stderr) => { - if (error) { - console.error('PM2 restart failed:', error.message); - if (stderr) { - console.error(stderr); - } - return; - } - - if (stdout) { - console.log(stdout); - } - }); + res.status(202).json({ ok: true, deploying: pm2AppName }); + runDeploy(); }); app.get('/healthz', (_req, res) => {