GitHub Webhooks for Auto Deploy
Automatically deploy your Node.js application to production whenever you push to GitHub using webhooks.
What Are GitHub Webhooks?
GitHub webhooks allow GitHub to send HTTP POST requests to your server whenever certain events occur in your repository. This enables real-time automation like automatic deployments whenever you push code.
The flow: You push code → GitHub sends webhook → Your server receives event → Deployment script runs → Application updates automatically.
Setting Up a Webhook Endpoint
Step 1: Create a Webhook Receiver Endpoint
Create a simple Express endpoint that GitHub can POST to. This will be called whenever events occur.
import express from 'express';
import crypto from 'crypto';
import { execSync } from 'child_process';
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
app.post('/api/deploy', (req, res) => {
// Verify webhook signature (see next section)
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
const hash = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== hash) {
console.error('Webhook signature verification failed');
return res.status(401).json({ error: 'Unauthorized' });
}
// Only deploy on push events
if (req.headers['x-github-event'] !== 'push') {
return res.status(200).json({ message: 'Ignoring non-push event' });
}
const branch = req.body.ref.split('/').pop();
// Only deploy from main branch
if (branch !== 'main') {
return res.status(200).json({ message: 'Ignoring non-main branch' });
}
console.log('Deploying from branch:', branch);
try {
// Execute deployment script
execSync('bash /home/user/deploy.sh', { stdio: 'inherit' });
return res.status(200).json({ message: 'Deployment started' });
} catch (error) {
console.error('Deployment failed:', error);
return res.status(500).json({ error: 'Deployment failed' });
}
});
app.listen(3001, () => {
console.log('Webhook receiver listening on port 3001');
});Step 2: Configure Environment Variables
Store your webhook secret in environment variables for security.
# .env.production GITHUB_WEBHOOK_SECRET=your_webhook_secret_here_min_32_chars NODE_ENV=production
Step 3: Expose Endpoint Publicly
GitHub needs to reach your endpoint. Use ngrok for local testing or deploy to a public server.
# Local testing with ngrok
npm install -g ngrok
ngrok http 3001
# You'll get a URL like: https://abc123.ngrok.io
# Webhook URL would be: https://abc123.ngrok.io/api/deploy
# Or use a reverse proxy on your server
# Configure Nginx to forward requests to your webhook endpoint
location /api/deploy {
proxy_pass http://localhost:3001;
}Verifying Webhook Signatures
GitHub signs every webhook with HMAC-SHA256. Always verify signatures to ensure webhooks are truly from GitHub.
import crypto from 'crypto';
// Middleware to verify GitHub webhook signature
const verifyWebhookSignature = (req, res, next) => {
const signature = req.headers['x-hub-signature-256'];
const githubSecret = process.env.GITHUB_WEBHOOK_SECRET;
if (!signature || !githubSecret) {
return res.status(403).json({ error: 'Missing signature or secret' });
}
// Get raw request body
const payload = req.rawBody || JSON.stringify(req.body);
// Calculate expected signature
const hash = 'sha256=' + crypto
.createHmac('sha256', githubSecret)
.update(payload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(hash)
);
if (!isValid) {
console.warn('Invalid webhook signature', { signature, hash });
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
// Use middleware on webhook route
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
// Safe to process webhook
console.log('Webhook verified and processing...');
});
// Important: Express raw body middleware (required for signature verification)
app.use(express.raw({ type: 'application/json' }));
app.use((req, res, next) => {
req.rawBody = req.body;
express.json()(req, res, next);
});Triggering Deploys on Push Events
Understanding GitHub Webhook Events
GitHub sends different event types. Check the event type header to determine what action to take.
// Handle different GitHub events
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
const eventType = req.headers['x-github-event'];
const payload = req.body;
console.log('Received GitHub event:', eventType);
// Only handle push events
if (eventType !== 'push') {
console.log('Ignoring event type:', eventType);
return res.status(200).json({ message: 'Event ignored' });
}
// Extract branch and commit info
const ref = payload.ref; // e.g., "refs/heads/main"
const branch = ref.split('/').pop(); // "main"
const commits = payload.commits;
const pushedBy = payload.pusher.name;
console.log({
branch,
commitsCount: commits.length,
pushedBy,
latestCommit: commits[0]?.message
});
// Trigger deployment
triggerDeployment(branch);
res.status(200).json({ message: 'Deployment triggered' });
});Complete Webhook Handler
const triggerDeployment = async (branch) => {
console.log('Starting deployment from branch:', branch);
try {
// Pull latest code
console.log('Pulling code...');
execSync('git pull origin ' + branch);
// Install dependencies
console.log('Installing dependencies...');
execSync('npm ci --production');
// Run tests
console.log('Running tests...');
execSync('npm test');
// Reload with PM2
console.log('Reloading application...');
execSync('pm2 reload app.js');
// Verify health
await new Promise(resolve => setTimeout(resolve, 2000));
const health = execSync('curl -f http://localhost:3000/health').toString();
console.log('Health check passed:', health);
console.log('Deployment completed successfully!');
} catch (error) {
console.error('Deployment failed:', error.message);
// Rollback on failure
console.log('Rolling back to previous version...');
execSync('git reset --hard HEAD~1');
execSync('npm ci --production');
execSync('pm2 reload app.js');
throw error;
}
};Branch Filtering
Only deploy from specific branches (usually main or production). Prevent accidental deployments from feature branches.
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
const eventType = req.headers['x-github-event'];
if (eventType !== 'push') {
return res.status(200).json({ message: 'Ignoring non-push event' });
}
const ref = req.body.ref;
const branch = ref.split('/').pop();
// List of branches that trigger deployment
const allowedBranches = ['main', 'production'];
if (!allowedBranches.includes(branch)) {
console.log('Branch not in deploy list:', branch);
return res.status(200).json({
message: 'Branch ignored',
branch,
allowedBranches
});
}
console.log('Branch allowed, triggering deployment:', branch);
triggerDeployment(branch);
res.status(200).json({
message: 'Deployment triggered',
branch
});
});Deployment Script
Create a standalone bash script that handles the complete deployment pipeline.
#!/bin/bash
set -e
# Deploy script for webhook triggers
# This script is called by the webhook handler
PROJECT_DIR="/home/user/myapp"
LOG_FILE="/home/user/deploy.log"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
notify_slack() {
local message=$1
local color=$2
curl -X POST "$SLACK_WEBHOOK" -H 'Content-Type: application/json' -d "{
"attachments": [{
"color": "$color",
"title": "Deployment Update",
"text": "$message",
"ts": $(date +%s)
}]
}"
}
# Change to project directory
cd "$PROJECT_DIR"
log "=== Deployment started ==="
# Pull latest code
log "Pulling latest code..."
git fetch origin main
git reset --hard origin/main
# Install dependencies
log "Installing dependencies..."
npm ci --production
# Run tests
log "Running tests..."
if ! npm test; then
log "Tests failed, aborting deployment"
notify_slack "Deployment failed: Tests did not pass" "danger"
exit 1
fi
# Reload application
log "Reloading application with PM2..."
pm2 reload app.js
# Wait for processes to start
sleep 3
# Health check
log "Performing health checks..."
for i in {1..5}; do
if curl -f http://localhost:3000/health > /dev/null; then
log "Health check passed"
break
fi
if [ $i -eq 5 ]; then
log "Health check failed after 5 attempts"
notify_slack "Deployment failed: Health check failed" "danger"
exit 1
fi
sleep 2
done
log "=== Deployment completed successfully ==="
notify_slack "Deployment successful" "good"Security Best Practices
Always verify webhook signatures
Use HMAC-SHA256 verification to ensure webhooks come from GitHub. Never skip signature verification.
Use environment variables for secrets
Never hardcode webhook secrets. Store in .env or secret management system.
Limit webhook endpoint exposure
Use a firewall to allow GitHub's IP addresses only. Check GitHub's documentation for current IP ranges.
Filter by branch and event type
Only deploy from specific branches. Ignore event types you don't need to handle.
Log all deployments
Keep detailed logs of all webhook triggers and deployments for auditing and debugging.
Use timing-safe comparison
Always use crypto.timingSafeEqual() for signature verification to prevent timing attacks.
Monitor webhook delivery
Check GitHub's webhook delivery logs regularly to ensure webhooks are being delivered successfully.
Testing Webhooks
1. GitHub UI Webhook Testing
GitHub allows you to manually trigger webhooks from the repository settings for testing.
# In your GitHub repository: 1. Go to Settings → Webhooks 2. Find your webhook in the list 3. Click it to edit 4. Scroll down to "Recent Deliveries" 5. Click "Redeliver" to send a test webhook 6. Check the response and see request/response details
2. Local Testing with Ngrok
Use ngrok to expose your local development server to GitHub.
# Terminal 1: Start your Node.js app npm start # Terminal 2: Expose with ngrok ngrok http 3000 # Terminal 3: Configure webhook in GitHub # Settings → Webhooks → Add webhook # Payload URL: https://your-ngrok-url.ngrok.io/api/deploy # Content type: application/json # Secret: your_webhook_secret # Events: Push events # Then test by pushing to your repo git add . git commit -m "Test webhook" git push origin main # Watch the deployment happen!
3. Manual Webhook Simulation
Test your webhook endpoint by sending a manual POST request with proper signature.
import crypto from 'crypto';
const payload = {
ref: 'refs/heads/main',
repository: { name: 'myapp' },
pusher: { name: 'testuser' },
commits: [{ message: 'Test commit' }]
};
const secret = 'your_webhook_secret';
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
fetch('https://your-domain.com/api/deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature-256': signature,
'X-GitHub-Event': 'push'
},
body: payloadString
})
.then(res => res.json())
.then(data => console.log('Response:', data))
.catch(err => console.error('Error:', err));How DeployWise Auto-Deploy Works
DeployWise simplifies webhook-based deployments with automatic setup and management:
- ✓Automatically creates and configures GitHub webhook for you
- ✓Generates secure webhook secret and stores it safely
- ✓Verifies all webhook signatures using HMAC-SHA256
- ✓Supports branch filtering and event filtering
- ✓Provides detailed logs and delivery status
- ✓Automatically rolls back on deployment failures
- ✓Sends notifications to Slack on deployment events
Related Guides
Automate deployments with GitHub webhooks
Let DeployWise handle webhook setup, verification, and automatic deployments on every push.