GitHub Actions ile Python CI Pipeline
Selamlar, bu yazımda Python projelerinde GitHub Actions ile sağlam bir CI pipeline'ı nasıl kurduğumu adım adım anlatacağım. Konu basit gibi duruyor ama detaylar var: matrix build, pip cache, ruff/mypy/pytest sıralaması, coverage upload, yeniden kullanılabilir workflow ve branch protection. Hepsini bir arada görünce iş netleşiyor. Hadi başlayalım.
CI'ın asıl işi bence çok basit: hatayı production'a düşmeden yakalamak. Lokalde her şey yeşilken merge'lenen bir PR prod'da patlıyorsa, eksik olan parça genellikle CI'da bir kontroldür. O yüzden minimum şu üçü her zaman olmalı: test, lint, type check.
Temel workflow iskeleti
Önce sade bir .github/workflows/ci.yml yazalım. Burada amacımız; her push ve PR'da Python'un birden fazla versiyonunda testleri çalıştırmak.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: requirements*.txt
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: pytest -q
Burada dikkat çekmek istediğim iki şey var. Birincisi matrix.python-version: kütüphane geliştiriyorsanız 3 versiyon test etmek pahalı değil ama ileride ortaya çıkacak uyumsuzlukları erkenden yakalıyor. İkincisi cache: 'pip': setup-python@v5 bunu kendi başına yapıyor, ekstra bir actions/cache adımına gerek yok. Yani 'pip cache nasıl kurulur?' diye uzun uzun yazmak zorunda değilsiniz, tek satır.
Lint ve type check
Test'in tek başına bir anlamı yok. Ben genelde lint'i ve type check'i ayrı job'lar olarak koşturuyorum çünkü hızlılar ve test job'ı uzun sürdüğünde önce bu ikisinden geri dönüş alıyorum.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install ruff mypy
- run: ruff check .
- run: ruff format --check .
- run: mypy src/
Ruff'un güzel yanı; flake8, isort, pyupgrade gibi araçların yaptığı işin büyük kısmını tek binary altında, üstelik çok hızlı yapıyor. Bence yeni başlayan bir proje için varsayılan tercih artık bu. mypy'ı da src/ altına yönlendirip testleri dışarıda tuttum, çünkü test kodunda strict mode bazen gereksiz gürültü çıkarıyor.
Coverage ve eşik
Test geçti diye işin bitmiyor; kapsamı da görmek lazım. pytest-cov ile XML çıktı üretip Codecov'a gönderiyoruz. Eşiği --cov-fail-under ile build tarafında zorluyoruz, böylece coverage düştüğünde PR kırmızı oluyor.
- run: pip install pytest pytest-cov
- run: pytest --cov=src --cov-report=xml --cov-fail-under=80
- if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
Buradaki if: matrix.python-version == '3.12' koşulu önemli: matrix'te 3 versiyon var ama Codecov'a tek seferde göndermek yetiyor. Yoksa aynı rapor üç kere yükleniyor ve dashboard kafa karıştırıcı oluyor.
Yeniden kullanılabilir workflow
Birden fazla repo'nuz varsa aynı CI'ı her birine kopyala-yapıştır yapmak işkence. Bunun yerine workflow_call ile tek bir 'çekirdek' workflow yazıp diğer repolardan çağırabilirsiniz.
on:
workflow_call:
inputs:
python-version:
type: string
default: '3.12'
Çağıran tarafta ise tek satır:
jobs:
ci:
uses: my-org/.github/.github/workflows/python-ci.yml@main
with:
python-version: '3.12'
Tecrübeyle sabit; üç-beş projeden sonra bu yapı kendini fazlasıyla amorti ediyor.
Branch protection
CI'ın gerçekten işe yaraması için kontrolün main'e doğrudan push'lanmasını engellemesi lazım. GitHub'ın Settings > Branches ekranından main için 'Require status checks to pass before merging' işaretleyin ve yukarıdaki lint, test, type-check job adlarını seçin. PR yeşil olmadan merge düğmesi tıklanamaz hâle gelir; ben buna 'CI'ın dişlerinin çıkması' diyorum.
Sık karşılaşılan hatalar
- Cache anahtarını lockfile'a bağlamamak:
requirements*.txtdeğişmediği sürece cache hit alıyorsunuz; ama lock dosyanız varsa onu dacache-dependency-pathlistesine ekleyin, yoksa eski paketler takılı kalıyor. - Tek bir job'da her şeyi koşturmak: lint 5 saniyede biten bir şey, test 4 dakika sürebilir. Aynı job'a koyarsanız ufacık bir format hatası için 4 dakika bekliyorsunuz. Job'ları ayırın, paralel çalışsınlar.
fail-fast: falsekoymamak: matrix'te 3.11 patladığı an diğer versiyonlar iptal oluyor. Versiyona özgü bir bug varsa sadece o satırı görmek istersiniz;strategy.fail-fast: falseekleyin.- Secret'ı log'a sızdırmak:
echo "$TOKEN"gibi bir şey yazmayın, GitHub maskeliyor ama her zaman değil. Sırrı doğrudan action'a parametre olarak verin. - Branch protection'ı unutmak: CI'ı kurup ayarlardan zorunlu kılmadıysanız, kimse aldırmıyor. Pipeline'ın anlamı burada bitiyor.
Kapanış
Bu yazıda Python için GitHub Actions üzerinde minimum kabul edilebilir bir CI'ı kurduk: matrix build, pip cache, ruff, mypy, pytest, coverage ve yeniden kullanılabilir workflow. Bence başlangıç için bu yeter; sonradan security scan, container build, release otomasyonu gibi parçalar zaten doğal olarak gelir. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
