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_digest ile 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-Delivery header’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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir