Receives GitHub webhook events, validates them, builds formatted notifications, and enqueues them for delivery to RocketChat and/or Microsoft Teams (via Power Automate).
GitHub → web/github.php → GithubWebhook (validate)
→ GithubMessageBuilder (format)
→ NotificationQueue (enqueue to Redis)
→ (fallback) Power Automate direct POST
| File | Purpose |
|---|---|
web/github.php |
Active webhook receiver endpoint |
web/github-old.php |
Legacy version (RocketChat only) |
| Class | File | Responsibility |
|---|---|---|
GithubWebhook |
src/GithubWebhook.php |
Request validation, payload parsing, signature verification |
GithubMessageBuilder |
src/GithubMessageBuilder.php |
Builds chat-formatted text per event type |
NotificationQueue |
src/NotificationQueue.php |
Redis enqueue with direct fallback |
IgnoredEventException |
src/IgnoredEventException.php |
Thrown for intentionally ignored events |
NotImplementedException |
src/NotImplementedException.php |
Thrown for unsupported event types |
Copy .env.dist to .env and configure:
# GitHub webhook HMAC secret — must match the secret in your GitHub hook settings
# https://github.com/organizations/interserver/settings/hooks
GITHUB_WEBHOOKS_SECRET=
# Notification queue settings
# When NOTIF_QUEUE_ENABLED=true (default), events go to Redis for teams-chat-bot to consume.
# When false, falls through to direct Power Automate POSTs.
NOTIF_QUEUE_ENABLED=true
NOTIF_QUEUE_KEY_PREFIX=notif:
# Redis connection (canonical values come from /home/sites/mystage/include/config/config.settings.php)
# Override here only when running outside the shared InterServer environment
REDIS_HOST=dragonfly.mailbaby.net
REDIS_PORT=6379
# Disk log verbosity for incoming events:
# debug → write every event payload
# info → write only on send-failure (recommended for production)
# warn → don't write at all
LOG_LEVEL=debug
# Producer-side per-event-type rate limiter window (seconds).
# 0 disables (default) — recommended, since teams-chat-bot already coalesces downstream.
# Only set non-zero to suppress same-dedup_key bursts at the webhook itself.
RATE_LIMIT_WINDOW=0Channel webhook URLs are stored in src/config.php under $chatChannels:
$chatChannels = [
'rocketchat' => [
'int-dev' => '...', // BAckhdSAoMsPieCof... (webhook URL)
'notifications' => '...',
'hardware' => '...',
'general' => '...',
'development' => '...',
],
'teams' => [
'notifications' => 'https://...powerautomate.com/...', // Power Automate webhook
'int-dev' => 'https://...powerautomate.com/...',
'int-dev-announce'=> 'https://...powerautomate.com/...',
'development' => 'https://...powerautomate.com/...',
'hardware' => 'https://...powerautomate.com/...',
'general' => 'https://...powerautomate.com/...',
],
];- Validate signature — HMAC SHA256 against
GITHUB_WEBHOOKS_SECRET - Parse request — Extract event type and JSON payload
- Check payload — Ensure payload is not empty
- Pick room — Route to
int-dev-announceornotificationsbased on repo - Build dedup key — Per-event-type deduplication key to prevent duplicates
- Rate limit check — Optional per-key suppression (disabled by default)
- Build message — Use
GithubMessageBuilderto format chat text - Enqueue — Push to Redis via
NotificationQueue::enqueueMessage() - Log result — Write disposition to error_log
Repositories starting with sugarcraft/ or in this list route to int-dev-announce:
detain/CandyCoredetain/scoop-emulatorsdetain/detaindetain/sugarcraftdetain/watchabledetain/php-dup-finder
All others route to notifications.
The GithubMessageBuilder handles these event types with rich formatting:
| Event | Emoji | Description |
|---|---|---|
issues |
🐛 | Issue opened, closed, edited, labeled, etc. Shows issue number, title, labels, body preview (500 chars), state reason |
pull_request |
🔀 | PR opened, closed, merged. Shows PR number, title, branch arrows (feature → main), draft status, commit/file stats, body preview |
push |
📦 | Push with commit list. Shows branch, commit count, per-commit SHA (7 chars), message, author, file change counts (+3 ~2 -1 files) |
check_suite / check_run |
✅❌⏳ | CI check results. Shows check name, conclusion, branch, details link, commit message |
workflow_run / workflow_job |
✅❌⏳🔄 | GitHub Actions status. Shows workflow name, status, branch, run link, commit message, current step |
gollum |
📝 | Wiki page updates. Lists pages created/edited with titles, URLs, and summaries |
* (default) |
ℹ️ | Generic fallback message for unhandled events |
- Markdown links —
[User](https://github.com/User),[#123 Title](url) - Branch backticks —
`feature` → `main` - Labels —
`bug` `enhancement` - Emoji indicators — 🐛 issues, 🔀 PRs, 📦 pushes, 📝 wiki, ✅❌⏳🔄 CI
- File counts —
+3 ~2 -1 files(added, modified, removed) - Body truncation — First 500 characters of issue/PR body
- Multi-author attribution — "by Author" shown when commits have different authors than pusher
| Event | Reason |
|---|---|
fork |
Too spammy |
star / watch |
Too spammy |
status |
Handled via check_run / check_suite instead |
The NotificationQueue class (src/NotificationQueue.php) handles message delivery:
{
"v": 1,
"id": "uuid-v4",
"ts": 1699999999,
"expires_at": 1700000299,
"room": "notifications",
"type": "msg",
"message": "📦 Alice pushed 3 commits to org/repo `main`...",
"card": null,
"extra": {
"dedup_key": "github:push:org/repo:main",
"level": "info",
"event_type": "push",
"action": null,
"repo": "org/repo",
"data": { /* full GitHub payload */ },
"source": "webhooks/github.php"
},
"fallback_webhook_url": "https://...powerautomate.com/..."
}- Redis available? →
LPUSHto queue list, increment metrics counter - Redis unavailable → Fall back to direct Power Automate POST
- JSON oversized (>256KB) → Strip raw payload first, then truncate message
Each event type has a specific dedup key format:
| Event | Dedup Key Format |
|---|---|
push |
github:push:{repo}:{branch} |
issues |
github:issue:{repo}:{number} |
pull_request |
github:pr:{repo}:{number} |
check_run |
github:checkrun:{repo}:{sha7} |
check_suite |
github:check:{repo}:{sha7} |
workflow_run |
github:wf:{repo}:{branch}:{name} |
workflow_job |
github:wfjob:{repo}:{branch}:{jobName} |
gollum |
github:wiki:{repo} |
status |
github:status:{repo}:{sha7} |
star / watch / fork |
github:{event}:{repo} |
ping |
github:ping:{repo} |
NotificationQueue::getLastStatus() returns:
| Value | Meaning |
|---|---|
queued |
Successfully pushed to Redis |
direct_flag_off |
NOTIF_QUEUE_ENABLED=false, sent directly |
direct_no_redis |
Redis unavailable, sent directly |
direct_redis_exception |
Redis error, sent directly |
direct_oversize |
Payload too large, truncated then sent directly |
failed_no_fallback |
No fallback URL configured |
Events are logged to log/Y/m/d/ directories:
log/
└── 2026/
└── 05/
└── 11/
├── 103045_push__alice_org_repo.json
├── 103120_issues_opened__bob_detain_repo.json
└── ...
Log verbosity controlled by LOG_LEVEL env var:
debug— writes every event with payload sizeinfo— writes only on failureswarn— no logging
A command-line tool for managing code reviews of GitHub Pull Requests.
Download the pre-built PHAR archive and install it system-wide:
# Download the latest PHAR from the releases page
# or build it yourself: php bin/build-phar
# Install globally on Linux/macOS
sudo mv github-review.phar /usr/local/bin/github-review
sudo chmod +x /usr/local/bin/github-review
# Verify installation
github-review --versionSystem Requirements:
- PHP 7.4 or higher
- PHP extensions: phar, openssl (for signing), zlib (for compression)
# Clone the repository
git clone https://github.com/interserver/webhooks.interserver.net.git
cd webhooks.interserver.net
# Install dependencies
composer install
# Make the CLI executable
chmod +x bin/github-review
# Run directly
bin/github-review --help
# Or run via composer
composer exec github-review# Clone and install dependencies
git clone https://github.com/interserver/webhooks.interserver.net.git
cd webhooks.interserver.net
composer install
# Build the PHAR
php -d phar.readonly=0 bin/build-phar
# The PHAR will be at build/github-review.phar
# Optional: Sign the PHAR
openssl genpkey -algorithm RSA -out private.key
php -d phar.readonly=0 bin/build-phar --sign
# Install the built PHAR
sudo mv build/github-review.phar /usr/local/bin/github-review
sudo chmod +x /usr/local/bin/github-review# List queued review jobs
bin/github-review list
# Show queue statistics
bin/github-review metrics
# Submit a PR for review
bin/github-review submit owner/repo --pr 42
# Check review status
bin/github-review status --repo owner/repo
# View help
bin/github-review --helpList pending code review jobs in the Redis queue.
bin/github-review list
bin/github-review list --limit 20
bin/github-review list --repo owner/repo
bin/github-review list --event-type pull_request --action opened
bin/github-review list --jsonOptions:
-l, --limit N— Show at most N entries (default: 50)-r, --repo OWNER/REPO— Filter by repository-e, --event-type TYPE— Filter by source type (push, pr, check_run, etc.)-a, --action ACTION— Filter by action (opened, synchronize, closed)--json— Output as JSON-d, --detailed— Include id and PR URL in output
Show queue statistics and processing metrics.
bin/github-review metrics
bin/github-review metrics --jsonSubmit a Pull Request for code review analysis.
bin/github-review submit owner/repo --pr 42
bin/github-review submit owner/repo 42 # Short form
bin/github-review submit owner/repo --all # All open PRs
bin/github-review submit owner/repo --pr 42 --audit-types security,performance
bin/github-review submit owner/repo --pr 42 --severity errorOptions:
repo— Repository in owner/repo format (first argument)pr— PR number (second argument or--proption)-t, --audit-types TYPES— Comma-separated audit types (security,performance,documentation,logic,style,full)-s, --severity LEVEL— Minimum severity (error, warning, info)--no-security,--no-performance,--no-documentation,--no-logic,--no-style— Disable specific audits--post-diffs— Post inline diff of suggested fixes as PR review comments--post-branch— Create a branch with suggested fixes (mutually exclusive with --post-diffs)--split-changes— Split changes into multiple PRs by file/audit-type--split-by STRATEGY— Split strategy: file, audit, severity, size--dry-run— Preview without submitting-w, --wait— Wait for completion
Check the status of a review job.
bin/github-review status --id abc-123
bin/github-review status --repo owner/repo --pr 42
bin/github-review status --watchOptions:
-i, --id UUID— Check specific job by ID-r, --repo OWNER/REPO— Filter by repository-p, --pr NUMBER— Filter by PR number--json— Output as JSON-w, --watch— Poll for updates continuously
Cancel queued or processing review jobs.
bin/github-review cancel --id abc-123
bin/github-review cancel --allOptions:
-i, --id UUID— Cancel specific job by ID--all— Clear entire queue
Manage CLI configuration.
bin/github-review config --list
bin/github-review config --add-repo owner/repo
bin/github-review config --set-tokenOptions:
-l, --list— List current configuration--add-repo OWNER/REPO— Add repository to watch list--remove-repo OWNER/REPO— Remove repository--set-token— Set/update GitHub token--set-checkout-root PATH— Set checkout directory--reset— Reset to defaults
-h, --help— Show help message-V, --version— Show version-v, --verbose— Increase verbosity (stackable:-vvv)-q, --quiet— Suppress output--json— Output as JSON (for scripting)--debug— Show debug information-n, --non-interactive— Non-interactive mode (disables prompting)
Configuration is loaded from multiple sources in order of priority (highest last):
- System config —
/etc/github-review/config.php - User config —
~/.config/github-review/config.php - Project config —
./config/review-cli.php - Environment variables
- CLI arguments
Copy config/review-cli.php.dist to config/review-cli.php:
<?php
return [
'github' => [
'token' => 'ghp_xxxx', // Or use GITHUB_TOKEN env var
],
'redis' => [
'host' => '67.217.60.234',
'port' => 6379,
],
'checkout' => [
'root' => '/tmp/pr-checkouts',
'cleanup_after' => 3600,
],
'opencode' => [
'analyze_cmd' => 'opencode analyze --dir {dir} --output json',
'improve_cmd' => 'opencode improve --dir {dir} --file {file} --line {line} --output json',
],
'repositories' => [
'owner/repo1',
'owner/repo2',
],
'defaults' => [
'audit_types' => 'full',
'severity' => 'warning',
'post_summary' => true,
],
];Any config value can be overridden via environment variables:
| Variable | Description |
|---|---|
GITHUB_TOKEN |
GitHub personal access token |
REDIS_HOST |
Redis server host |
REDIS_PORT |
Redis server port |
CHECKOUT_ROOT |
Checkout directory for PRs |
OPENCODE_ANALYZE_CMD |
OpenCode analyze command template |
OPENCODE_IMPROVE_CMD |
OpenCode improve command template |
CLI_NON_INTERACTIVE |
Force non-interactive mode |
When running without a TTY (cron jobs, CI/CD pipelines) or with --non-interactive flag, the CLI:
- Requires all parameters via flags/arguments
- Fails on first error
- Uses JSON output for scripting
- Disables progress bars
When running interactively, the CLI:
- Shows colored tables
- Enables progress bars
- Offers interactive prompts for missing arguments
- Provides suggestions and defaults
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments |
| 3 | Not found |
| 4 | Validation error |
| 5 | Redis error |
| 6 | GitHub API error |
| 7 | Checkout error |
| 8 | Timeout |
class GithubWebhook
{
// Validates and parses the current HTTP request
public function ProcessRequest(): bool { ... }
// Validates X-Hub-Signature-256 header against secret
public function ValidateHubSignature(string $SecretKey): bool { ... }
// Returns the event type (e.g. 'push', 'issues')
public function GetEventType(): string { ... }
// Returns the decoded JSON payload as array
public function GetPayload(): array { ... }
// Returns 'org/repo' format repository name
public function GetFullRepositoryName(): string { ... }
}class GithubMessageBuilder
{
public function __construct(string $EventType, array $Payload) { ... }
// Builds chat message: ['avatar' => ..., 'alias?' => ..., 'text' => ...]
public function build(): array { ... }
public function getRepositoryName(): string { ... }
public function getUser(): string { ... }
}# Run all tests
composer test
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage/
# Run a specific test
./vendor/bin/phpunit --filter testPushSameAuthorOmitsByLineEach event type has a directory under tests/events/{event_name}/ with:
payload.json— Raw GitHub webhook payloadtype.txt— Event name (e.g.push)expected_text.txt— Expected output text
Available fixtures:
commit_comment,delete,dependabot_alert_createddiscussion_comment_created,discussion_createdgollum,gollum_createdissue_closed,issue_comment,issue_comment_many_new_lines,issues_closed,issues_openedmember,milestone,package,ping,ping_orgproject,publicpull_request_auto_merge_enabled,pull_request_closed_merged,pull_request_dependabotpull_request_merged,pull_request_opened,pull_request_review,pull_request_review_commentpush,push_branch_with_underscores,push_created,push_created_plus,push_multi_authorpush_multi_same_author,push_no_author,push_tagrelease,release_different_name,release_no_namerepository,repository_renamed,repository_transferredrepository_vulnerability_alert,repository_vulnerability_alert_dismiss,repository_vulnerability_alert_resolveworkflow_run_completed
# Static analysis
composer analyse
# Code style check
composer cs-check
# Auto-fix code style
composer cs-fix- PHP >=7.4
- predis/predis ^2.0 — Redis client
- vlucas/phpdotenv ^5.5 — Environment variable loading
- phpunit/phpunit ^9.0 — Testing (dev)
- phpstan/phpstan ^0.12.53 — Static analysis (dev)
- GitHub Webhooks Documentation
- [RocketChat Webhook Integration](https://docs.rocket.chat/guides/administration/administration/ integrations)
- Microsoft Teams Incoming Webhooks