A production-ready, secure webhook server for automating CI/CD deployments from GitHub
Features β’ Quick Start β’ API β’ Configuration β’ Contributing
- β¨ Features
- π Quick Start
- π¦ Installation
- βοΈ Configuration
- π GitHub Webhook Setup
- π API Documentation
- π οΈ Development
- π§ͺ Testing
- ποΈ Architecture
- π Security
- π Deployment
- π Monitoring
- π§ Troubleshooting
- π€ Contributing
- GitHub Signature Verification - Cryptographic webhook validation
- Rate Limiting - Configurable request throttling (default: 100 req/min)
- CORS Protection - Cross-origin request security
- Helmet Security Headers - Additional HTTP security layers
- Input Validation - Joi schema validation for all inputs
- Script Sanitization - Path traversal protection
- TypeScript - Full type safety and IntelliSense support
- Modular Architecture - Clean separation of concerns
- Graceful Shutdown - Proper cleanup of resources and active deployments
- Error Handling - Custom error classes with proper HTTP status codes
- Request Tracing - Unique request IDs for debugging
- Health Checks -
/healthendpoint with deployment statistics - Metrics API -
/metricsendpoint for monitoring tools - Structured Logging - Winston-powered JSON logging with rotation
- Deployment Tracking - Success/failure statistics and timing
- Hot Reload - Development mode with automatic restarts
- Test Suite - Jest testing with coverage reporting
- Code Quality - ESLint + Prettier for consistent code style
- Comprehensive Documentation - API docs, examples, and guides
Get your webhook server running in under 5 minutes:
# 1. Clone the repository
git clone https://github.com/EmadMirzaieR/webhook-deploy.git
cd webhook-deploy
# 2. Install dependencies
pnpm install
# 3. Set up environment
cp env-example .env
# Edit .env with your webhook secret
# 4. Configure projects
# Edit config.json with your repositories
# 5. Start development server
pnpm run devYour webhook server is now running at http://localhost:6521! π
# Check server health
curl http://localhost:6521/health
# View metrics
curl http://localhost:6521/metrics- Node.js >= 14.x
- pnpm (recommended) or npm
- Git (for deployment scripts)
git clone https://github.com/EmadMirzaieR/webhook-deploy.git
cd webhook-deploy
pnpm install# Clone and build
git clone https://github.com/EmadMirzaieR/webhook-deploy.git
cd webhook-deploy
pnpm install --prod
pnpm run build
# Start production server
pnpm startnpm install -g webhook-deploy
webhook-deploy --config ./config.jsonCreate a .env file in the project root:
# Required: GitHub webhook secret
WEBHOOK_SECRET=your_super_secret_webhook_key
# Optional: Environment settings
NODE_ENV=production # development | production
LOG_LEVEL=info # error | warn | info | debug
CONFIG_PATH=./config.json # Path to configuration file
PORT=3000 # Override config.json portCreate or edit config.json:
{
"port": 3000,
"logDirectory": "./logs",
"maxLogFiles": 10,
"maxLogSize": "20m",
"rateLimit": {
"windowMs": 60000,
"max": 100
},
"cors": {
"origin": ["https://yourdomain.com", "https://api.yourdomain.com"],
"credentials": false
},
"projects": [
{
"repo": "username/frontend-app",
"branch": "main",
"script": "./scripts/deploy-frontend.sh",
"workingDirectory": "/var/www/frontend",
"timeout": 300000,
"enabled": true
},
{
"repo": "username/api-service",
"branch": "production",
"script": "npm run deploy:prod && pm2 restart api",
"workingDirectory": "/home/user/api",
"timeout": 600000,
"enabled": true
}
]
}| Option | Type | Default | Description |
|---|---|---|---|
port |
number | 3000 |
Server port |
logDirectory |
string | "./logs" |
Log files directory |
maxLogFiles |
number | 10 |
Max log files to keep |
maxLogSize |
string | "20m" |
Max size per log file |
| Option | Type | Default | Description |
|---|---|---|---|
rateLimit.windowMs |
number | 60000 |
Rate limit window (ms) |
rateLimit.max |
number | 100 |
Max requests per window |
cors.origin |
string/array | "*" |
Allowed origins |
cors.credentials |
boolean | false |
Allow credentials |
| Option | Type | Required | Description |
|---|---|---|---|
repo |
string | β | GitHub repo (owner/name) |
branch |
string | β | Branch to monitor |
script |
string | β | Deployment command |
workingDirectory |
string | β | Script execution directory |
timeout |
number | β | Script timeout (ms) |
enabled |
boolean | β | Enable/disable project |
- Go to your GitHub repository
- Navigate to Settings β Webhooks
- Click Add webhook
Payload URL: https://your-server.com/github-webhook
Content type: application/json
Secret: [your WEBHOOK_SECRET from .env]
SSL verification: Enable (recommended)
- Recommended: Select "Just the push event"
- Advanced: Choose individual events if needed
GitHub provides a test button to verify your setup. You should see:
- β 200-202 response for successful deployments
- β Request ID in response for tracking
GET /health
curl http://localhost:6521/healthResponse:
{
"status": "ok",
"uptime": 3600.5,
"timestamp": 1640995200000,
"version": "0.2.0",
"deployments": {
"total": 42,
"successful": 40,
"failed": 2
}
}GET /metrics
curl http://localhost:6521/metricsResponse:
{
"deployments": {
"total": 42,
"successful": 40,
"failed": 2
},
"activeDeployments": 1,
"uptime": 3600.5,
"projectCount": 3,
"enabledProjects": 2
}POST /github-webhook
Headers:
Content-Type: application/json
X-GitHub-Event: push
X-Hub-Signature-256: sha256=<signature>
Success Response (202):
{
"message": "Deployment triggered",
"requestId": "abc123def456",
"projects": 1
}Error Responses:
| Status | Error | Description |
|---|---|---|
| 400 | Invalid signature | Webhook signature verification failed |
| 403 | Forbidden | Rate limit exceeded |
| 404 | Not found | Endpoint doesn't exist |
| 422 | Validation error | Invalid payload format |
# Development
pnpm run dev # Start with hot reload
pnpm run watch # TypeScript watch mode
# Building
pnpm run build # Compile TypeScript
pnpm run start # Run production build
# Testing
pnpm test # Run test suite
pnpm run test:watch # Run tests in watch mode
pnpm run test:coverage # Run with coverage report
# Code Quality
pnpm run lint # Run ESLint
pnpm run lint:fix # Fix ESLint issues
pnpm run format # Format with Prettierwebhook-deploy/
βββ src/
β βββ types/ # TypeScript type definitions
β β βββ index.ts
β βββ services/ # Business logic services
β β βββ ConfigService.ts # Configuration management
β β βββ LoggerService.ts # Logging and monitoring
β β βββ WebhookService.ts # Webhook verification
β β βββ DeploymentService.ts # Script execution
β βββ app.ts # Express application setup
β βββ index.ts # Application entry point
βββ tests/
β βββ services/ # Service unit tests
β βββ setup.ts # Test configuration
βββ dist/ # Compiled JavaScript (generated)
βββ logs/ # Application logs (generated)
βββ config.json # Project configuration
βββ .env # Environment variables
βββ package.json # Dependencies and scripts
- Create a service in
src/services/ - Add types in
src/types/index.ts - Write tests in
tests/services/ - Update the main app in
src/app.ts
Example service:
// src/services/NotificationService.ts
class NotificationService {
private static instance: NotificationService;
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
public async sendDeploymentNotification(result: DeploymentResult): Promise<void> {
// Implementation here
}
}
export default NotificationService;# Run all tests
pnpm test
# Run specific test file
pnpm test ConfigService
# Run with coverage
pnpm run test:coverage
# Watch mode for development
pnpm run test:watchTests use Jest and follow this structure:
// tests/services/YourService.test.ts
import YourService from '../../src/services/YourService';
describe('YourService', () => {
let service: YourService;
beforeEach(() => {
service = YourService.getInstance();
});
describe('methodName', () => {
it('should do something', () => {
// Test implementation
expect(service.methodName()).toBe(expectedValue);
});
});
});Test webhook endpoints with curl:
# Test webhook with signature
PAYLOAD='{"repository":{"full_name":"test/repo"},"ref":"refs/heads/main"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "your-secret" | cut -d' ' -f2)
curl -X POST http://localhost:6521/github-webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-Hub-Signature-256: sha256=$SIGNATURE" \
-d "$PAYLOAD"The application follows a clean service layer architecture:
βββββββββββββββββββ
β HTTP Layer β β Express.js routes and middleware
βββββββββββββββββββ€
β Service Layer β β Business logic services
βββββββββββββββββββ€
β Data Layer β β Configuration and logging
βββββββββββββββββββ
graph TD
A[WebhookApp] --> B[ConfigService]
A --> C[LoggerService]
A --> D[WebhookService]
A --> E[DeploymentService]
E --> C
E --> D
C --> B
sequenceDiagram
participant GH as GitHub
participant WH as Webhook Server
participant DS as DeploymentService
participant Script as Deploy Script
GH->>WH: POST /github-webhook
WH->>WH: Verify signature
WH->>WH: Validate payload
WH->>WH: Find matching projects
WH-->>GH: 202 Accepted
WH->>DS: Execute deployment
DS->>Script: Run deployment script
Script-->>DS: Success/Failure
DS-->>WH: Log result
- π Signature Verification: HMAC-SHA256 webhook validation
- π‘οΈ Rate Limiting: Prevent abuse and DoS attacks
- π CORS Protection: Control cross-origin requests
- π Security Headers: Helmet.js security middleware
- β Input Validation: Joi schema validation
- π« Path Sanitization: Prevent directory traversal
# Use strong, unique webhook secrets
WEBHOOK_SECRET=$(openssl rand -hex 32)
# Restrict environment access
chmod 600 .env# Run behind reverse proxy (nginx/apache)
# Enable HTTPS/TLS
# Configure firewall rules# Run as non-root user
useradd --system --home /opt/webhook-deploy webhook
chown -R webhook:webhook /opt/webhook-deploy
# Restrict file permissions
chmod 750 scripts/
chmod +x scripts/*.sh#!/bin/bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Validate inputs
if [[ -z "${WEBHOOK_REPO:-}" ]]; then
echo "Error: WEBHOOK_REPO not set"
exit 1
fi
# Use absolute paths
cd /var/www/production
# Log all commands
exec > >(tee -a deployment.log)
exec 2>&1- Strong webhook secret (32+ characters)
- HTTPS enabled
- Rate limiting configured
- CORS properly configured
- Scripts use absolute paths
- Non-root user execution
- File permissions restricted
- Log monitoring enabled
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
COPY config.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]# Build and run
docker build -t webhook-deploy .
docker run -d -p 3000:3000 --env-file .env webhook-deploy# Install PM2
npm install -g pm2
# Start application
pm2 start dist/index.js --name webhook-deploy
# Save PM2 configuration
pm2 save
# Setup startup script
pm2 startup# /etc/systemd/system/webhook-deploy.service
[Unit]
Description=GitHub Webhook Deploy Server
After=network.target
[Service]
Type=simple
User=webhook
WorkingDirectory=/opt/webhook-deploy
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target# Enable and start service
sudo systemctl enable webhook-deploy
sudo systemctl start webhook-deploy
# Check status
sudo systemctl status webhook-deploy# /etc/nginx/sites-available/webhook-deploy
server {
listen 80;
server_name webhook.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name webhook.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}The server provides several monitoring endpoints:
# Health check
curl https://webhook.yourdomain.com/health
# Detailed metrics
curl https://webhook.yourdomain.com/metrics# View recent logs
tail -f logs/combined.log
# Filter error logs
grep "error" logs/combined.log | tail -20
# Search by request ID
grep "abc123def456" logs/combined.log// Custom metrics endpoint for Prometheus
app.get('/prometheus', (req, res) => {
const stats = loggerService.getDeploymentStats();
const metrics = `
# HELP webhook_deployments_total Total number of deployments
# TYPE webhook_deployments_total counter
webhook_deployments_total ${stats.total}
# HELP webhook_deployments_successful Successful deployments
# TYPE webhook_deployments_successful counter
webhook_deployments_successful ${stats.successful}
# HELP webhook_deployments_failed Failed deployments
# TYPE webhook_deployments_failed counter
webhook_deployments_failed ${stats.failed}
`;
res.set('Content-Type', 'text/plain');
res.send(metrics);
});Key metrics to monitor:
- Request rate and response times
- Deployment success/failure rates
- Active deployment count
- Error rates by type
- Server uptime and memory usage
# Check if server is running
curl http://localhost:6521/health
# Verify GitHub can reach your server
# Check webhook delivery in GitHub settings
# Test webhook manually
curl -X POST http://localhost:6521/github-webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-d '{"repository":{"full_name":"test/repo"},"ref":"refs/heads/main"}'# Check webhook secret matches
echo $WEBHOOK_SECRET
# Test signature generation
PAYLOAD='{"test":"data"}'
echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "your-secret"# Check script permissions
ls -la scripts/deploy.sh
# Test script manually
cd /path/to/working/directory
./scripts/deploy.sh
# Check script logs
tail -f logs/combined.log | grep "deployment"# Check current rate limit settings
grep -A5 "rateLimit" config.json
# Monitor rate limit hits
tail -f logs/combined.log | grep "rate limit"Enable detailed logging:
# Set debug log level
export LOG_LEVEL=debug
# Or in .env file
echo "LOG_LEVEL=debug" >> .env# Check memory usage
ps aux | grep node
# Monitor active deployments
curl http://localhost:6521/metrics | grep active
# Check log file sizes
du -sh logs/*We welcome contributions! Here's how to get started:
# Fork the repository
git clone https://github.com/your-username/webhook-deploy.git
cd webhook-deploy
# Install dependencies
pnpm install
# Create feature branch
git checkout -b feature/amazing-feature
# Make your changes
# ... code changes ...
# Run tests
pnpm test
# Check code quality
pnpm run lint
pnpm run format
# Commit changes
git commit -m "Add amazing feature"
# Push to GitHub
git push origin feature/amazing-feature
# Create Pull RequestWe use ESLint and Prettier for consistent code style:
# Check code style
pnpm run lint
# Auto-fix issues
pnpm run lint:fix
# Format code
pnpm run format- All new features must include tests
- Maintain or improve test coverage
- Tests must pass in CI/CD
# Run tests with coverage
pnpm run test:coverage
# Coverage should be > 80%- Clear Description: Explain what your PR does and why
- Small Changes: Keep PRs focused and small
- Tests: Include tests for new functionality
- Documentation: Update README if needed
- Backwards Compatibility: Don't break existing APIs
When reporting issues, please include:
- Environment: Node.js version, OS, etc.
- Configuration: Relevant config (remove secrets!)
- Logs: Error logs and stack traces
- Steps to Reproduce: Clear reproduction steps
This project is licensed under the MIT License - see the LICENSE file for details.
- Express.js - Web framework
- Winston - Logging library
- Joi - Schema validation
- Helmet - Security middleware
- Jest - Testing framework
- Repository: GitHub
- Issues: Bug Reports
- Discussions: GitHub Discussions
- Author: Emad Mirzaie
Made with β€οΈ by Emad Mirzaie
β Star this repository if it helped you! β