Tüm yazılar
Altyapı 7 dk okuma

Tek Kişilik Ekip İçin CI: GitHub Actions ile Hafif Bir Boru Hattı

GitHub Actions ile CI: tek kişilik ekip için lint, test ve güvenlik kontrolü içeren hafif bir boru hattı; önbellek, matrix ve SHA sabitleme örnekleriyle.

#linux #automation #monitoring

Küçük projelerde GitHub Actions ile CI kurmayı erteleme gerekçesi hemen her zaman aynıdır: kodu tek kişi yazıyor, testleri de aynı kişi koşturuyor, üstüne bir de boru hattı kurmak fazladan iş gibi görünüyor. Otomasyon projelerinde gördüğüm şu: maliyetli olan CI'ın kendisi değil, eksikliği. Yerelde geçen bir test sunucuda kırıldığında sebep çoğu zaman sabitlenmemiş bir bağımlılık ya da yanlışlıkla commit edilmiş bir ayar dosyası oluyor ve bunu fark etmek, kurulumu yapmaktan çok daha uzun sürüyor.

Bu yazıda tek kişilik bir ekibin bakımını üstlenebileceği kadar hafif, ama gerçekten iş gören bir kurulum anlatıyorum: lint, test ve basit bir güvenlik kontrolü. Amaç kapsamlı bir dağıtım hattı değil; hatalı kodun ana dala girmesini engelleyen küçük bir kapı.

Örnekler Python projeleri üzerinden ilerliyor, ancak yapı dile bağlı değil. Adımların tamamı yerelde de koşturulabildiği için commit öncesi kontrol ile sunucu tarafı aynı komutları paylaşıyor; iki ortam birbirinden ayrışmıyor.

Tek kişilik ekipte CI neden gerekli?

Tek kişilik ekipte CI, ikinci bir göz görevi görür: kodu inceleyecek bir meslektaş yoksa, en azından her değişiklikte aynı kontrolleri tekrar eden bir makine olmalıdır. İnsan dikkati dalgalanır; testleri bazen koşturur, bazen küçük bir değişiklik diye atlarız. Boru hattı bu istisnaları ortadan kaldırır ve projeyi aylar sonra yeniden açtığınızda hangi durumda olduğunu size somut olarak söyler.

Hafif kurulumun kapsamını bilerek dar tutuyorum. Üç adım yeterli:

  • Lint: biçim hatalarını, kullanılmayan değişkenleri ve bariz mantık kusurlarını saniyeler içinde yakalar. Örneklerde ruff kullanıyorum.
  • Test: mevcut pytest takımı her push'ta koşar; testler yoksa boru hattı yalnızca lint ve güvenlik adımıyla başlar, testler sonradan eklenir.
  • Güvenlik: depoya sızmış sır desenlerini ve sürümü sabitlenmemiş bağımlılıkları arayan küçük bir betik.

Test tarafını daha önce otomasyon betiklerini pytest ile test etmek yazısında ele almıştım; buradaki boru hattı o testlerin her değişiklikte gerçekten koştuğunu garanti ediyor.

Minimum workflow: önbellek, matrix ve eşzamanlılık iptali

Minimum workflow tek bir YAML dosyasından oluşur ve üç pratik sorunu çözer: bağımlılıkları önbelleğe alarak her koşuda yeniden indirmeyi önler, matrix ile birden fazla Python sürümünde aynı adımları tekrar eder ve concurrency ayarıyla aynı dala art arda push geldiğinde eski koşuyu iptal eder. Dosya deponun .github/workflows/ci.yml yolunda durur.

name: ci

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  kontrol:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python: ["3.11", "3.12"]
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
        with:
          python-version: ${{ matrix.python }}
          cache: pip
      - run: pip install -r requirements.txt -r requirements-dev.txt
      - name: Lint
        run: ruff check .
      - name: Test
        run: python -m pytest -q
      - name: Guvenlik kontrolu
        run: python tools/sir_tara.py .

Dosyadaki birkaç satır özellikle önemli:

  • cache: pip satırı, requirements dosyalarının içeriğine göre anahtarlanan bir önbellek kurar; bağımlılıklar değişmediği sürece kurulum adımı önbellekten gelir.
  • fail-fast: false, bir Python sürümünde hata çıktığında diğerinin de sonuna kadar koşmasını sağlar; iki sürümde farklı davranan kodu ancak böyle görürsünüz.
  • cancel-in-progress: true, aynı dala üst üste push attığınızda eski koşuyu iptal eder; tek kişilik ekipte dakikalar içinde birkaç düzeltme commit'i atmak olağandır, kuyruğun şişmesi gerekmez.
  • permissions: contents: read, workflow'a verilen belirtecin yalnızca okuma yapabilmesini sağlar; ayrıntısı güvenlik bölümünde.

Süreyi kısa tutmak tek kişilik ekipte ayrı bir disiplin ister; birkaç dakikayı geçen bir hat, sonucu beklemeden bir sonraki işe geçmeye ve kırmızıyı fark etmemeye yol açar. Uyguladığım birkaç pratik var: lint adımını testlerden önce koymak (saniyeler içinde biter, bariz hatada testler hiç koşmaz), ağır entegrasyon testlerini pytest -m işaretiyle ayrı bir işe almak ve matrix'i gerçekten desteklenen sürümlerle sınırlamak. Önbellek çalışıyorsa kurulum adımı da saniyeler sürer; süre birden uzadıysa ilk şüpheli, requirements dosyasının değişip önbelleği geçersiz kılmasıdır.

Commit öncesi aynı kontrolleri yerelde koşturmak

Yerelde koşan kontrol betiği, boru hattının kırılmasını beklemek yerine hatayı commit'ten önce göstermenin en ucuz yoludur. Betik lint, test ve güvenlik adımlarını sırayla çalıştırır; ilk başarısız adımda aynı çıkış koduyla durur, böylece Git'in pre-commit kancasına bağlandığında hatalı commit hiç oluşmaz. Sunucudaki workflow ile aynı komutları kullandığı için iki ortam birbirinden uzaklaşmaz.

#!/usr/bin/env python3
"""Commit oncesi yerel kapi: lint, test, guvenlik. Ilk hatada durur."""
import subprocess
import sys

ADIMLAR = [
    ("lint", ["ruff", "check", "."]),
    ("test", [sys.executable, "-m", "pytest", "-q"]),
    ("guvenlik", [sys.executable, "tools/sir_tara.py", "."]),
]


def kos(ad, komut):
    print(f"[{ad}] {' '.join(komut)}")
    return subprocess.run(komut).returncode


def main():
    for ad, komut in ADIMLAR:
        kod = kos(ad, komut)
        if kod != 0:
            print(f"[{ad}] basarisiz (cikis kodu {kod}), commit durduruldu")
            sys.exit(kod)
    print("Tum kontroller gecti.")


if __name__ == "__main__":
    main()

Betiği tools/kontrol.py olarak kaydedip .git/hooks/pre-commit dosyasından çağırmak yeterli. Kanca, betiğin çıkış kodu sıfır değilse commit'i durdurur. Ben kancayı zorunlu tutmuyorum; acil bir düzeltmede --no-verify ile atlanabilir, ama sunucudaki workflow aynı kontrolü yine koşturacağı için atlamak hatayı yalnızca erteler.

Güvenlik katmanı: sır taraması, SHA sabitleme ve en az yetki

Güvenlik katmanı küçük bir projede iki parçadan oluşur: depoya sızan sırları ve sabitlenmemiş bağımlılıkları yakalayan bir betik, bir de workflow'un kendi saldırı yüzeyini daraltan birkaç satır ayar. İkisi de dakikalar içinde kurulur ve sonradan edinilmesi en zor olan alışkanlıkları en baştan devreye sokar.

#!/usr/bin/env python3
"""Depoda sabit sir deseni ve surumu sabitlenmemis bagimlilik arar."""
import re
import sys
from pathlib import Path

DESENLER = [
    ("github-token", re.compile(r"ghp_[A-Za-z0-9]{36}")),
    ("aws-anahtar", re.compile(r"AKIA[0-9A-Z]{16}")),
    ("ozel-anahtar", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
    ("atanmis-sir", re.compile(r'(?i)(password|secret|token)\s*=\s*"[^"]{8,}"')),
]
UZANTILAR = {".py", ".yml", ".yaml", ".json", ".toml", ".sh", ".ini"}
ATLA = {".git", ".venv", "node_modules", "__pycache__"}
GEREKSINIMLER = ("requirements.txt", "requirements-dev.txt")


def dosyalar(kok):
    for yol in Path(kok).rglob("*"):
        if any(parca in ATLA for parca in yol.parts):
            continue
        if yol.is_file() and (yol.suffix in UZANTILAR or yol.name.startswith(".env")):
            yield yol


def sir_tara(kok):
    bulgular = []
    for yol in dosyalar(kok):
        satirlar = yol.read_text(encoding="utf-8", errors="ignore").splitlines()
        for no, satir in enumerate(satirlar, 1):
            for ad, desen in DESENLER:
                if desen.search(satir):
                    bulgular.append(f"{yol}:{no} sir deseni: {ad}")
    return bulgular


def sabitleme_kontrol(dosya):
    bulgular = []
    yol = Path(dosya)
    if not yol.exists():
        return bulgular
    for no, satir in enumerate(yol.read_text(encoding="utf-8").splitlines(), 1):
        s = satir.strip()
        if not s or s.startswith("#") or s.startswith("-"):
            continue
        if "==" not in s:
            bulgular.append(f"{dosya}:{no} surum sabit degil: {s}")
    return bulgular


def main():
    kok = sys.argv[1] if len(sys.argv) > 1 else "."
    bulgular = sir_tara(kok)
    for dosya in GEREKSINIMLER:
        bulgular.extend(sabitleme_kontrol(dosya))
    for bulgu in bulgular:
        print(bulgu)
    if bulgular:
        print(f"{len(bulgular)} bulgu, kontrol basarisiz")
        sys.exit(1)
    print("Sir deseni yok, bagimliliklar sabit.")


if __name__ == "__main__":
    main()

Betik kapsamlı bir tarayıcı değil; birkaç bilinen deseni ve == ile sabitlenmemiş satırları yakalar. Değeri, her push'ta otomatik koşmasından gelir. Sırların doğru yeri ise depo değil, şifreli bir kasadır; bu geçişi .env dosyasından şifreli sır kasasına geçiş yazısında adım adım anlatmıştım.

Workflow'un kendi yüzeyini daraltan dört ayar:

  • Action'ları etikete değil commit SHA'sına sabitleyin. actions/checkout@v4 gibi bir etiket sonradan başka bir commit'e taşınabilir; yukarıdaki örnekte bu yüzden 40 karakterlik SHA kullanıp sürümü yorum olarak yanına yazdım. SHA'yı action'ın kendi sürüm sayfasından kopyalayın.
  • GITHUB_TOKEN için en az yetki. Üst düzeyde permissions: contents: read yazmak, workflow'daki herhangi bir adımın depoya yazmasını ya da PR açmasını baştan engeller; yazma gereken adım varsa yetkiyi yalnızca o iş için açın.
  • Sırlar depoya değil Secrets'a. API anahtarı ya da dağıtım belirteci gerekiyorsa depo ayarlarındaki Secrets alanına koyun ve workflow'da ${{ secrets.AD }} ile okuyun; loglara basmayın.
  • Fork PR'larında sır sızıntısı riski. Bir çatal deposundan gelen pull request, varsayılan olarak sırlara erişemez; bunu pull_request_target tetikleyicisiyle açmak, yabancı kodu sizin sırlarınızla koşturmak demektir. Küçük projede bu tetikleyiciden uzak durun.

Kırmızı koşuyu okumak ve hattı zorunlu kılmak

Kırmızıya dönen bir koşu, tek kişilik ekipte çoğu zaman görmezden gelinir; birkaç gün sonra hangi commit'in kırdığı bilinmez olur ve hat güvenilirliğini yitirir. İki alışkanlık bunu önler: kırmızı koşuyu aynı gün okumak ve ana dalı yalnızca yeşil koşuyla değişebilir hale getirmek. İkisi de yapılandırmadan çok disiplin işi, ancak GitHub tarafındaki birkaç ayar bu disiplini kolaylaştırır.

Kırmızı koşuyu okurken ilk baktığım şey hangi adımın durduğu: lint mi, test mi, güvenlik mi. Actions sekmesinde başarısız iş işaretlenir, adım açıldığında betiğin çıkış kodu ve son satırları görünür. Lint hatası neredeyse her zaman yerelde bir dakikada düzelir. Test hatası matrix'in yalnızca bir sürümünde çıkıyorsa sebep genellikle o sürüme özgü bir kütüphane davranışıdır; fail-fast: false ayarını bu yüzden açık tutuyorum. Güvenlik adımı kırmızıysa bulguyu susturmak yerine sırrı depodan çıkarıp yenilemek gerekir; tarama betiği yalnızca deseni görür, sırrın sızıp sızmadığına karar veremez.

Hattı zorunlu kılmak için ana dala branch koruma kuralı ekliyorum: depo ayarlarında main için durum kontrollerinin geçmesini şart koşan seçeneği açıp kontrol işini zorunlu olarak işaretlemek yeterli. Bundan sonra kırmızı koşusu olan bir değişiklik ana dala giremez; tek kişilik ekipte bile kendi kendinize koyduğunuz bu kapı, acele edilen bir akşam sonra düzeltirim diyerek geçilen commit'leri engeller. Doğrudan push yerine kısa ömürlü dallar ve pull request kullanmak da bu kuralla anlam kazanır.

Kontrolleri hangi sırayla eklemeli sorusunun cevabı bence net: önce lint ve mevcut testler, güvenlik betiği daha sonra. İlk günden her şeyi zorunlu yaparsanız hat sürekli kırmızı kalır ve onu görmezden gelme alışkanlığı erken yerleşir. İzlediğim sıra:

  • İlk gün: lint ve testler eklenir, branch koruma henüz kapalıdır; amaç hattın gerçekten çalıştığını görmek.
  • Hat bir süre yeşil kaldığında: branch koruma açılır, kontroller zorunlu olur.
  • Sonra: güvenlik betiği eklenir, önce mevcut bulgular temizlenir, ardından o da zorunlu hale gelir.

Son bir ekleme haftalık bağımlılık kontrolü: workflow'a schedule tetikleyicisiyle bir cron satırı ekleyip haftada bir, sabitlenmiş sürümlerin güncel olup olmadığını pip list --outdated ile raporlatıyorum. Bu koşu ana dalı kırmaz, yalnızca çıktı üretir; güncellemeyi ne zaman yapacağıma ben karar veririm. Push olmadan da çalıştığı için projeye aylarca dokunulmadığında bile bağımlılıkların eskidiğini haber verir.

Sonuç

Sonuç olarak tek kişilik bir ekip için CI'ın değeri, kurulumunun büyüklüğüyle değil sürekliliğiyle ölçülür. Lint, test ve basit bir güvenlik kontrolünden oluşan bu boru hattı bakım gerektirmeyecek kadar küçük, ama sabitlenmemiş bir bağımlılığı ya da yanlışlıkla commit edilmiş bir anahtarı ana dala sokmayacak kadar sıkıdır. Aynı betiğin yerelde de koşması, hatayı en ucuz olduğu yerde yakalar.

Benzer otomasyon ve altyapı işlerinde nasıl çalıştığımı ana sayfada özetledim; kendi projeniz için hafif bir CI kurulumu ya da mevcut hattınızın gözden geçirilmesi üzerine konuşmak isterseniz iletişim bölümünden yazabilirsiniz.