2 * @file github-webhook.js
3 * @description GitHub webhook handler for automated CI/CD builds. Listens for push events to main branch,
4 * detects agent/tray file modifications, and triggers builds via builder service. Includes HMAC
5 * signature verification and infinite loop prevention for version bump commits.
6 * @module routes/github-webhook
10 * @apiDefine GitHubWebhookGroup GitHub Webhook CI/CD
11 * GitHub webhook automation for agent builds
14// GitHub Webhook handler for automated agent builds
15const express = require('express');
16const router = express.Router();
17const crypto = require('crypto');
18const axios = require('axios');
19const { execSync } = require('child_process');
20const path = require('path');
22// Verify GitHub webhook signature
28function verifyGitHubSignature(req, secret) {
29 const signature = req.headers['x-hub-signature-256'];
30 if (!signature) return false;
32 const hmac = crypto.createHmac('sha256', secret);
33 const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
35 return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
39 * @api {post} /api/github/webhook GitHub Webhook Handler
40 * @apiName GitHubWebhook
41 * @apiGroup GitHubWebhookGroup
42 * @apiDescription Receives GitHub push webhooks from main branch, validates HMAC signature, detects agent/tray
43 * file modifications, and queues build jobs. Ignores version bump commits to prevent infinite loops.
44 * Uses BUILDER_URL to queue jobs. No authentication beyond webhook signature.
45 * @apiHeader {string} x-hub-signature-256 HMAC SHA256 signature for request validation
46 * @apiHeader {string} x-github-event GitHub event type (e.g., 'push')
47 * @apiParam {string} ref Git ref (e.g., refs/heads/main)
48 * @apiParam {object[]} commits Array of commit objects
49 * @apiParam {string} commits.message Commit message
50 * @apiParam {string[]} commits.added Added files
51 * @apiParam {string[]} commits.modified Modified files
52 * @apiParam {string[]} commits.removed Removed files
53 * @apiSuccess {boolean} success Build job queued or skipped
54 * @apiSuccess {string} message Description of action taken
55 * @apiSuccess {string} [jobId] Build job UUID (if build queued)
56 * @apiSuccess {number} commits Number of commits processed
57 * @apiError 401 Invalid HMAC signature
58 * @apiError 500 Failed to queue build job
59 * @apiExample {curl} GitHub Webhook:
60 * curl -X POST https://api.example.com/api/github/webhook \
61 * -H "x-hub-signature-256: sha256=abc123..." \
62 * -H "x-github-event: push" \
63 * -H "Content-Type: application/json" \
64 * -d '{"ref":"refs/heads/main", "commits":[...]}'
65 * @apiExample {json} Success-Response (200) - Build Queued:
68 * "message": "Agent build job queued",
69 * "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
72 * @apiExample {json} Success-Response (200) - Skipped:
75 * "message": "No agent or tray changes detected",
79// POST /api/github/webhook
80router.post('/webhook', express.json(), async (req, res) => {
82 const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET || process.env.github_token;
84 // Verify signature if secret is set
85 if (webhookSecret && !verifyGitHubSignature(req, webhookSecret)) {
86 console.log('[GitHub Webhook] Invalid signature');
87 return res.status(401).json({ error: 'Invalid signature' });
90 const event = req.headers['x-github-event'];
91 const payload = req.body;
93 console.log('[GitHub Webhook] Received event:', event);
95 // Only handle push events to main branch
96 if (event === 'push' && payload.ref === 'refs/heads/main') {
97 const commits = payload.commits || [];
99 // Ignore version bump commits from builder (prevents infinite loop)
100 const hasNonVersionBumpCommits = commits.some(commit =>
101 !commit.message || !commit.message.startsWith('chore: bump agent version')
104 if (!hasNonVersionBumpCommits) {
105 console.log('[GitHub Webhook] Only version bump commits detected, skipping build to prevent loop');
108 message: 'Version bump commits ignored',
109 commits: commits.length
113 const agentModified = commits.some(commit => {
114 const files = [...(commit.added || []), ...(commit.modified || []), ...(commit.removed || [])];
115 return files.some(file => file.startsWith('rmm-psa-platform/agent/'));
118 const trayModified = commits.some(commit => {
119 const files = [...(commit.added || []), ...(commit.modified || []), ...(commit.removed || [])];
120 return files.some(file => file.startsWith('rmm-psa-platform/trayicon/'));
124 console.log('[GitHub Webhook] Agent files modified, queuing build job...');
126 // Queue build job via builder service (non-blocking)
128 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
129 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
133 console.log('[GitHub Webhook] Build job queued:', response.data);
137 message: 'Agent build job queued',
138 jobId: response.data.jobId,
139 commits: commits.length
142 console.error('[GitHub Webhook] Failed to queue build:', buildErr.message);
143 return res.status(500).json({
144 error: 'Failed to queue build job',
145 details: buildErr.message
148 } else if (trayModified) {
149 console.log('[GitHub Webhook] Tray files modified, triggering tray build...');
151 // Trigger tray build directly (lighter than full agent build)
153 await buildTrayIcon();
157 message: 'Tray build completed',
158 commits: commits.length
161 console.error('[GitHub Webhook] Failed to build tray:', buildErr.message);
162 return res.status(500).json({
163 error: 'Failed to build tray',
164 details: buildErr.message
168 console.log('[GitHub Webhook] No agent or tray files modified, skipping build');
171 message: 'No agent or tray changes detected',
172 commits: commits.length
177 res.json({ success: true, message: 'Event received but not processed' });
179 console.error('[GitHub Webhook] Error:', err);
180 res.status(500).json({ error: err.message });
185 * @api {post} /api/github/build Trigger Manual Agent Build
186 * @apiName TriggerManualBuild
187 * @apiGroup GitHubWebhookGroup
188 * @apiDescription Manually triggers an agent build job via builder service. No authentication required.
189 * Returns jobId for status polling. Useful for testing or manual rebuilds.
190 * @apiSuccess {boolean} success Build job queued successfully
191 * @apiSuccess {string} message Confirmation message
192 * @apiSuccess {string} jobId Build job UUID
193 * @apiSuccess {string} checkStatus URL to check build status
194 * @apiError 500 Failed to queue build job (builder service unavailable)
195 * @apiExample {curl} Manual Build:
196 * curl -X POST https://api.example.com/api/github/build \
197 * -H "Content-Type: application/json"
198 * @apiExample {json} Success-Response (200):
201 * "message": "Agent build job queued",
202 * "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
203 * "checkStatus": "http://127.0.0.1:3001/api/build/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
206// Manual build trigger endpoint (queues job via builder)
207router.post('/build', async (req, res) => {
209 console.log('[GitHub Webhook] Manual build triggered');
211 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
212 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
216 console.log('[GitHub Webhook] Build job queued:', response.data);
220 message: 'Agent build job queued',
221 jobId: response.data.jobId,
222 checkStatus: `${builderUrl}/api/build/status/${response.data.jobId}`
225 console.error('[GitHub Webhook] Failed to queue build:', err.message);
226 res.status(500).json({
227 error: 'Failed to queue build job',
233// Build tray icon function
237async function buildTrayIcon() {
238 const GIT_DIR = path.resolve(process.cwd(), '..');
239 const TRAY_DIR = path.join(GIT_DIR, 'trayicon');
241 console.log('[Tray Build] Starting tray icon build...');
244 // Run tray build script
245 execSync('./build_tray.sh', {
248 timeout: 300000 // 5 minute timeout
251 console.log('[Tray Build] ✅ Tray build completed successfully');
253 console.error('[Tray Build] ❌ Tray build failed:', err.message);
258// Build agent function (for compatibility)
262async function buildAgent() {
264 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
265 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
269 console.log('[Agent Build] Build job queued:', response.data);
270 return response.data;
272 console.error('[Agent Build] Failed to queue build:', err.message);