Neden kalite otomasyonu?
Ilgaz Bank'ın kredi skorlama paketini üç kişi geliştiriyor. Biri sekmeyle, diğeri boşlukla girinti yapıyor; birinin bıraktığı print çağrısı üretim logunu dolduruyor; None dönebilen bir fonksiyon başka bir yerde .upper() ile çağrılıyor ve gece yarısı çöküyor. Bunların hiçbiri "dikkat etmekle" çözülmez; insan dikkati tükenir, araçlar tükenmez. Kalite otomasyonu (quality automation), bu kontrolleri makineye devretmek ve her commit'te, her pull request'te aynı standardı uygulamaktır.
Üretime hazır bir Python projesinde kalite zinciri dört halkadan oluşur: biçimlendirici (formatter) kodu tek stile getirir, linter şüpheli kalıpları yakalar, tip denetleyici (type checker) sözleşmeleri doğrular, test koşucusu davranışı kanıtlar. pre-commit bu zinciri yerelde, CI (continuous integration, sürekli tümleştirme) sunucuda çalıştırır.
Biçim ve lint: ruff, black, isort
black "uzlaşmaz biçimlendirici"dir: tırnak, satır kırma, boşluk gibi tartışmaları tek stile bağlar; tartışma biter. isort import satırlarını gruplar ve alfabetik sıralar (standart kütüphane, üçüncü parti, proje). ruff ise Rust ile yazılmış, çok hızlı bir linter; flake8, isort ve pyflakes kurallarının büyük kısmını tek araçta toplar ve ruff format komutuyla black'in yerini de alabilir. Yeni projelerde yaygın tercih yalnızca ruff'tır; eski projelerde üçlüyü birlikte görürsün.
Ayarlar pyproject.toml içinde yaşar:
[tool.ruff] line-length = 88 target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I", "B", "UP"] # pycodestyle, pyflakes, isort, bugbear, pyupgrade ignore = ["E501"] # satır uzunluğunu format halleder [tool.mypy] python_version = "3.12" strict = true
| Araç | Ne yapar | Komut | Yakaladığı örnek |
|---|---|---|---|
| ruff check | Lint: hatalı/şüpheli kalıp | ruff check . | Kullanılmayan import, tanımsız ad, mutable default |
| ruff format / black | Biçimlendirme | ruff format . | Girinti, tırnak, satır kırma |
| isort | Import sıralama | isort . | Karışık import grupları |
| mypy | Statik tip denetimi | mypy src/ | str | None değere .upper() çağırmak |
| pytest --cov | Test ve kapsam | pytest --cov=paket | Hiç test edilmeyen dal |
Kendi lint kuralını yazmak: ast
Linter'ların sihri yok: kaynak kodu ast (abstract syntax tree, soyut sözdizimi ağacı) ile ayrıştırır ve ağaçta dolaşırlar. Aynı şeyi sen de yapabilirsin. Aşağıdaki örnek, bir kod parçasında unutulmuş print çağrılarını bulur:
import ast
KOD = '''
import os
import json
def skor_hesapla(gelir, borc):
print("debug:", gelir)
oran = borc / gelir
return json.dumps({"oran": oran})
'''
agac = ast.parse(KOD)
for dugum in ast.walk(agac):
if isinstance(dugum, ast.Call) and isinstance(dugum.func, ast.Name):
if dugum.func.id == "print":
print(f"satır {dugum.lineno}: print bırakılmış")
ast.walk ağaçtaki her düğümü gezer; ast.Call bir fonksiyon çağrısı, ast.Name basit bir addır. Aynı yaklaşımla kullanılmayan import'ları bulmak da kolay: import edilen adları topla, kodda ast.Name olarak geçenleri topla, farkı al. Yukarıdaki kodda os import edilmiş ama hiç kullanılmamış; ruff bunu F401 koduyla bildirir. Alıştırmalarda bu kuralı kendin yazacaksın.
json.dumps gibi çağrılarda modül adı ast.Attribute düğümünün value kısmındaki ast.Name olarak görünür. ast.walk tüm alt düğümleri de gezdiği için yalnızca ast.Name toplamak yeterlidir.mypy ile tip sözleşmesi
Tip ipuçları (type hints) belge gibidir; mypy onları sözleşmeye dönüştürür. Şu fonksiyonu düşün:
def musteri_bul(idler: dict[int, str], mid: int) -> str | None:
return idler.get(mid)
ad = musteri_bul({1: "Ayşe"}, 2)
# mypy: Item "None" of "str | None" has no attribute "upper"
# print(ad.upper())
if ad is not None:
print(ad.upper())
else:
print("müşteri yok")
Yorum satırındaki çağrı mypy'de hata verir, çünkü dönüş tipi None olabilir. Bu uyarı, gece yarısı çökmesini kod yazılırken yakalar. strict = true ile her fonksiyonun tip açıklaması zorunlu olur; yeni projede baştan açmak kolay, eski projede modül modül açmak gerçekçidir.
pytest fixture ve coverage
Testlerin çoğu aynı hazırlığı tekrarlar: örnek sipariş listesi, geçici dizin, sahte veritabanı. fixture, bu hazırlığı bir fonksiyona koyup testlere parametre olarak enjekte eder. pytest, test fonksiyonunun parametre adına bakarak aynı adlı fixture'ı bulur ve çağırır:
import pytest
@pytest.fixture
def siparisler():
return [{"id": 1, "tutar": 250}, {"id": 2, "tutar": 90}]
@pytest.fixture
def gecici_db(tmp_path): # fixture başka fixture kullanabilir
yol = tmp_path / "test.db"
baglanti = sqlite3.connect(yol)
yield baglanti # test burada çalışır
baglanti.close() # temizlik (teardown)
def test_toplam(siparisler):
assert toplam_tutar(siparisler) == 340
Fixture gövdesindeki yield, kurulum ile temizliği ayırır: yield'den öncesi kurulum, sonrası temizlik. Test hata verse bile temizlik çalışır. Bu, bağlam yöneticisiyle (context manager) aynı mantıktır; aşağıdaki örnekte pytest olmadan aynı deseni kendi elinle kuruyorsun:
from contextlib import contextmanager
gunluk = []
@contextmanager
def ornek_hesaplar():
gunluk.append("kurulum")
hesaplar = {"TR1": 1000, "TR2": 250}
try:
yield hesaplar
finally:
gunluk.append("temizlik")
with ornek_hesaplar() as h:
h["TR1"] -= 100
print("bakiye:", h["TR1"])
try:
with ornek_hesaplar() as h:
raise ValueError("test patladı")
except ValueError:
pass
print(gunluk)
coverage (kapsam) ise testlerin kodun yüzde kaçını çalıştırdığını ölçer: pytest --cov=anadolu --cov-report=term-missing komutu her dosya için yüzde ve test edilmeyen satır numaralarını listeler. Yüzde yüz hedef değil, işaret fenerdir: hiç dokunulmayan bir hata dalı (except bloğu gibi) genelde en çok çökmeye yol açan yerdir.
pre-commit: hatayı commit anında yakala
pre-commit, git'in commit kancasına (hook) bağlanan bir araçtır. Her git commit öncesinde tanımlı araçları yalnızca değişen dosyalarda çalıştırır; biri başarısız olursa commit yapılmaz. Yapılandırma .pre-commit-config.yaml dosyasındadır:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
hooks:
- id: mypy
Kurulum iki komut: pip install pre-commit ve pre-commit install. İlk kez tüm projede çalıştırmak için pre-commit run --all-files. Ekip için anlamı şu: biçim tartışması pull request'e hiç gelmez, inceleme (review) mantığa odaklanır.
GitHub Actions ile CI
Yerel kanca kişisel bir güvencedir; birisi --no-verify ile atlayabilir. CI, aynı kontrolleri sunucuda, herkes için, her push'ta çalıştırır. GitHub Actions iş akışı (workflow) .github/workflows/ci.yml içinde YAML olarak tanımlanır:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
kalite:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: pip install -e ".[dev]"
- run: ruff check .
- run: ruff format --check .
- run: mypy src/
- run: pytest --cov=anadolu --cov-fail-under=80
Yapı üç katmanlıdır: on ne zaman tetikleneceğini, jobs hangi işlerin çalışacağını, her işteki steps sıralı adımları belirtir. Bir adım ya hazır bir eylem kullanır (uses) ya da bir kabuk komutu çalıştırır (run). matrix aynı işi birden fazla Python sürümünde koşturur. Son satırdaki --cov-fail-under=80, kapsam eşiğin altına düşerse işi kırmızıya boyar.
YAML yazarken yapıyı kafanda bir Python sözlüğü gibi tut: iş akışı bir dict, jobs bir dict, steps bir dict listesi. Aşağıdaki örnek, aynı yapıyı Python sözlüğü olarak kuruyor ve kurallara uyup uymadığını denetliyor; alıştırmada bunu tam bir doğrulayıcıya çevireceksin:
akis = {
"name": "CI",
"on": ["push", "pull_request"],
"jobs": {
"kalite": {
"runs-on": "ubuntu-latest",
"steps": [
{"uses": "actions/checkout@v4"},
{"run": "pip install -e ."},
{"run": "pytest"},
],
}
},
}
for ad, is_ in akis["jobs"].items():
adimlar = is_.get("steps", [])
print(f"{ad}: {len(adimlar)} adım, runs-on={is_.get('runs-on')}")
for i, adim in enumerate(adimlar, 1):
tur = "uses" if "uses" in adim else "run"
print(f" {i}. {tur}: {adim[tur]}")
${{ secrets.DB_PASSWORD }} ile oku. Loglarda otomatik maskelenir.Kod inceleme listesi
Araçlar biçimi ve tipi denetler; mantığı insan denetler. Pull request incelerken şu listeyi sırayla geç:
- Amaç: PR açıklaması ne yapıldığını ve neden yapıldığını söylüyor mu? Tek bir konuya mı odaklanmış?
- Test: Yeni davranışın testi var mı? Hata senaryosu (boş liste, None, negatif tutar) test edilmiş mi?
- Adlandırma: Fonksiyon adı ne yaptığını söylüyor mu?
veri2,tmp,islem_yapgibi adlar var mı? - Hata yönetimi: Çıplak
except:var mı? Hata yutuluyor mu, loglanıyor mu? - Yan etki: Fonksiyon parametresini (listeyi, sözlüğü) yerinde değiştiriyor mu? Global durum var mı?
- Gereksiz karmaşıklık: Aynı iş daha az kodla, standart kütüphaneyle yapılabilir mi?
- Güvenlik: Kullanıcı girdisi SQL'e, kabuk komutuna, dosya yoluna doğrudan gidiyor mu? Gizli bilgi kodda mı?
Yorumlarda ton önemli: "Bu yanlış" yerine "Burada tutar None gelirse ne olur?" diye sor. İyi inceleme, yazarın öğrenmesini sağlar; kötü inceleme bir sonraki PR'ı geciktirir.
Derinleş: kalite hattını üretim düzeyine taşımak
ruff, mypy, pre-commit ve temel bir GitHub Actions iş akışı kurmayı gördün. Gerçek bir ekipte bunların üstüne dört konu daha eklenir: karmaşıklık ölçümü (kodun ne kadar dallandığı), bilinçli istisnalar (noqa, dosya bazlı kural kapatma), güvenlik taraması (bandit, pip-audit) ve ileri CI (işler arası bağımlılık, önbellek, matris, eşzamanlılık). Parametrize testler ve mock ayrıntıları "İleri test" dersinde; paket yayımlama adımları "Paketleme ve dağıtım" dersinde.
1. Döngüsel karmaşıklık (cyclomatic complexity)
Bir fonksiyondaki bağımsız yol sayısıdır: 1'den başlar, her if, elif, for, while, except, üçlü ifade ve her ek and/or koşulu bir ekler. 10'u geçen fonksiyonu test etmek zorlaşır, çünkü her yol için ayrı test gerekir. ruff'ta C901 kuralı ve max-complexity ayarı bunu denetler; radon cc -s src/ ise harf notu (A–F) verir. Hesap ast ile birkaç satırdır:
import ast
KOD = """
def kredi_karari(skor, gelir, kara_liste):
if kara_liste:
return "red"
elif skor >= 700 and gelir > 30000:
return "onay"
elif skor >= 600 or gelir > 80000:
return "inceleme"
return "red"
def kdv(tutar):
return round(tutar * 1.2, 2)
"""
DALLAR = (ast.If, ast.For, ast.While, ast.IfExp, ast.ExceptHandler)
for f in ast.parse(KOD).body:
k = 1
for d in ast.walk(f):
if isinstance(d, DALLAR):
k += 1
elif isinstance(d, ast.BoolOp):
k += len(d.values) - 1
print(f"{f.name}: {k}")
elif ast'te ayrı bir düğüm değildir; önceki if'in orelse listesinde iç içe bir ast.If olarak durur. Bu yüzden saymak doğru sonucu verir: kredi_karari için 1 + 3 if + 2 BoolOp = 6.
2. Bilinçli istisnalar: noqa ve per-file-ignores
Her kural her yerde doğru değildir. __init__.py içindeki yeniden dışa aktarımlar (re-export) "kullanılmayan import" (F401) gibi görünür; testlerde assert kullanımı bandit'in S101 kuralına takılır. Kuralı bütün projede kapatmak yerine dar kapsamda kapat:
[tool.ruff.lint] select = ["E", "F", "I", "B", "UP", "C90", "S"] [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # yeniden dışa aktarım "tests/**" = ["S101"] # testte assert serbest
Tek satırlık istisna için satır sonuna # noqa: F401 yazılır. Kural kodu olmadan çıplak # noqa o satırdaki her uyarıyı susturur; bu yüzden ruff'ın PGH004 kuralı çıplak noqa'yı yasaklar. ruff check --statistics hangi kuralın kaç kez tetiklendiğini özetler; yeni bir kuralı açmadan önce bu sayıya bakarsın.
import re
KAYNAK = """import os # noqa: F401
import sys # noqa
eval(girdi) # noqa: S307, E501
x=1"""
RX = re.compile(r"#s*noqa(?::s*([A-Z]+[0-9]+(?:s*,s*[A-Z]+[0-9]+)*))?", re.I)
for no, satir in enumerate(KAYNAK.splitlines(), 1):
m = RX.search(satir)
if not m:
print(no, "istisna yok")
elif m[1] is None:
print(no, "TÜM kurallar susturuldu (tehlikeli)")
else:
print(no, "susturulan:", [k.strip().upper() for k in m[1].split(",")])
3. Güvenlik taraması
| Araç | Kurulum ve komut | Ne bulur? |
|---|---|---|
| bandit | pip install bandit → bandit -r src -ll | Kod kalıpları: eval, shell=True, sabit parola, pickle.loads, SQL birleştirme |
| ruff S kuralları | select = ["S"] | bandit kurallarının ruff içindeki karşılığı, çok daha hızlı |
| pip-audit | pip install pip-audit → pip-audit -r requirements.txt | Bilinen zafiyetli bağımlılık sürümleri (CVE) |
| detect-secrets / gitleaks | pre-commit kancası olarak | Depoya giren token ve anahtarlar |
Sık bulgular: B602 subprocess.run(komut, shell=True) (kabuk enjeksiyonu; liste argüman kullan), B105 parola = "1234" (gizli bilgiyi ortam değişkeninden oku), B608 f-string ile kurulmuş SQL (parametreli sorgu kullan), B307 eval. Bir bulgu yanlış alarmsa # nosec B602 ile gerekçesini yazarak kapatırsın.
pyproject.toml dev bağımlılıklarında ve .pre-commit-config.yaml'daki rev alanında sabitle; pre-commit autoupdate ile bilinçli olarak yükselt.4. İleri GitHub Actions
name: CI
on:
push: { branches: [main] }
pull_request:
workflow_dispatch: # elle tetikleme düğmesi
concurrency: # aynı PR'a yeni push gelince eskisini iptal et
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
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 && ruff check . && mypy src/
test:
needs: lint # lint geçmeden test başlamaz
timeout-minutes: 15
strategy:
fail-fast: false # bir kombinasyon düşse de diğerleri sürsün
matrix:
os: [ubuntu-latest, windows-latest]
python: ["3.11", "3.12"]
exclude:
- { os: windows-latest, python: "3.11" }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "${{ matrix.python }}", cache: pip }
- run: pip install -e ".[dev]" && pytest --cov --cov-report=xml
- uses: actions/upload-artifact@v4
if: always()
with: { name: "kapsam-${{ matrix.os }}-${{ matrix.python }}", path: coverage.xml }
Matrisi kafanda genişletebilmelisin: os × python 4 kombinasyon, exclude birini çıkarır, 3 iş koşar. Aynı mantık Python'da:
from itertools import product
matris = {"os": ["ubuntu-latest", "windows-latest"], "python": ["3.11", "3.12"]}
haric = [{"os": "windows-latest", "python": "3.11"}]
anahtarlar = list(matris)
isler = []
for degerler in product(*matris.values()):
kombin = dict(zip(anahtarlar, degerler))
if any(all(kombin[k] == v for k, v in h.items()) for h in haric):
continue
isler.append(kombin)
for i in isler:
print(i["os"], i["python"])
print("toplam iş:", len(isler))
Yerelde birden fazla Python sürümünde aynı kontrolü koşmak için nox (ya da tox) kullanılır: pip install nox, kökte noxfile.py ve nox -s testler. CI dosyası da yalnızca nox çağırır; böylece "CI'da ne çalışıyorsa yerelde de o çalışır".
import nox
@nox.session(python=["3.11", "3.12"])
def testler(session):
session.install("-e", ".[dev]")
session.run("pytest", "--cov")
@nox.session
def lint(session):
session.install("ruff")
session.run("ruff", "check", ".")
concurrency ile eski koşular iptal ediliyor mu, yavaş entegrasyon testleri -m "not slow" ile PR'dan ayrılmış mı. Bir de PR açıklamasına "CI yeşil, kapsam %82 → %84" yazmak, inceleyenin işini ciddi biçimde kolaylaştırır.CI "kod sağlam mı?" sorusunu yanıtlar. Sürekli dağıtım (continuous delivery/deployment, CD) ise bir sonraki soruyu: "Sağlam kod kullanıcıya nasıl, güvenle ve geri alınabilir biçimde ulaşır?" Bu bölüm imaj üretmeyi, ortamları, sürüm etiketlerini, geri almayı ve ML projelerinde model kalite kapısını anlatıyor.
Teslim mi, dağıtım mı?
| Kavram | Ne olur? |
|---|---|
| Sürekli entegrasyon (CI) | Her push'ta lint, tip, test; ana dal hep yeşil |
| Sürekli teslim (continuous delivery) | Her yeşil commit yayına hazır bir paket (imaj) üretir; prod'a çıkış bir onay ya da etiketle olur |
| Sürekli dağıtım (continuous deployment) | Yeşil commit insan onayı olmadan prod'a çıkar |
Çoğu ekip ortadakini kullanır: main'e her birleşme staging'e otomatik gider; prod'a çıkış bir sürüm etiketi (v1.4.2) ve onayla olur.
İmajı bir kez inşa et, her yere aynısını taşı
CD'nin altın kuralı: staging'de test ettiğin aynı imaj prod'a gider; prod için yeniden inşa edilmez. Ortamlar arasında yalnız yapılandırma (ortam değişkenleri, sırlar) değişir. İmaj, kayıt defterine (registry: GitHub Container Registry, Docker Hub, ECR) değiştirilemez bir etiketle gönderilir: commit SHA'sı ya da sürüm numarası. latest yalnız kolaylık etiketidir, dağıtımda kullanılmaz.
# .github/workflows/cd.yml
name: cd
on:
push:
branches: [main]
tags: ["v*.*.*"]
jobs:
imaj:
runs-on: ubuntu-latest
permissions: {contents: read, packages: write}
outputs: {etiket: ${{ steps.meta.outputs.version }}}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: {registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }}}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/anadolu/market-api
tags: |
type=sha
type=semver,pattern={{version}}
- uses: docker/build-push-action@v6
with: {push: true, tags: ${{ steps.meta.outputs.tags }}}
staging:
needs: imaj
environment: staging
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh staging ${{ needs.imaj.outputs.etiket }}
- run: ./smoke_test.sh https://staging.market.example.com
prod:
needs: staging
if: startsWith(github.ref, 'refs/tags/v')
environment: production # GitHub'da "required reviewers" ile onay ister
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh prod ${{ needs.imaj.outputs.etiket }}
environment: anahtarı GitHub'da ortam başına ayrı sırlar ve koruma kuralları (onaylayıcı, bekleme süresi, yalnız etiketlerden dağıtım) tanımlamanı sağlar. Duman testi (smoke test), dağıtımdan hemen sonra birkaç kritik uç noktayı (/health, giriş, sepet) çağıran kısa bir denetimdir; başarısızsa hat durur.
Sürüm numarası: anlamsal sürümleme
Anlamsal sürümleme (semantic versioning) MAJOR.MINOR.PATCH biçimindedir: geriye uyumsuz değişiklik major'ı, yeni özellik minor'ı, hata düzeltmesi patch'i artırır. Commit mesajlarını Conventional Commits kuralıyla (feat:, fix:, feat!:) yazarsan bir sonraki sürümü araçlar (release-please, semantic-release) otomatik hesaplar ve değişiklik günlüğünü (CHANGELOG) üretir.
Geri alma (rollback)
Her dağıtım bir gün geri alınacakmış gibi yapılır. Değiştirilemez etiketler sayesinde geri almak, bir önceki başarılı imaj etiketini yeniden dağıtmaktır (kubectl rollout undo, Render/Fly panelinde "redeploy previous"). Geri almayı zorlaştıran tek şey veritabanıdır: yeni sürümün migration'ı eski kodu bozuyorsa geri dönüş de bozulur. Bu yüzden sqlalchemy-orm dersindeki genişlet-daralt kalıbı CD'nin ön koşuludur. Riskli özellikleri özellik bayrağı (feature flag) arkasında yayınlamak, kodu geri almadan özelliği kapatmayı sağlar. Kanarya (canary) dağıtımda yeni sürüm önce trafiğin küçük bir yüzdesine açılır, hata oranı izlenir, sorun yoksa genişletilir.
ML projelerinde model kalite kapısı
Model de bir yapıt (artifact) gibi sürümlenir ve dağıtılır; ama "testler geçti" yetmez, yeni model eskisinden kötü olabilir. Kalite kapısı, aday modeli üretimdeki (temel) modelle aynı sabit doğrulama kümesinde karşılaştırır ve kurallardan biri bozulursa dağıtımı durdurur: ana metrik en az temel kadar olmalı, gecikme ve model boyutu sınırı aşmamalı, hiçbir müşteri segmentinde performans belirgin düşmemeli.
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=1500, n_features=8, random_state=7)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=7)
temel = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
aday = DecisionTreeClassifier(max_depth=3, random_state=7).fit(X_tr, y_tr)
auc_t = roc_auc_score(y_te, temel.predict_proba(X_te)[:, 1])
auc_a = roc_auc_score(y_te, aday.predict_proba(X_te)[:, 1])
TOLERANS = 0.005 # gürültü payı
gecti = auc_a >= auc_t - TOLERANS
print(f"temel AUC={auc_t:.3f} aday AUC={auc_a:.3f} kapı: {'GEÇTİ' if gecti else 'REDDEDİLDİ'}")
Bu betik CI'da çalışır ve reddedince sys.exit(1) ile hattı durdurur. Doğrulama kümesi sabit tutulur (sürümlenir); her eğitimde değişirse karşılaştırma anlamsızlaşır. MLflow model kayıt defterinde "Staging → Production" geçişi de bu kapının arkasına konur.
Kalite kapıları (quality gates)
Kurumsal ekiplerde ana dala birleştirme (merge) için koşullar tanımlanır: CI yeşil, en az bir onay, kapsam düşmemiş, tip hatası sıfır. GitHub'da "branch protection" ile bu kapılar zorunlu kılınır. Kapıları kademeli sıkılaştır; ilk gün yüzde doksan kapsam istemek ekibi küstürür.
Hızlı CI
Yavaş CI, atlanan CI'dır. Üç pratik: bağımlılıkları önbelleğe al (actions/setup-python içindeki cache: pip), yalnızca değişen testleri önce koş (pytest --lf), yavaş entegrasyon testlerini ayrı işe taşı ve yalnızca ana dalda çalıştır.
ruff kural seçimi
Tüm kuralları açmak gürültü yaratır. Başlangıç seti E, F, I; olgun projede B (bugbear: mutable default, gereksiz lambda), UP (pyupgrade: eski sözdizimi), SIM (sadeleştirme) eklenir. Her kuralı kapatırken ignore listesine yorum yaz: altı ay sonra neden kapattığını hatırlamazsın.
Tip denetimini kademeli açmak
Eski kod tabanında mypy önce yüzlerce hata verir. Çözüm: [[tool.mypy.overrides]] ile eski modüllere ignore_errors = true, yeni modüllere strict = true. Her dokunulan modül sıkı moda taşınır; birkaç ayda tüm taban temizlenir.