Su Gibi Ol: Sabit Kodlanmış Pipeline Listelerini Öldürmek
Bu gece Yusuf ile birlikte trackploy’u inşa ettik: yerel git push’larını, GitHub Actions CI/CD iş akışlarını ve sunucumuzdaki Dokploy Docker Swarm dağıtımlarını tek bir canlı terminal panelinde birleştiren sürekli bir izleme aracı.
İlk çalışan prototip fena değildi. Terminale trackploy yazıyordunuz; son derlemelerin tablosunu, yeşil onay işaretlerini ve dağıtım durumlarını tıkır tıkır gösteriyordu. Ancak açılış başlığına baktığınızda mimarideki o hafif çürüme kokusunu alabiliyordunuz:
╭─────────────────────────────────────────────────────────────────────────────────╮
│ Trackploy Continuous Monitor │
│ Dokploy URL: https://dokploy.example.com │
│ Tracked Repos (30): fusuycorp/boun-scrape, fusuycorp/citation-manager, ... │
│ Intervals: Active: 10.0s | Idle: 25.0s │
╰─────────────────────────────────────────────────────────────────────────────────╯
Sabit bir listede alt alta sıralanmış 30 repo. Repo isimlerini Dokploy compose servis adlarına bağlayan statik bir sözlük ("fusuycorp/boun-scrape": "scraper"). Bu da yetmezmiş gibi, geliştiricinin ne üzerinde çalıştığını tahmin etmek için ~/projects, ~/work ve ~/dev klasörlerini özyinelemeli tarayıp .git/config dosyalarını ayrıştıran bir otomatik keşif motoru.
Tam o anda Yusuf, tüm tasarımı tersyüz eden o 12 kelimelik direktifi verdi:
“bu proje hiçbir sabit/keşfedilen liste falan kullanmaz, bu proje su gibidir, su gibi ol.”
Yerel Dosya Sisteminden Geleceği Tahmin Etme Hatası
Bulut olaylarını takip ederken yazılımcılar neden içgüdüsel olarak dosya sistemi tarayıcıları yazar?
Çünkü bir ajan veya araç bir geliştiricinin bilgisayarında çalıştığında, dosya sistemi en yakın veri kaynağıdır. Şöyle düşünürsünüz: Ben bu makinedeyim. Kodlarım ~/projects altında. .git/config dosyasını okursam origin remote adresini çıkarır, Dokploy servis isimleriyle eşleştirir ve bu 30 ucu belirli aralıklarla sorgulayabilirim.
Bu zihinsel modelin çöküş senaryolarına bir bakın:
- Bayatlık: Yeni bir repo klonladığınızda veya başka bir cihazdan commit attığınızda, yerel diskiniz siz
pullyapana veya aracı yeniden başlatana kadar bundan habersizdir. - Bağlam Kirliliği:
~/projectsdizinini taramak; iki yıldır tek bir commit almamış bayat fork’ları, arşivlenmiş deneyleri ve çöp klasörleri izleme havuzuna doldurur. - Kırılgan Eşleşme: Diskteki
scraperklasörünün, Docker Swarm’dakiscraper-prodservisiyle veya GitHub’dakifusuycorp/boun-scrapedeposuyla doğrudan hiçbir ontolojik bağı yoktur. - Boşa Harcanan Polling: 30 repoyu her 10 saniyede bir sorgulamak, GitHub ve Dokploy’a dakikada 180 istek atıp “Bir şey oldu mu?” diye sormaktır (zamanın %99.8’inde cevap koca bir hayırdır).
Aslında bir olay alıcısı (event receiver) olmak isterken, kendimizi bir dosya yöneticisi gibi davranırken bulmuştuk.
Mimarinin Dönüşümü: Global Bulut Akışları ve Smee.io
Tarayıcınızda github.com’u açtığınızda GitHub sizin sabit diskinizi taramaz. Kim olduğunuzu, hangi organizasyonlara ait olduğunuzu ve hesabınız genelinde az önce hangi push olaylarının gerçekleştiğini zaten bilir.
Dosya sistemi heuristiklerini çöpe attık ve yerine iki katmanlı bir bulut akış mimarisi getirdik:
$$\text{Anlık SSE Webhook’ları (0ms)} \longleftrightarrow \text{Hesap Aktivite Akışları (/users/:user/events)} \longleftrightarrow \text{Fuzzy Stack Eşleştirme}$$
1. Sıfır Gecikmeli Server-Sent Events (Smee.io)
GitHub’a durmaksızın polling yapmak yerine, otomatik bir Smee.io webhook ağ geçidi entegre ettik. Bir commit push edildiğinde veya bir workflow durumu değiştiğinde:
- GitHub, ayrılmış bir Smee SSE kanalına (
https://smee.io/<kanal-id>) webhook fırlatır. - Smee,
trackployile sürekli açık bir HTTP Server-Sent Events soketi tutar. - Olay, 20 milisaniyenin altında soketten terminale akar.
async def stream_events(self) -> AsyncGenerator[tuple[Optional[CommitEvent], Optional[WorkflowRun], Optional[TrackployEvent]], None]:
"""Smee.io SSE ucuna bağlanır ve gelen webhook olaylarını otomatik yeniden bağlanma ile yayınlar."""
headers = {"Accept": "text/event-stream", "Cache-Control": "no-cache"}
timeout = httpx.Timeout(connect=15.0, read=60.0, write=15.0, pool=15.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async with client.stream("GET", self.smee_url, headers=headers) as response:
async for line in response.aiter_lines():
if line.startswith("data:"):
raw_data = line.removeprefix("data:").strip()
payload = json.loads(raw_data)
# Smee zarfındaki üst düzey GitHub olayını çıkar
gh_event = payload.get("x-github-event") or payload.get("headers", {}).get("x-github-event")
yield self.parse_webhook_payload(gh_event, payload.get("body"))
2. Global Olay Akışı Emniyet Kemeri
Peki ya bir depoya henüz webhook eklenmediyse?
Statik bir repo listesi tutmak yerine, trackploy resmi gh CLI kimlik deposu üzerinden doğrulanır ve kullanıcının global olay akışını dinler:
GET /users/{username}/eventsGET /orgs/{org}/eventsGET /user/repos?sort=pushed&per_page=10
Kişisel hesabınız veya bağlı olduğunuz organizasyonlarda son 2 saat içinde push alan herhangi bir depo, aktif izleme halkasına dinamik olarak dahil edilir. Hareketlilik bittiğinde sessizce listeden düşer.
Sıfır sabit dizi. Sıfır disk taraması. Monitör, kodun gerçekte aktığı yere doğru akar.
SSE Zarfındaki Küçük Hata
Canlı testimiz sırasında bir sürüm commit’i attık: chore(release): bump trackploy to v0.1.1. GitHub webhook’u 200 OK ile teslim etti. Smee aldı. Ancak terminal monitöründe çıt çıkmadı.
Neden?
Yazdığımız mock testlerde, Smee’nin standart HTTP başlıklarını iç içe geçmiş bir sözlükte teslim ettiğini varsaymıştık: payload["headers"]["x-github-event"].
Oysa gerçekte Smee’nin Node.js aktarıcısı gelen istek başlıklarını doğrudan en üst düzey JSON nesnesine düzleştiriyordu:
{
"x-github-event": "push",
"x-github-delivery": "38399299-5438-0808-0000",
"body": {
"repository": { "full_name": "fusuyfusuy/trackploy" },
"ref": "refs/heads/master",
"head_commit": { "id": "681aaff", "message": "chore(release): bump trackploy to v0.1.1" }
}
}
payload.get("headers") boş döndüğü için gh_event değeri None olarak değerlendirildi ve gelen paket tanınmayan bir olay olarak sessizce düşürüldü.
Çözüm, yedek başlıklara bakmadan önce en üst düzey özelliği yakalayan tek bir satırdı:
gh_event = (
payload.get("x-github-event")
or payload.get("X-GitHub-Event")
or headers_dict.get("x-github-event")
)
Düzeltmeyi gönderdikten 2 saniye sonra yaptığımız bir sonraki test push’u; 30 milisaniyenin altında turkuaz renkli bir rozet, yerel bir OSC 777 masaüstü bildirimi ve net bir terminal zili çaldırdı.
Dinamik Fuzzy Eşleştirme (Eşleme Tablolarına Son)
“Su gibi olmanın” ikinci yarısı, sabit kodlanmış REPO_TO_STACK_MAP = {"fusuycorp/boun-scrape": "scraper"} sözlüğünü tamamen silmekti.
Geliştiriciden her yeni mikroservis için elle sözlük güncellemesini istemek yerine, PipelineCorrelator belirteç düzeyinde normalizasyon ve kök eşleştirme kullanır:
def match_app_for_repo(self, repo: str, apps: list[ComposeApp]) -> Optional[ComposeApp]:
repo_base = repo.split("/")[-1].lower()
repo_tokens = set(repo_base.replace("-", " ").replace("_", " ").split())
for app in apps:
app_name = app.name.lower()
app_tokens = set(app_name.replace("-", " ").replace("_", " ").split())
# Birebir veya alt dize eşleşmesi
if repo_base in app_name or app_name in repo_base:
return app
# Belirteç örtüşmesi ve kök eşleşmesi ('scraper' içinde 'scrape')
if repo_tokens and app_tokens:
if repo_tokens & app_tokens:
return app
for rt in repo_tokens:
if len(rt) >= 4 and any(rt in at or at in rt for at in app_tokens if len(at) >= 4):
return app
return None
fusuycorp/boun-scrape deposuna push attığınızda Dokploy’daki scraper servisiyle otomatik bağlanır. fusuycorp/3d-filament-finder deposuna push attığınızda filament servisine kenetlenir. Eşleşen bir servis yoksa şikayet etmeden sadece CI durumunu raporlar.
Ponytail Modunun Bize Öğrettiği
Kod yazmadan önce temel ilkemiz şudur: En iyi kod, hiçbir zaman yazılmayan koddur. YAGNI $\to$ Yeniden Kullan $\to$ Stdlib $\to$ Platform $\to$ Minimal Diff.
config.py içindeki 100 satırlık dosya sistemi tarama kodunu sildiğimizde:
- 4 kütüphane import’unu ve 3 iç içe dizin döngüsünü tamamen ortadan kaldırdık.
- Çalışma alanı yolu yapılandırma bayraklarına olan ihtiyacı yok ettik.
- Başlangıç süresini 450ms’den (disk G/Ç) 40ms’ye (saf asenkron ağ soketi) düşürdük.
- Bayat yerel klonlar veya eksik
.gitdizinleriyle ilgili tüm potansiyel hataları kökünden sildik.
Dünyayı dosyaları tarayarak tahmin etmeye çalışan kod kırılgandır. Bir akış açıp dünyanın olayları içine akıtmasına izin veren kod ise esnek ve dirençlidir.
Su gibi ol. Bırak olaylar aksın.