A production-ready Node.js app that delivers rich, color-coded Discord embeds for every GitHub event. Uses GitHub Apps for account-level installation — one setup covers all current and future repositories automatically.
- Features
- How It Works
- Architecture
- Supported Events
- Prerequisites
- GitHub App Setup
- Installation & Setup
- Configuration
- Deployment
- Testing
- Troubleshooting
- Security
- Contributing
- License
- Global repository coverage — Install once, cover all repos including future ones
- Rich Discord embeds — Color-coded notifications with avatars, links, and context
- HMAC-SHA256 verification — Every webhook payload is verified before processing
- Event-driven handlers — Dedicated, modular handler per event type
- Minimal dependencies — Express.js and Node.js stdlib only
- Production ready — systemd service, env config, and deployment scripts included
- Comprehensive logging — Detailed output for monitoring and debugging
- GitHub Event — An event occurs (push, issue, star, etc.) in any monitored repo
- Webhook Delivery — GitHub POSTs the payload to your configured webhook URL
- Signature Verification — Server checks the HMAC-SHA256 signature against your secret
- Event Routing — Payload is dispatched to the correct handler via
X-GitHub-Event - Discord Notification — Handler formats the event into an embed and sends it to Discord
GitHub Apps give you account-level installation — unlike per-repo webhooks, they:
- Cover all repositories from a single installation
- Automatically include new repos without extra configuration
- Offer fine-grained, auditable permissions
- Use webhook secrets for authentication (no private key download needed)
ghook/
├── src/
│ ├── server.js # Express server and webhook endpoint
│ ├── verify.js # HMAC-SHA256 signature verification
│ ├── discord.js # Discord webhook HTTP client
│ ├── router.js # Event routing logic
│ └── handlers/
│ ├── push.js # Git push events
│ ├── create.js # Branch/tag creation
│ ├── delete.js # Branch/tag deletion
│ ├── star.js # Repository starring
│ ├── fork.js # Repository forking
│ ├── pullRequest.js # Pull request lifecycle
│ ├── issues.js # Issue events
│ ├── issueComment.js # Issue comment events
│ ├── release.js # Release events
│ └── workflowRun.js # GitHub Actions run events
├── github-discord-bot.service # systemd unit file
├── package.json
├── .env.example
└── README.md
server.js — Express app on configurable port (default 3000). Raw body parsing for signature validation, health check at /, webhook endpoint at /webhook.
verify.js — HMAC-SHA256 verification using Node.js crypto. Timing-safe comparison prevents timing attacks.
discord.js — HTTPS client for the Discord webhook API. Sends rich embed payloads as JSON with error handling.
router.js — Maps X-GitHub-Event headers to handler functions. Logs unhandled events for visibility.
Event Handlers — Each handler extracts payload data and produces a Discord embed with author avatar, color coding, repo context, links, and timestamps.
| Event | Handler | Description | Color |
|---|---|---|---|
push |
handlePush |
Commits pushed to a branch | Green #238636 |
create |
handleCreate |
Branch or tag created | Blue #1f6feb |
delete |
handleDelete |
Branch or tag deleted | Red #f85149 |
watch |
handleStar |
Repository starred | Yellow #e3b341 |
fork |
handleFork |
Repository forked | Purple #a371f7 |
pull_request |
handlePullRequest |
PR opened, closed, merged | Blue #58a6ff |
issues |
handleIssues |
Issue opened, closed, reopened | Green/Red/Orange |
issue_comment |
handleIssueComment |
Comment on an issue | Blue #58a6ff |
release |
handleRelease |
Release published or prereleased | Green #238636 |
workflow_run |
handleWorkflowRun |
GitHub Actions run completed | Green/Red/Grey/Orange |
Push event
[User Avatar] User Name
Pushed 3 commits to `main`
[`a1b2c3d`] Initial commit message
[`e4f5g6h`] Fix bug in authentication
[`i7j8k9l`] Update documentation
Repository: owner/repo
Branch: `main`
ghook • [timestamp]
Delete event
[User Avatar] User Name
Deleted branch `feature-x`
Repository: owner/repo
Type: Branch
ghook • [timestamp]
Release event
[User Avatar] User Name
🚀 v1.0.0
Release notes or description here...
Repository: owner/repo
Tag: `v1.0.0`
Type: Release
ghook • [timestamp]
Star event
[User Avatar] User Name
⭐ owner/repo
Repository description here...
Stars: ⭐ 1,234
Forks: 🍴 567
Language: JavaScript
ghook • [timestamp]
- Node.js 16+ (tested on 20.x)
- OS Linux (Ubuntu 20.04+ recommended), macOS, or Windows
- RAM 128 MB minimum, 256 MB recommended
- Disk ~50 MB for install + logs
- GitHub account with repository creation permissions
- Discord server with "Manage Webhooks" permission in the target channel
- VPS or server for production deployment (optional for local dev)
- Inbound on port
3000(configurable) for webhook delivery - Outbound HTTPS to
discord.com - Optional: TLS certificate for a secure webhook URL
-
Go to GitHub Settings → Developer settings → GitHub Apps and click New GitHub App
-
Fill in basic info:
- Name:
ghook(or your preference) - Homepage URL: your repo or personal site
- Name:
-
Set Repository permissions:
Actions→ Read-only (for workflow run notifications)Contents→ Read-onlyIssues→ Read-onlyMetadata→ Read-only (required)Pull requests→ Read-only
-
Subscribe to events:
- ✅ Push, Create, Delete, Fork, Issues, Issue comment, Pull request, Release, Watch, Workflow run
-
Configure the webhook:
- URL:
https://yourdomain.com/webhook - Secret: a strong random string — this becomes
GITHUB_WEBHOOK_SECRETin.env - SSL verification: enabled
- URL:
-
After creating, go to Install App → install on your account → select repositories or All repositories
| Permission | Access | Required For |
|---|---|---|
Contents |
Read | Release events, repo metadata |
Issues |
Read | Issue and comment events |
Metadata |
Read | Repository info (always required) |
Pull requests |
Read | Pull request events |
No private key download needed — authentication is handled via webhook secrets.
git clone https://github.com/jedbillyb/ghook.git
cd ghook
npm install
cp .env.example .env
# Edit .env with your values
npm run dev # Auto-restarts on file changesVerify it's running:
curl http://localhost:3000
# → "GitHub → Discord bot is running."Automated:
wget https://raw.githubusercontent.com/jedbillyb/ghook/main/setup.sh
chmod +x setup.sh
sudo ./setup.shManual:
# 1. Install Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs
# 2. Create app directory
sudo mkdir -p /opt/ghook
sudo chown $USER:$USER /opt/ghook
# 3. Upload and install
scp -r ./ghook root@YOUR_VPS_IP:/opt/
cd /opt/ghook
npm install --production
# 4. Configure
cp .env.example .env
nano .envCreate a .env file in the project root:
# Discord
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
# GitHub
GITHUB_WEBHOOK_SECRET=your_strong_random_secret_here
# Server
PORT=3000
# Privacy
NOTIFY_PRIVATE_REPOS=false
# Discord rendering (optional)
# DISCORD_LEGACY_EMBEDS=true # set to fall back to classic embeds
# WEBHOOK_FOOTER=github.com/jedbillyb/ghook
# WEBHOOK_FOOTER_URL=https://github.com/jedbillyb/ghook
# Event filters (optional)
# IGNORED_EVENTS=watch,fork
# BRANCH_FILTER=main,develop,release/*
# Multi-webhook routing (optional)
# DISCORD_WEBHOOK_RELEASES=https://discord.com/api/webhooks/.../...
# DISCORD_WEBHOOK_CI=https://discord.com/api/webhooks/.../...
# ROUTES=release:RELEASES,workflow_run:CI
# Localization (optional)
# LOCALE=frDiscord webhook URL — Discord → Server Settings → Integrations → Webhooks → create or copy.
Webhook secret — 32+ character random string. Must match exactly in GitHub App settings.
Port — Default 3000. Change if occupied, and ensure your firewall allows inbound connections.
Notify private repos — Default false. Private repository events are skipped to avoid leaking activity from private work into a public channel. Set to true to opt in.
Discord legacy embeds — Default false. Messages are rendered using Discord's Components V2 format. Set to true to fall back to the classic embed rendering. Components V2 is still server-gated by Discord: if your webhook's server hasn't been granted access, V2 messages fail silently — set DISCORD_LEGACY_EMBEDS=true until access is rolled out.
Webhook footer — Default github.com/jedbillyb/ghook. Override the small attribution text shown at the bottom of each message (useful when self-hosting). Pair with WEBHOOK_FOOTER_URL to change the link target in Components V2 mode (defaults to https://github.com/jedbillyb/ghook).
Ignored events — Optional. Comma-separated list of GitHub event names that should be dropped before reaching their handler (e.g. IGNORED_EVENTS=watch,fork to silence stars and forks).
Branch filter — Optional. Comma-separated list of branch name patterns. When set, push, create, and delete events whose branch does not match any pattern are dropped. Tag refs always pass through. * matches a single path segment, so release/* matches release/v1 but not release/v1/hotfix. Example: BRANCH_FILTER=main,develop,release/*. Other event types (issues, pull requests, comments, …) are unaffected.
Multi-webhook routing — Optional. Declare additional Discord webhooks with the DISCORD_WEBHOOK_<NAME> prefix (e.g. DISCORD_WEBHOOK_RELEASES, DISCORD_WEBHOOK_CI), then set ROUTES to a comma-separated list of event:NAME pairs. The GitHub event name is matched against each rule from left to right; the first match wins. Events with no matching rule (and the case where ROUTES is unset) fall back to DISCORD_WEBHOOK_URL. Example: ROUTES=release:RELEASES,workflow_run:CI,issues:ISSUES,issue_comment:ISSUES sends releases to a dedicated channel, CI runs to another, issues and issue comments to a third, and everything else (push, pull_request, star, fork, …) to the default webhook.
Locale — Optional. Default en. Switches the language of the static strings ghook renders into Discord messages (titles, field labels, statuses). Supported values: en, fr. Tags like fr-FR are normalized to fr; unknown locales fall back to en. User-authored content — commit messages, issue/PR titles, release notes, comment bodies — is always passed through unchanged. To add a language, drop a src/i18n/<code>.json file with the same keys as en.json and register it in src/i18n/index.js.
sudo cp /opt/ghook/github-discord-bot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable github-discord-bot
sudo systemctl start github-discord-bot
# Monitor
sudo systemctl status github-discord-bot
sudo journalctl -u github-discord-bot -fFROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]docker build -t ghook .
docker run -p 3000:3000 --env-file .env ghookCaddy (recommended — auto HTTPS)
sudo apt install -y caddy/etc/caddy/Caddyfile:
yourdomain.com {
reverse_proxy localhost:3000
}
sudo systemctl restart caddyNginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Unit tests run on Node's built-in test runner — no extra dependencies required.
npm testCovers signature verification, the Components V2 / legacy embed builders, the Discord routing logic, and the push-batching utility. Every PR is also exercised by the GitHub Actions workflow at .github/workflows/ci.yml against Node 20 and 22.
# Health check
curl http://localhost:3000
# Simulate a webhook (signature will fail — expected)
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-Hub-Signature-256: sha256=fake_signature" \
-d '{"repository":{"full_name":"test/repo"},"commits":[]}'npm install -g ngrok
ngrok http 3000
# Use the ngrok URL as your GitHub App webhook URLcurl -H "Content-Type: application/json" \
-d '{"content": "Test notification from ghook"}' \
YOUR_DISCORD_WEBHOOK_URLInvalid signature - rejected
GITHUB_WEBHOOK_SECRETin.envdoesn't match GitHub App settings- Check for leading/trailing spaces — must be an exact match
Discord API error: 401
- Discord webhook URL is invalid or was deleted
- Regenerate in Discord → Server Settings → Integrations → Webhooks
Address already in use
- Port
3000is taken by another process - Change
PORTin.envor find the conflict:sudo lsof -i :3000
Service won't start
sudo systemctl status github-discord-bot
sudo journalctl -u github-discord-bot -n 50No Discord notifications
- Check service is running:
sudo systemctl status github-discord-bot - Tail logs:
sudo journalctl -u github-discord-bot -f - Test Discord webhook manually with curl
- Check GitHub App → webhook delivery history for errors
sudo ufw allow 3000
sudo ufw statussudo journalctl -u github-discord-bot -n 100 # Recent logs
sudo journalctl -u github-discord-bot -f # Live tail
sudo journalctl -u github-discord-bot --since "1 hour ago"In server.js, add verbose output for incoming events:
console.log(`📥 Received event: ${event}`, JSON.stringify(req.body, null, 2));- HMAC-SHA256 signatures prevent spoofed deliveries
- Timing-safe comparison eliminates timing oracle attacks
- Raw body verification ensures payload integrity before parsing
- Use a strong, unique webhook secret (32+ characters)
- Never commit
.envto version control — it's in.gitignore - Use HTTPS in production (Caddy handles this automatically)
- Rotate webhook secrets periodically
- Restrict
.envpermissions:chmod 600 .env - Monitor logs for unexpected event patterns
- Fork the repository
- Create a feature branch:
git checkout -b feature/new-handler - Implement changes following existing code patterns
- Test with sample webhook payloads
- Submit a pull request with a description and screenshots
- Create
src/handlers/newEvent.js - Implement the handler following existing patterns
- Register it in
router.js - Update GitHub App permissions if the new event requires them
- Test with a real or simulated payload
- ES6+ syntax throughout
- Follow existing naming conventions
- JSDoc comments on all exported functions
- Graceful error handling — never crash on a bad payload
- Keep dependencies minimal
MIT — see LICENSE for details.
