Ver Fonte

Add modular Node.js webhook server for Gogs

Implement HTTP server that accepts and logs Gogs webhook requests with extensible callback system for future event handling.

Features:
- Modular architecture (server, handler, logger)
- Console logging with formatted output
- Callback system for event-specific and global handlers
- Health check endpoint
- Example implementation with callbacks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude (AgentBox) há 9 meses atrás
commit
c6c4f8a633
8 ficheiros alterados com 603 adições e 0 exclusões
  1. 9 0
      .gitignore
  2. 192 0
      README.md
  3. 73 0
      examples/with-callbacks.js
  4. 17 0
      package.json
  5. 49 0
      src/index.js
  6. 45 0
      src/logger.js
  7. 142 0
      src/server.js
  8. 76 0
      src/webhookHandler.js

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.env
+.env.local
+.env.*.local
+*.log
+.DS_Store

+ 192 - 0
README.md

@@ -0,0 +1,192 @@
+# Gogs Webhook Server
+
+A modular Node.js server for receiving and processing Gogs webhooks. The server logs all incoming webhooks to the console and provides an extensible callback system for custom event handling.
+
+## Features
+
+- **Modular Architecture**: Clean separation of concerns (server, handler, logger)
+- **Extensible Callback System**: Register callbacks for specific events or all events
+- **Console Logging**: Colored, formatted logging of all webhook events
+- **Health Check Endpoint**: Built-in health monitoring
+- **Graceful Shutdown**: Proper cleanup on process termination
+- **Zero Dependencies**: Pure Node.js implementation
+
+## Requirements
+
+- Node.js >= 18.0.0
+
+## Installation
+
+```bash
+npm install
+```
+
+## Usage
+
+### Basic Usage (Logging Only)
+
+Start the server with default configuration:
+
+```bash
+npm start
+```
+
+The server will start on port 3000 and log all incoming webhooks to the console.
+
+### Development Mode (Auto-reload)
+
+```bash
+npm run dev
+```
+
+### Configuration
+
+Configure the server using environment variables:
+
+```bash
+PORT=8080 WEBHOOK_PATH=/gogs WEBHOOK_SECRET=your-secret npm start
+```
+
+Available environment variables:
+- `PORT`: Server port (default: 3000)
+- `WEBHOOK_PATH`: Webhook endpoint path (default: /webhook)
+- `WEBHOOK_SECRET`: Optional webhook secret for signature verification
+
+### Endpoints
+
+- `POST /webhook` - Webhook endpoint
+- `GET /health` - Health check endpoint
+
+## Extending with Callbacks
+
+The server is designed to be easily extended with custom callbacks. See `examples/with-callbacks.js` for a complete example.
+
+### Register Event-Specific Callbacks
+
+```javascript
+import { WebhookServer } from './src/server.js';
+
+const server = new WebhookServer({ port: 3000 });
+const handler = server.getHandler();
+
+// Handle push events
+handler.on('push', async (payload, headers) => {
+  console.log('Push to:', payload.repository.name);
+  console.log('Commits:', payload.commits.length);
+  // Your custom logic here
+});
+
+// Handle pull request events
+handler.on('pull_request', async (payload, headers) => {
+  console.log('PR:', payload.action);
+  // Your custom logic here
+});
+
+server.start();
+```
+
+### Register Global Callbacks
+
+```javascript
+// Handle all events
+handler.onAny(async (eventType, payload, headers) => {
+  console.log('Received event:', eventType);
+  // Send to analytics, external logging, etc.
+});
+```
+
+### Run the Example
+
+```bash
+node examples/with-callbacks.js
+```
+
+## Gogs Webhook Configuration
+
+1. Go to your Gogs repository settings
+2. Navigate to Webhooks → Add Webhook → Gogs
+3. Configure:
+   - **Payload URL**: `http://your-server:3000/webhook`
+   - **Content Type**: `application/json`
+   - **Secret**: (optional) your webhook secret
+   - **Events**: Select which events to receive
+4. Click "Add Webhook"
+
+### Supported Events
+
+- `push` - Repository push
+- `create` - Branch or tag creation
+- `delete` - Branch or tag deletion
+- `pull_request` - Pull request actions
+- `issues` - Issue actions
+- `issue_comment` - Issue comment actions
+- `release` - Release actions
+
+## Project Structure
+
+```
+.
+├── src/
+│   ├── index.js           # Main entry point
+│   ├── server.js          # HTTP server implementation
+│   ├── webhookHandler.js  # Webhook processing and callbacks
+│   └── logger.js          # Logging utility
+├── examples/
+│   └── with-callbacks.js  # Example with custom callbacks
+├── package.json
+└── README.md
+```
+
+## Architecture
+
+### WebhookServer (`src/server.js`)
+- Creates HTTP server
+- Handles routing and request parsing
+- Manages webhook verification
+- Delegates webhook processing to handler
+
+### WebhookHandler (`src/webhookHandler.js`)
+- Manages callback registration
+- Executes callbacks for events
+- Supports event-specific and global callbacks
+- Provides error handling for callbacks
+
+### Logger (`src/logger.js`)
+- Formats and colorizes console output
+- Provides structured logging methods
+- Handles timestamp formatting
+
+## Testing
+
+You can test the webhook server using curl:
+
+```bash
+# Test health check
+curl http://localhost:3000/health
+
+# Test webhook with push event
+curl -X POST http://localhost:3000/webhook \
+  -H "Content-Type: application/json" \
+  -H "X-Gogs-Event: push" \
+  -H "X-Gogs-Delivery: 12345" \
+  -d '{
+    "ref": "refs/heads/main",
+    "repository": {
+      "name": "test-repo",
+      "full_name": "user/test-repo"
+    },
+    "pusher": {
+      "username": "testuser"
+    },
+    "commits": [
+      {
+        "id": "abc123",
+        "message": "Test commit"
+      }
+    ]
+  }'
+```
+
+## License
+
+ISC

+ 73 - 0
examples/with-callbacks.js

@@ -0,0 +1,73 @@
+/**
+ * Example: Webhook server with custom callbacks
+ * This demonstrates how to extend the server with custom event handlers
+ */
+import { WebhookServer } from '../src/server.js';
+import logger from '../src/logger.js';
+
+const server = new WebhookServer({
+  port: 3000,
+  path: '/webhook'
+});
+
+const handler = server.getHandler();
+
+// Handle push events
+handler.on('push', async (payload, headers) => {
+  logger.info('Processing push event');
+
+  const { repository, pusher, commits, ref } = payload;
+
+  console.log('\nPush Details:');
+  console.log(`  Repository: ${repository?.full_name || repository?.name}`);
+  console.log(`  Pusher: ${pusher?.username || pusher?.name}`);
+  console.log(`  Branch: ${ref}`);
+  console.log(`  Commits: ${commits?.length || 0}`);
+
+  if (commits && commits.length > 0) {
+    console.log('\n  Recent commits:');
+    commits.slice(0, 3).forEach(commit => {
+      console.log(`    - ${commit.id?.substring(0, 7)}: ${commit.message}`);
+    });
+  }
+});
+
+// Handle create events (new branch or tag)
+handler.on('create', async (payload, headers) => {
+  logger.info('Processing create event');
+
+  const { ref, ref_type, repository } = payload;
+  console.log(`\nCreated ${ref_type}: ${ref} in ${repository?.name}`);
+});
+
+// Handle delete events
+handler.on('delete', async (payload, headers) => {
+  logger.info('Processing delete event');
+
+  const { ref, ref_type, repository } = payload;
+  console.log(`\nDeleted ${ref_type}: ${ref} from ${repository?.name}`);
+});
+
+// Handle pull request events
+handler.on('pull_request', async (payload, headers) => {
+  logger.info('Processing pull request event');
+
+  const { action, pull_request, repository } = payload;
+  console.log(`\nPull Request ${action}:`);
+  console.log(`  #${pull_request?.number}: ${pull_request?.title}`);
+  console.log(`  Repository: ${repository?.name}`);
+  console.log(`  Author: ${pull_request?.user?.username}`);
+});
+
+// Global callback for all events
+handler.onAny(async (eventType, payload, headers) => {
+  // You can add analytics, logging to external services, etc.
+  const delivery = headers['x-gogs-delivery'];
+  console.log(`\n[Analytics] Event: ${eventType}, Delivery ID: ${delivery}`);
+});
+
+// Start the server
+server.start();
+
+console.log('\nServer started with custom callbacks!');
+console.log('Registered callbacks:', handler.getStats());

+ 17 - 0
package.json

@@ -0,0 +1,17 @@
+{
+  "name": "gogs-webhook-server",
+  "version": "1.0.0",
+  "description": "Modular Node.js server for handling Gogs webhooks",
+  "main": "src/index.js",
+  "type": "module",
+  "scripts": {
+    "start": "node src/index.js",
+    "dev": "node --watch src/index.js"
+  },
+  "keywords": ["gogs", "webhook", "git"],
+  "author": "",
+  "license": "ISC",
+  "engines": {
+    "node": ">=18.0.0"
+  }
+}

+ 49 - 0
src/index.js

@@ -0,0 +1,49 @@
+/**
+ * Main entry point for Gogs webhook server
+ */
+import { WebhookServer } from './server.js';
+import logger from './logger.js';
+
+// Configuration
+const config = {
+  port: process.env.PORT || 3000,
+  path: process.env.WEBHOOK_PATH || '/webhook',
+  secret: process.env.WEBHOOK_SECRET || null
+};
+
+// Create and start server
+const server = new WebhookServer(config);
+
+// Get the webhook handler for registering callbacks
+const handler = server.getHandler();
+
+// Example: You can register callbacks here
+// Uncomment to add custom behavior:
+
+// handler.on('push', (payload, headers) => {
+//   logger.info('Push event received!', {
+//     repository: payload.repository?.name,
+//     pusher: payload.pusher?.username,
+//     commits: payload.commits?.length
+//   });
+// });
+
+// handler.onAny((eventType, payload, headers) => {
+//   logger.info(`Any event: ${eventType}`);
+// });
+
+// Start the server
+server.start();
+
+// Graceful shutdown
+process.on('SIGTERM', async () => {
+  logger.info('SIGTERM received, shutting down gracefully');
+  await server.stop();
+  process.exit(0);
+});
+
+process.on('SIGINT', async () => {
+  logger.info('SIGINT received, shutting down gracefully');
+  await server.stop();
+  process.exit(0);
+});

+ 45 - 0
src/logger.js

@@ -0,0 +1,45 @@
+/**
+ * Logger module for webhook events
+ */
+
+export class Logger {
+  constructor(options = {}) {
+    this.enableTimestamps = options.enableTimestamps ?? true;
+    this.enableColors = options.enableColors ?? true;
+  }
+
+  _getTimestamp() {
+    return this.enableTimestamps ? `[${new Date().toISOString()}]` : '';
+  }
+
+  _colorize(text, colorCode) {
+    return this.enableColors ? `\x1b[${colorCode}m${text}\x1b[0m` : text;
+  }
+
+  info(message, data = null) {
+    const timestamp = this._getTimestamp();
+    console.log(`${timestamp} ${this._colorize('INFO', '36')}:`, message);
+    if (data) {
+      console.log(JSON.stringify(data, null, 2));
+    }
+  }
+
+  error(message, error = null) {
+    const timestamp = this._getTimestamp();
+    console.error(`${timestamp} ${this._colorize('ERROR', '31')}:`, message);
+    if (error) {
+      console.error(error);
+    }
+  }
+
+  webhook(event, payload) {
+    const timestamp = this._getTimestamp();
+    console.log(`\n${this._colorize('='.repeat(80), '33')}`);
+    console.log(`${timestamp} ${this._colorize('WEBHOOK', '32')} - Event: ${this._colorize(event, '35')}`);
+    console.log(`${this._colorize('-'.repeat(80), '33')}`);
+    console.log(JSON.stringify(payload, null, 2));
+    console.log(`${this._colorize('='.repeat(80), '33')}\n`);
+  }
+}
+
+export default new Logger();

+ 142 - 0
src/server.js

@@ -0,0 +1,142 @@
+/**
+ * HTTP server for receiving Gogs webhooks
+ */
+import http from 'http';
+import logger from './logger.js';
+import { WebhookHandler } from './webhookHandler.js';
+
+export class WebhookServer {
+  constructor(options = {}) {
+    this.port = options.port || 3000;
+    this.path = options.path || '/webhook';
+    this.secret = options.secret || null;
+    this.webhookHandler = new WebhookHandler();
+    this.server = null;
+  }
+
+  /**
+   * Parse the request body
+   */
+  _parseBody(req) {
+    return new Promise((resolve, reject) => {
+      let body = '';
+      req.on('data', chunk => {
+        body += chunk.toString();
+      });
+      req.on('end', () => {
+        try {
+          resolve(body ? JSON.parse(body) : {});
+        } catch (error) {
+          reject(new Error('Invalid JSON payload'));
+        }
+      });
+      req.on('error', reject);
+    });
+  }
+
+  /**
+   * Verify webhook signature if secret is configured
+   */
+  _verifySignature(signature, payload) {
+    if (!this.secret) {
+      return true;
+    }
+    // Gogs uses HMAC-SHA256 for signatures
+    // Format: X-Gogs-Signature: <hmac-sha256-hex>
+    // Implementation can be added when secret verification is needed
+    logger.info('Signature verification skipped (not implemented)');
+    return true;
+  }
+
+  /**
+   * Handle incoming HTTP requests
+   */
+  async _handleRequest(req, res) {
+    const { method, url } = req;
+
+    // Health check endpoint
+    if (url === '/health' && method === 'GET') {
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
+      return;
+    }
+
+    // Webhook endpoint
+    if (url === this.path && method === 'POST') {
+      try {
+        const payload = await this._parseBody(req);
+        const eventType = req.headers['x-gogs-event'] || 'unknown';
+        const signature = req.headers['x-gogs-signature'];
+        const delivery = req.headers['x-gogs-delivery'];
+
+        // Log basic request info
+        logger.info('Received webhook', {
+          event: eventType,
+          delivery: delivery,
+          hasSignature: !!signature
+        });
+
+        // Verify signature if configured
+        if (!this._verifySignature(signature, payload)) {
+          res.writeHead(401, { 'Content-Type': 'application/json' });
+          res.end(JSON.stringify({ error: 'Invalid signature' }));
+          return;
+        }
+
+        // Process webhook
+        await this.webhookHandler.handle(eventType, payload, req.headers);
+
+        // Send success response
+        res.writeHead(200, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ status: 'received' }));
+      } catch (error) {
+        logger.error('Error processing webhook', error);
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ error: error.message }));
+      }
+      return;
+    }
+
+    // 404 for other routes
+    res.writeHead(404, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ error: 'Not found' }));
+  }
+
+  /**
+   * Start the server
+   */
+  start() {
+    this.server = http.createServer((req, res) => this._handleRequest(req, res));
+
+    this.server.listen(this.port, () => {
+      logger.info(`Webhook server listening on port ${this.port}`);
+      logger.info(`Webhook endpoint: http://localhost:${this.port}${this.path}`);
+      logger.info(`Health check: http://localhost:${this.port}/health`);
+    });
+
+    return this;
+  }
+
+  /**
+   * Stop the server
+   */
+  stop() {
+    return new Promise((resolve) => {
+      if (this.server) {
+        this.server.close(() => {
+          logger.info('Server stopped');
+          resolve();
+        });
+      } else {
+        resolve();
+      }
+    });
+  }
+
+  /**
+   * Get the webhook handler instance for registering callbacks
+   */
+  getHandler() {
+    return this.webhookHandler;
+  }
+}

+ 76 - 0
src/webhookHandler.js

@@ -0,0 +1,76 @@
+/**
+ * Webhook handler with modular callback system
+ */
+import logger from './logger.js';
+
+export class WebhookHandler {
+  constructor() {
+    this.callbacks = new Map();
+    this.globalCallbacks = [];
+  }
+
+  /**
+   * Register a callback for a specific event type
+   * @param {string} eventType - The Gogs event type (e.g., 'push', 'create', 'delete')
+   * @param {Function} callback - Function to handle the event (payload, headers) => void
+   */
+  on(eventType, callback) {
+    if (!this.callbacks.has(eventType)) {
+      this.callbacks.set(eventType, []);
+    }
+    this.callbacks.get(eventType).push(callback);
+    logger.info(`Registered callback for event: ${eventType}`);
+  }
+
+  /**
+   * Register a global callback that runs for all events
+   * @param {Function} callback - Function to handle any event (eventType, payload, headers) => void
+   */
+  onAny(callback) {
+    this.globalCallbacks.push(callback);
+    logger.info('Registered global callback');
+  }
+
+  /**
+   * Process an incoming webhook
+   * @param {string} eventType - The event type from X-Gogs-Event header
+   * @param {object} payload - The parsed webhook payload
+   * @param {object} headers - Request headers
+   */
+  async handle(eventType, payload, headers) {
+    logger.webhook(eventType, payload);
+
+    // Execute global callbacks
+    for (const callback of this.globalCallbacks) {
+      try {
+        await callback(eventType, payload, headers);
+      } catch (error) {
+        logger.error('Error in global callback', error);
+      }
+    }
+
+    // Execute event-specific callbacks
+    const callbacks = this.callbacks.get(eventType);
+    if (callbacks && callbacks.length > 0) {
+      for (const callback of callbacks) {
+        try {
+          await callback(payload, headers);
+        } catch (error) {
+          logger.error(`Error in callback for event '${eventType}'`, error);
+        }
+      }
+    }
+  }
+
+  /**
+   * Get count of registered callbacks
+   */
+  getStats() {
+    return {
+      globalCallbacks: this.globalCallbacks.length,
+      eventCallbacks: Object.fromEntries(
+        Array.from(this.callbacks.entries()).map(([event, cbs]) => [event, cbs.length])
+      )
+    };
+  }
+}