Ana içeriğe geç
Kesit Python
İleri · Üretime hazırlık

Kod kalitesi ve CI: ruff, mypy, pre-commit, pytest, GitHub Actions

Kod kalitesini otomatik araçlarla korumayı, test ve kapsam ölçmeyi, pre-commit ve GitHub Actions ile her commit'te kontrol çalıştırmayı öğrenirsin.

Uygulamada aç, 32 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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 yaparKomutYakaladığı örnek
ruff checkLint: hatalı/şüpheli kalıpruff check .Kullanılmayan import, tanımsız ad, mutable default
ruff format / blackBiçimlendirmeruff format .Girinti, tırnak, satır kırma
isortImport sıralamaisort .Karışık import grupları
mypyStatik tip denetimimypy src/str | None değere .upper() çağırmak
pytest --covTest ve kapsampytest --cov=paketHiç 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.

ast.Attributejson.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.

Kapsam yüzdesi kaliteyi garanti etmezSatırın çalışmış olması doğru çalıştığı anlamına gelmez; assert içermeyen bir test yüzde yüz kapsam verir ama hiçbir şey kanıtlamaz. Kapsamı düşük dalları bulmak için kullan, hedef sayı için değil.

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]}")
Gizli bilgilerAPI anahtarı ve parolayı YAML'a yazma; depo ayarlarındaki Secrets bölümüne koy ve ${{ 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ç:

  1. Amaç: PR açıklaması ne yapıldığını ve neden yapıldığını söylüyor mu? Tek bir konuya mı odaklanmış?
  2. Test: Yeni davranışın testi var mı? Hata senaryosu (boş liste, None, negatif tutar) test edilmiş mi?
  3. Adlandırma: Fonksiyon adı ne yaptığını söylüyor mu? veri2, tmp, islem_yap gibi adlar var mı?
  4. Hata yönetimi: Çıplak except: var mı? Hata yutuluyor mu, loglanıyor mu?
  5. Yan etki: Fonksiyon parametresini (listeyi, sözlüğü) yerinde değiştiriyor mu? Global durum var mı?
  6. Gereksiz karmaşıklık: Aynı iş daha az kodla, standart kütüphaneyle yapılabilir mi?
  7. 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ş

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 komutNe bulur?
banditpip install bandit → bandit -r src -llKod 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-auditpip install pip-audit → pip-audit -r requirements.txtBilinen zafiyetli bağımlılık sürümleri (CVE)
detect-secrets / gitleakspre-commit kancası olarakDepoya 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.

Yerelde geçti, CI'da patladıÇoğu zaman sebep sürüm farkıdır: yerelde ruff 0.4, CI'da en yeni sürüm kurulur ve yeni kurallar devreye girer. Araç sürümlerini 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", ".")
Gerçek işteYeni katıldığın bir ekipte ilk haftanın tipik görevi: CI'ın neden 20 dakika sürdüğünü bulmak ya da "flaky" (bazen geçen bazen düşen) bir testi ayıklamak. İlk bakacağın yerler: pip önbelleği açık mı, lint ve test ayrı işlerde paralel mi, 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.
Derinleş

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ı?

KavramNe 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.

"İleri düzelt" her zaman seçenek değildirProd bozukken hızlı bir düzeltme commit'i yazıp yeniden dağıtmak (fix forward) cazip gelir ama CI süresi kadar kesinti demektir. Önce geri al, kullanıcıyı kurtar, sonra sakin kafayla düzelt.

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.

Gerçek işteOlgun ekiplerde prod'a çıkış sıkıcı bir olaydır: günde birkaç kez, küçük değişikliklerle, otomatik duman testi ve tek tıkla geri almayla. Dağıtım sıklığı, değişiklik başarısızlık oranı, geri dönüş süresi ve değişikliğin prod'a ulaşma süresi (DORA metrikleri) bu olgunluğun ölçüsüdür.
Uzmanlaş

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.

Alıştırmalara geç (32)