GitHub Webhooks ile Otomatik Bildirim ve Entegrasyon Sistemleri Kurma
Ekibinizde birileri bir PR merge ettigi anda Slack’te bildirim geliyor, deployment otomatik basliyor, Jira ticket’i kapaniyor… Bunlarin hepsini GitHub Webhooks ile kurabilirsiniz. Bu yazi, webhook sistemlerini sifirdan kurmak ve gercek dunya entegrasyonlari yazmak icin ihtiyaciniz olan her seyi kapsayacak.
GitHub Webhooks Nedir ve Neden Kullanmalisiniz
Webhook, temel olarak “bir sey olunca su URL’e HTTP POST at” mantigi uzerine calisan bir mekanizmadir. GitHub tarafinda bir olay gerceklestigi anda, sizin belirlediginiz bir endpoint’e JSON payload gonderir. Siz de o endpoint’te bu veriyi alip istedigi seyi yapabilirsiniz.
Pull request acildi mi? Bildir. Yeni bir tag push edildi mi? Build baslat. Issue kapandi mi? Musteiye mail gonder. Webhook’lar bu senaryolari gercek zamanli ve guvenilir sekilde calistirmanizi saglar.
Polling alternatifine kiyasla webhook’lar cok daha verimlidir. Her 30 saniyede bir GitHub API’ini sorgulamak yerine, GitHub olaylari size push eder. Bu hem rate limit sorununu ortadan kaldirir hem de anlık tepki surenizi dramatik sekilde dusurur.
Webhook Endpoint’i Kurma
Oncelikle webhook isteklerini alacak bir HTTP sunucusuna ihtiyaciniz var. Python ile basit bir Flask uygulamasi yazalim:
pip install flask gunicorn
#!/usr/bin/env python3
# webhook_server.py
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('GITHUB_WEBHOOK_SECRET', 'supersecret')
def verify_signature(payload_body, signature_header):
"""GitHub imzasini dogrula"""
if not signature_header:
abort(403, 'X-Hub-Signature-256 header eksik')
hash_object = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
msg=payload_body,
digestmod=hashlib.sha256
)
expected_signature = 'sha256=' + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
abort(403, 'Imza dogrulamasi basarisiz')
@app.route('/webhook', methods=['POST'])
def github_webhook():
# Imzayi dogrula
verify_signature(
request.get_data(),
request.headers.get('X-Hub-Signature-256')
)
event_type = request.headers.get('X-GitHub-Event')
payload = request.get_json()
print(f"Event alindi: {event_type}")
print(f"Repository: {payload.get('repository', {}).get('full_name')}")
return {'status': 'ok', 'event': event_type}, 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Bu sunucuyu production ortaminda guvenli sekilde calistirmak icin systemd service dosyasi olusturalim:
# /etc/systemd/system/webhook-server.service
[Unit]
Description=GitHub Webhook Server
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/webhook-server
Environment=GITHUB_WEBHOOK_SECRET=your_secret_here
Environment=SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx
ExecStart=/opt/webhook-server/venv/bin/gunicorn
--workers 2
--bind 0.0.0.0:5000
--access-logfile /var/log/webhook/access.log
--error-logfile /var/log/webhook/error.log
webhook_server:app
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# Servisi etkinlestir ve baslat
sudo systemctl daemon-reload
sudo systemctl enable webhook-server
sudo systemctl start webhook-server
sudo systemctl status webhook-server
GitHub Tarafinda Webhook Tanimlama
Repository ayarlarindan webhook ekleyebilirsiniz ama bunu API uzerinden de yapabilirsiniz, ki bu daha tekrarlanabilir bir yontemdir:
# GitHub CLI ile webhook olustur
gh api
--method POST
-H "Accept: application/vnd.github+json"
/repos/OWNER/REPO/hooks
-f name='web'
-f active=true
-F 'config[url]="https://webhook.sirketiniz.com/webhook"'
-F 'config[content_type]="json"'
-F 'config[secret]="your_secret_here"'
-F 'config[insecure_ssl]="0"'
-F 'events[]=push'
-F 'events[]=pull_request'
-F 'events[]=issues'
-F 'events[]=release'
Hangi event’leri dinleyeceginizi secmek performans acisindan onemlidir. Her event’i dinlemek gereksiz trafik yaratir. Yukaridaki ornekte sadece ihtiyacimiz olan dort event tipini sectik.
Slack Entegrasyonu
En yaygin kullanim senaryosu Slack bildirimleridir. Bir PR merge edildiginde veya yeni bir release ciktiginda ekibe bildirim atmak isteyebilirsiniz:
# slack_notifier.py
import requests
import os
SLACK_WEBHOOK_URL = os.environ.get('SLACK_WEBHOOK_URL')
def notify_pr_merged(payload):
"""Pull request merge edildiginde Slack bildirimi gonder"""
pr = payload.get('pull_request', {})
repo = payload.get('repository', {})
if payload.get('action') != 'closed' or not pr.get('merged'):
return
message = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"PR Merge Edildi: {repo['name']}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*PR:*n<{pr['html_url']}|{pr['title']}>"
},
{
"type": "mrkdwn",
"text": f"*Merge Eden:*n{pr['merged_by']['login']}"
},
{
"type": "mrkdwn",
"text": f"*Branch:*n`{pr['head']['ref']}` -> `{pr['base']['ref']}`"
},
{
"type": "mrkdwn",
"text": f"*Degisiklikler:*n+{pr['additions']} / -{pr['deletions']} satirlik degisiklik"
}
]
}
]
}
response = requests.post(SLACK_WEBHOOK_URL, json=message)
if response.status_code != 200:
print(f"Slack bildirimi gonderilemedi: {response.text}")
else:
print(f"Slack bildirimi gonderildi: PR #{pr['number']}")
def notify_new_release(payload):
"""Yeni release ciktiginda Slack bildirimi gonder"""
release = payload.get('release', {})
repo = payload.get('repository', {})
if payload.get('action') != 'published':
return
message = {
"text": f":rocket: *{repo['name']}* icin yeni release yayinda!",
"attachments": [
{
"color": "#36a64f",
"fields": [
{
"title": "Versiyon",
"value": release.get('tag_name', 'Bilinmiyor'),
"short": True
},
{
"title": "Release Notu",
"value": release.get('body', 'Aciklama yok')[:500],
"short": False
}
]
}
]
}
requests.post(SLACK_WEBHOOK_URL, json=message)
Otomatik Deployment Senaryosu
Gercek dunya senaryosu: main branch’ine her push yapildiginda staging sunucunuza otomatik deploy etmek istiyorsunuz. Bu tam anlamiyla CI/CD’nin en temel yapisi:
# deploy_handler.py
import subprocess
import threading
import logging
import os
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/log/webhook/deploy.log'),
logging.StreamHandler()
]
)
DEPLOY_BRANCH = os.environ.get('DEPLOY_BRANCH', 'main')
DEPLOY_SCRIPT = os.environ.get('DEPLOY_SCRIPT', '/opt/scripts/deploy.sh')
def handle_push_event(payload):
"""Push event'i isle ve gerekirse deploy baslat"""
ref = payload.get('ref', '')
repo = payload.get('repository', {})
pusher = payload.get('pusher', {})
commits = payload.get('commits', [])
# Sadece hedef branch'e push'lari isle
if ref != f'refs/heads/{DEPLOY_BRANCH}':
logging.info(f"Deploy atla: {ref} hedef branch degil")
return
commit_count = len(commits)
latest_commit = commits[-1] if commits else {}
logging.info(f"Deploy tetiklendi: {repo['full_name']}")
logging.info(f"Push yapan: {pusher.get('name')}")
logging.info(f"Commit sayisi: {commit_count}")
logging.info(f"Son commit: {latest_commit.get('message', 'N/A')[:80]}")
# Deploy'u ayri thread'de calistir, webhook response'u bekletme
deploy_thread = threading.Thread(
target=run_deployment,
args=(repo['full_name'], latest_commit.get('id', 'HEAD')[:8])
)
deploy_thread.daemon = True
deploy_thread.start()
def run_deployment(repo_name, commit_hash):
"""Deployment scriptini calistir"""
try:
logging.info(f"Deployment basliyor: {repo_name} @ {commit_hash}")
result = subprocess.run(
[DEPLOY_SCRIPT, repo_name, commit_hash],
capture_output=True,
text=True,
timeout=300 # 5 dakika timeout
)
if result.returncode == 0:
logging.info(f"Deployment basarili: {commit_hash}")
logging.info(result.stdout)
else:
logging.error(f"Deployment basarisiz: {commit_hash}")
logging.error(result.stderr)
# Burada hata bildirimi de atabilirsiniz
except subprocess.TimeoutExpired:
logging.error(f"Deployment zaman asimi: {repo_name}")
except Exception as e:
logging.error(f"Deployment hatasi: {str(e)}")
Deploy scriptinin kendisi de basit tutulabilir:
#!/bin/bash
# /opt/scripts/deploy.sh
set -euo pipefail
REPO_NAME=$1
COMMIT_HASH=$2
APP_DIR="/var/www/myapp"
LOG_FILE="/var/log/webhook/deploy-${COMMIT_HASH}.log"
echo "=== Deployment Basliyor: ${COMMIT_HASH} ===" | tee -a $LOG_FILE
# Uygulamayi guncelle
cd $APP_DIR
git fetch origin main 2>&1 | tee -a $LOG_FILE
git reset --hard origin/main 2>&1 | tee -a $LOG_FILE
# Bagimliliklari guncelle
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt --quiet 2>&1 | tee -a $LOG_FILE
fi
if [ -f "package.json" ]; then
npm ci --silent 2>&1 | tee -a $LOG_FILE
npm run build 2>&1 | tee -a $LOG_FILE
fi
# Servisi yeniden baslat
sudo systemctl reload myapp 2>&1 | tee -a $LOG_FILE
# Health check
sleep 3
if curl -sf http://localhost:8000/health > /dev/null; then
echo "=== Health check basarili ===" | tee -a $LOG_FILE
exit 0
else
echo "=== Health check basarisiz! Rollback yapiliyor ===" | tee -a $LOG_FILE
git reset --hard HEAD~1
sudo systemctl reload myapp
exit 1
fi
Issue ve PR Event’lerini Jira ile Senkronize Etme
Bir diger yaygin senaryo: GitHub issue’larini veya PR’lari Jira ticket’lariyla eslestirmek. Branch adi veya PR basligindaki ticket numarasini yakalayip Jira’yi guncellemek:
# jira_integration.py
import re
import requests
import os
from base64 import b64encode
JIRA_BASE_URL = os.environ.get('JIRA_BASE_URL') # https://sirketiniz.atlassian.net
JIRA_USER = os.environ.get('JIRA_USER')
JIRA_TOKEN = os.environ.get('JIRA_API_TOKEN')
def get_jira_headers():
credentials = b64encode(f"{JIRA_USER}:{JIRA_TOKEN}".encode()).decode()
return {
"Authorization": f"Basic {credentials}",
"Content-Type": "application/json"
}
def extract_ticket_numbers(text):
"""Metinden PROJ-123 formatinda ticket numaralarini cikart"""
pattern = r'b[A-Z]{2,10}-d+b'
return re.findall(pattern, text or '')
def update_jira_on_pr_merge(payload):
"""PR merge edildiginde ilgili Jira ticket'larini In Review'dan Done'a gec"""
pr = payload.get('pull_request', {})
if payload.get('action') != 'closed' or not pr.get('merged'):
return
# PR basliginda ve branch adinda ticket ara
search_texts = [
pr.get('title', ''),
pr.get('head', {}).get('ref', ''),
pr.get('body', '')
]
ticket_numbers = []
for text in search_texts:
ticket_numbers.extend(extract_ticket_numbers(text))
ticket_numbers = list(set(ticket_numbers)) # Tekrarlari kaldir
if not ticket_numbers:
print("PR'da Jira ticket numarasi bulunamadi")
return
for ticket in ticket_numbers:
transition_ticket(ticket, 'Done')
add_comment_to_ticket(
ticket,
f"Bu ticket PR merge edildi: [{pr['title']}]({pr['html_url']})"
)
def transition_ticket(ticket_key, target_status):
"""Jira ticket'ini belirtilen duruma gec"""
transitions_url = f"{JIRA_BASE_URL}/rest/api/3/issue/{ticket_key}/transitions"
# Mevcut transitionlari al
response = requests.get(transitions_url, headers=get_jira_headers())
if response.status_code != 200:
print(f"Transition listesi alinmadi: {ticket_key}")
return
transitions = response.json().get('transitions', [])
target_transition = next(
(t for t in transitions if t['name'].lower() == target_status.lower()),
None
)
if not target_transition:
print(f"'{target_status}' transition bulunamadi: {ticket_key}")
return
# Transition uygula
data = {"transition": {"id": target_transition['id']}}
result = requests.post(transitions_url, json=data, headers=get_jira_headers())
if result.status_code == 204:
print(f"Ticket guncellendi: {ticket_key} -> {target_status}")
else:
print(f"Ticket guncellenemedi: {ticket_key}, Status: {result.status_code}")
def add_comment_to_ticket(ticket_key, comment_text):
"""Jira ticket'ina yorum ekle"""
url = f"{JIRA_BASE_URL}/rest/api/3/issue/{ticket_key}/comment"
data = {
"body": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [{"type": "text", "text": comment_text}]
}
]
}
}
requests.post(url, json=data, headers=get_jira_headers())
Ana Webhook Handler’i Birlestirme
Tum bu parcalari bir araya getirelim. Event router mantigi:
# Ana webhook_server.py guncellenmis hali
from flask import Flask, request, abort
from slack_notifier import notify_pr_merged, notify_new_release
from deploy_handler import handle_push_event
from jira_integration import update_jira_on_pr_merge
import hmac, hashlib, os
app = Flask(__name__)
EVENT_HANDLERS = {
'push': [handle_push_event],
'pull_request': [notify_pr_merged, update_jira_on_pr_merge],
'release': [notify_new_release],
}
@app.route('/webhook', methods=['POST'])
def github_webhook():
verify_signature(request.get_data(), request.headers.get('X-Hub-Signature-256'))
event_type = request.headers.get('X-GitHub-Event')
payload = request.get_json()
handlers = EVENT_HANDLERS.get(event_type, [])
for handler in handlers:
try:
handler(payload)
except Exception as e:
app.logger.error(f"Handler hatasi ({handler.__name__}): {str(e)}")
return {'status': 'ok', 'event': event_type, 'handlers_run': len(handlers)}, 200
Guvenlik ve Production Onerileri
Webhook sisteminizi production’a tasirken dikkat etmeniz gerekenler:
- Secret dogrulamasi zorunlu: Her istekte HMAC imzasini
hmac.compare_digestile kiyaslayin, string karsilastirmasi yapmayin (timing attack riski) - HTTPS kullanin: Webhook URL’iniz mutlaka HTTPS olmali, aksi halde payload’lar arasinda ki veri ifsa olabilir
- Rate limiting ekleyin: Nginx veya uygulama katmaninda rate limiting koyun, kotu niyetli isteklere karsi
- Idempotent handler yazin: Ayni event iki kez gelebilir, handler’lariniz buna hazir olmali. GitHub en az bir kez garanti eder
- Timeout degerini ayarlayin: GitHub, 10 saniye icinde cevap alamazsa timeout sayar ve yeniden dener. Uzun surecek islemleri mutlaka ayri thread’e veya kuyruga alin
- Delivery ID’yi kaydedin:
X-GitHub-Deliveryheader’i her istege ozgu UUID icerir, loglara ekleyin - Webhook event’lerini veritabanina loglayin: Hangi event’in ne zaman geldigini, hangi handler’in calistigini ve sonucunu kayit altinda tutun
Nginx ile webhook endpoint’ini expose etmek icin minimal konfigurasyon:
# /etc/nginx/sites-available/webhook
server {
listen 443 ssl;
server_name webhook.sirketiniz.com;
ssl_certificate /etc/letsencrypt/live/webhook.sirketiniz.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/webhook.sirketiniz.com/privkey.pem;
location /webhook {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Sadece GitHub IP bloklarindan gelen isteklere izin ver
# GitHub meta API'den guncel listeyi alabilirsiniz
allow 192.30.252.0/22;
allow 185.199.108.0/22;
allow 140.82.112.0/20;
deny all;
limit_req zone=webhook burst=20 nodelay;
}
}
Sonuc
GitHub Webhooks, DevOps workflow’unuzu otomatize etmenin en etkili yollarindan biridir. Bu yazidat anlattik:
- Flask ile imzalamali webhook endpoint’i kurmayi
- Sistemi guvenli sekilde production’da calistirmayi
- Slack bildirim entegrasyonu yazmay
- Otomatik deployment pipeline’i kurmayi
- Jira entegrasyonuyla ticket durumlarini guncellemeyi
- Tum handler’lari event router mimarisiyle birlestirmeyi
Burada anlatilanlar baslangic noktalaridir. Gercek ortamda bu sistemi Redis/Celery ile kuyruga almak, her handler’in sonucunu veritabanina kaydetmek ve alerting eklemek isteyeceksiniz. Ama temel mantik ayni: GitHub bir seylerin oldugunu size soyler, siz de ona gore hareket edersiniz.
Basit bir Slack bildirimi ile baslayin. Sistemi anlayin. Sonra ustune deployment, Jira, monitoring entegrasyonlarini ekleyin. Her sey adim adim.
