EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
github-webhook.js
Go to the documentation of this file.
1/**
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
7 */
8
9/**
10 * @apiDefine GitHubWebhookGroup GitHub Webhook CI/CD
11 * GitHub webhook automation for agent builds
12 */
13
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');
21
22// Verify GitHub webhook signature
23/**
24 *
25 * @param req
26 * @param secret
27 */
28function verifyGitHubSignature(req, secret) {
29 const signature = req.headers['x-hub-signature-256'];
30 if (!signature) return false;
31
32 const hmac = crypto.createHmac('sha256', secret);
33 const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
34
35 return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
36}
37
38/**
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:
66 * {
67 * "success": true,
68 * "message": "Agent build job queued",
69 * "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
70 * "commits": 3
71 * }
72 * @apiExample {json} Success-Response (200) - Skipped:
73 * {
74 * "success": true,
75 * "message": "No agent or tray changes detected",
76 * "commits": 2
77 * }
78 */
79// POST /api/github/webhook
80router.post('/webhook', express.json(), async (req, res) => {
81 try {
82 const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET || process.env.github_token;
83
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' });
88 }
89
90 const event = req.headers['x-github-event'];
91 const payload = req.body;
92
93 console.log('[GitHub Webhook] Received event:', event);
94
95 // Only handle push events to main branch
96 if (event === 'push' && payload.ref === 'refs/heads/main') {
97 const commits = payload.commits || [];
98
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')
102 );
103
104 if (!hasNonVersionBumpCommits) {
105 console.log('[GitHub Webhook] Only version bump commits detected, skipping build to prevent loop');
106 return res.json({
107 success: true,
108 message: 'Version bump commits ignored',
109 commits: commits.length
110 });
111 }
112
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/'));
116 });
117
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/'));
121 });
122
123 if (agentModified) {
124 console.log('[GitHub Webhook] Agent files modified, queuing build job...');
125
126 // Queue build job via builder service (non-blocking)
127 try {
128 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
129 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
130 timeout: 5000
131 });
132
133 console.log('[GitHub Webhook] Build job queued:', response.data);
134
135 return res.json({
136 success: true,
137 message: 'Agent build job queued',
138 jobId: response.data.jobId,
139 commits: commits.length
140 });
141 } catch (buildErr) {
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
146 });
147 }
148 } else if (trayModified) {
149 console.log('[GitHub Webhook] Tray files modified, triggering tray build...');
150
151 // Trigger tray build directly (lighter than full agent build)
152 try {
153 await buildTrayIcon();
154
155 return res.json({
156 success: true,
157 message: 'Tray build completed',
158 commits: commits.length
159 });
160 } catch (buildErr) {
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
165 });
166 }
167 } else {
168 console.log('[GitHub Webhook] No agent or tray files modified, skipping build');
169 return res.json({
170 success: true,
171 message: 'No agent or tray changes detected',
172 commits: commits.length
173 });
174 }
175 }
176
177 res.json({ success: true, message: 'Event received but not processed' });
178 } catch (err) {
179 console.error('[GitHub Webhook] Error:', err);
180 res.status(500).json({ error: err.message });
181 }
182});
183
184/**
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):
199 * {
200 * "success": true,
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"
204 * }
205 */
206// Manual build trigger endpoint (queues job via builder)
207router.post('/build', async (req, res) => {
208 try {
209 console.log('[GitHub Webhook] Manual build triggered');
210
211 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
212 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
213 timeout: 5000
214 });
215
216 console.log('[GitHub Webhook] Build job queued:', response.data);
217
218 res.json({
219 success: true,
220 message: 'Agent build job queued',
221 jobId: response.data.jobId,
222 checkStatus: `${builderUrl}/api/build/status/${response.data.jobId}`
223 });
224 } catch (err) {
225 console.error('[GitHub Webhook] Failed to queue build:', err.message);
226 res.status(500).json({
227 error: 'Failed to queue build job',
228 details: err.message
229 });
230 }
231});
232
233// Build tray icon function
234/**
235 *
236 */
237async function buildTrayIcon() {
238 const GIT_DIR = path.resolve(process.cwd(), '..');
239 const TRAY_DIR = path.join(GIT_DIR, 'trayicon');
240
241 console.log('[Tray Build] Starting tray icon build...');
242
243 try {
244 // Run tray build script
245 execSync('./build_tray.sh', {
246 cwd: TRAY_DIR,
247 stdio: 'inherit',
248 timeout: 300000 // 5 minute timeout
249 });
250
251 console.log('[Tray Build] ✅ Tray build completed successfully');
252 } catch (err) {
253 console.error('[Tray Build] ❌ Tray build failed:', err.message);
254 throw err;
255 }
256}
257
258// Build agent function (for compatibility)
259/**
260 *
261 */
262async function buildAgent() {
263 try {
264 const builderUrl = process.env.BUILDER_URL || 'http://127.0.0.1:3001';
265 const response = await axios.post(`${builderUrl}/api/build/agent`, {}, {
266 timeout: 5000
267 });
268
269 console.log('[Agent Build] Build job queued:', response.data);
270 return response.data;
271 } catch (err) {
272 console.error('[Agent Build] Failed to queue build:', err.message);
273 throw err;
274 }
275}
276
277module.exports = {
278 router,
279 buildAgent,
280 buildTrayIcon
281};