GitHub Webhook Integration: Automating Your CI/CD Pipeline
Learn how to use GitHub webhooks to trigger builds, run tests, and deploy code automatically. A practical guide with real-world examples for teams of any size.
HookWatch Team
February 8, 2026
GitHub webhooks are one of the most powerful tools for automating your development workflow. Every push, pull request, issue, and release can trigger actions in your infrastructure—without polling the GitHub API.
Why GitHub Webhooks?
GitHub webhooks let your systems react instantly to repository events:
- CI/CD triggers: Start builds and deployments on every push
- Code review automation: Assign reviewers, run linters, post status checks
- Issue tracking: Sync GitHub issues with your project management tool
- Release management: Publish packages, update changelogs, notify stakeholders
Setting Up GitHub Webhooks
Via Repository Settings
- Go to your repository → Settings → Webhooks
- Click "Add webhook"
- Enter your payload URL
- Select content type (JSON recommended)
- Choose which events to listen for
- Click "Add webhook"
Via GitHub API
curl -X POST https://api.github.com/repos/owner/repo/hooks \
-H "Authorization: token ghp_xxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "web",
"active": true,
"events": ["push", "pull_request"],
"config": {
"url": "https://hook.hookwatch.dev/wh/your-endpoint",
"content_type": "json",
"secret": "your-webhook-secret"
}
}'
Essential Events for CI/CD
Push Events
Trigger builds on every code push:
app.post('/webhooks/github', async (req, res) => {
const event = req.headers['x-github-event'];
const payload = req.body;
if (event === 'push') {
const branch = payload.ref.replace('refs/heads/', '');
const commits = payload.commits;
console.log(`Push to ${branch}: ${commits.length} commits`);
if (branch === 'main') {
await triggerProductionDeploy(payload);
} else if (branch === 'staging') {
await triggerStagingDeploy(payload);
} else {
await triggerCIBuild(payload);
}
}
res.status(200).json({ received: true });
});
Pull Request Events
Run checks and post status updates:
async function handlePullRequest(payload) {
const { action, pull_request, repository } = payload;
switch (action) {
case 'opened':
case 'synchronize':
// Run tests on new/updated PRs
await runTestSuite(pull_request.head.sha);
// Post pending status
await updateCommitStatus(
repository.full_name,
pull_request.head.sha,
'pending',
'Tests are running...'
);
break;
case 'closed':
if (pull_request.merged) {
// Trigger deployment after merge
await triggerDeploy(pull_request.base.ref);
}
// Clean up preview environments
await destroyPreviewEnv(pull_request.number);
break;
}
}
Release Events
Automate package publishing:
async function handleRelease(payload) {
if (payload.action !== 'published') return;
const { tag_name, prerelease } = payload.release;
if (prerelease) {
await publishToStaging(tag_name);
} else {
await publishToProduction(tag_name);
await notifySlack(`Released ${tag_name}`);
await updateChangelog(payload.release);
}
}
Verifying GitHub Webhooks
GitHub signs every payload with your webhook secret using HMAC-SHA256:
const crypto = require('crypto');
function verifyGitHubSignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/webhooks/github', (req, res) => {
const signature = req.headers['x-hub-signature-256'];
if (!verifyGitHubSignature(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
});
Building a Deployment Pipeline
Here's a complete pipeline triggered by GitHub webhooks:
[GitHub Push] → [HookWatch] → [Your Server]
│
┌─────┴──────┐
│ Run Tests │
└─────┬──────┘
│
┌───────────┴───────────┐
│ │
Tests Pass Tests Fail
│ │
Deploy App Notify Team
│ │
Update Status Post PR Comment
async function deployPipeline(payload) {
const sha = payload.after;
const repo = payload.repository.full_name;
// Step 1: Run tests
await updateStatus(repo, sha, 'pending', 'Running tests...');
const testResult = await runTests(sha);
if (!testResult.success) {
await updateStatus(repo, sha, 'failure', 'Tests failed');
await notifyTeam(testResult.errors);
return;
}
// Step 2: Build
await updateStatus(repo, sha, 'pending', 'Building...');
const buildResult = await buildApp(sha);
if (!buildResult.success) {
await updateStatus(repo, sha, 'failure', 'Build failed');
return;
}
// Step 3: Deploy
await updateStatus(repo, sha, 'pending', 'Deploying...');
await deploy(buildResult.artifact);
await updateStatus(repo, sha, 'success', 'Deployed!');
}
Common Pitfalls
Webhook Timeouts
GitHub expects a response within 10 seconds. For long-running CI tasks, acknowledge immediately and process asynchronously:
app.post('/webhooks/github', async (req, res) => {
// Acknowledge immediately
res.status(200).json({ received: true });
// Process in background
processGitHubWebhook(req.body).catch(console.error);
});
Missing Events During Downtime
If your server is down during a deployment, you'll miss webhooks. Use HookWatch to buffer and retry automatically.
Branch Filtering
Don't trigger deployments for every branch:
const deployableBranches = ['main', 'staging', 'production'];
if (!deployableBranches.includes(branch)) {
console.log(`Skipping non-deployable branch: ${branch}`);
return;
}
Using HookWatch with GitHub
Route your GitHub webhooks through HookWatch for reliability:
- Create a HookWatch endpoint for your repository
- Set the HookWatch URL as your GitHub webhook URL
- Configure HookWatch to forward to your CI/CD server
- Get automatic retries, logging, and alerting
Never miss a deployment trigger again—even during server maintenance.