Mert Özen Her Satırda Daha İleri
Yapay Zeka GitHub Copilot — #08

Copilot Code Review: PR'da Otomatik Review, Ruleset ile Zorunlu Kılmak, Talimatlarla Yönlendirmek ve Sınırları

Mert Özen 15 Kas 2026 12 dk 5 görüntülenme
Copilot Code Review: PR'da Otomatik Review, Ruleset ile Zorunlu Kılmak, Talimatlarla Yönlendirmek ve Sınırları

Copilot code review talimatları pull request'in head branch'inden okur ve tsconfig.json, lock dosyaları gibi bazı dosyalara hiç bakmaz; bu yüzden bir pull request kendi review kuralını gevşetebilir. Bookstore'da bunu bir script'le gösterdik. Otomatik review ve ruleset JSON'u, effort seviyeleri ve maliyet, review talimatları ve code-review skill'i, Copilot approvals ve insan review'uyla işbölümü.

Sürüm notu: Bu yazıdaki kodlar ve script'ler .NET 10 ve Entity Framework Core (EF Core) 10.0.15 ile derlenip denenmiştir; testler SQL Server 2025 üzerinde çalıştırılmış, ruleset dosyası GitHub API'sinin OpenAPI açıklamasındaki şemaya karşı doğrulanmıştır. Copilot code review bu yazıda çalıştırılmadı (GitHub hesabı ve Copilot aboneliği kullanılmadı); review'un nasıl tetiklendiği, neyi okuduğu, effort seviyeleri ve fiyatlar 15 Kasım 2026'da docs.github.com'dan alınmıştır. Hiçbir yerde Copilot'un yazdığı bir review yorumu gösterilmiyor.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) siparişe kargo ücreti ekleyen bir pull request açıldı ve repoda Copilot code review otomatik çalışıyor. Pull request'te dört sorun var: HasPrecision verilmemiş yeni bir decimal property, Created kontrolünü IsSuccessStatusCode'a gevşeten bir test, review talimatından "değişen assertion'ı sor" satırını silen bir değişiklik ve Angular'ın tsconfig.json'undan çıkarılmış noImplicitReturns.

Tahminin ne? Copilot dördünü de yakalar mı? Hangisine hiç bakmaz? Talimattan silinen satır review'u etkiler mi, yoksa Copilot main'deki talimatı mı kullanır? İkisinin cevabı dokümanda, birini bu yazıdaki script'le gördük.

Review İstemek ve Otomatik Review

En basit yol pull request'in sağındaki Reviewers bölümünde Copilot'un yanındaki Request düğmesi; doküman review'un genelde 30 saniyeden kısa sürdüğünü söylüyor. Copilot varsayılan olarak bir "Comment" review bırakıyor, "Approve" ya da "Request changes" değil; bu yüzden varsayılan ayarlarla zorunlu onay sayısına katılmıyor. Yorumları insan yorumları gibi yanıtlanıp çözülebiliyor ama yazdığın cevapları Copilot görmüyor. Önerilen değişiklikler birkaç tıkla uygulanabiliyor ya da Fix with Copilot ile yedinci yazıdaki cloud agent'a devredilebiliyor.

Otomatik review'u üç yerden açabiliyorsun: kendi pull request'lerin için kişisel ayarlardan (Pro, Pro+, Max, Business ve Enterprise), repository için bir ruleset ile ve organization için bir organization ruleset'i ile. Bu ayarlar birbirini ezmiyor; biri açıksa review yapılıyor ve ikisi de açıksa yine tek review geliyor. Tetikleyiciler: pull request "Open" olarak açıldığında ya da draft ilk kez "Open" yapıldığında; Review new pushes seçiliyse her yeni push'ta; Review draft pull requests seçiliyse draft'ken de. Review new pushes kapalıyken Copilot bir pull request'i yalnız bir kez review ediyor; sonraki değişiklikler için review'u elle yeniden istemek gerekiyor. Dokümanın bir uyarısı: yeniden review'da Copilot çözdüğün ya da olumsuz oyladığın yorumları tekrar edebiliyor.

Ruleset ile Zorunlu Kılmak

Bookstore'da main için istediğimiz kural iki parçalı: her pull request'e bir insan onayı ve her push'ta Copilot review'u. Ruleset'i arayüzden de oluşturabilirsin; biz tekrar üretilebilsin diye repoya bir JSON dosyası olarak koyduk:

{
  "name": "main: human review and Copilot review",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["~DEFAULT_BRANCH"],
      "exclude": []
    }
  },
  "rules": [
    { "type": "deletion" },
    { "type": "non_fast_forward" },
    {
      "type": "pull_request",
      "parameters": {
        "required_approving_review_count": 1,
        "dismiss_stale_reviews_on_push": true,
        "require_code_owner_review": false,
        "require_last_push_approval": true,
        "required_review_thread_resolution": true,
        "require_extra_approval_for_unattributed_changes": true
      }
    },
    {
      "type": "copilot_code_review",
      "parameters": {
        "review_on_push": true,
        "review_draft_pull_requests": false
      }
    }
  ]
}

copilot_code_review kuralının iki parametresi var: review_on_push ve review_draft_pull_requests. pull_request kuralındaki require_extra_approval_for_unattributed_changes ise bu serinin konusu açısından ilginç: Copilot bir pull request'i insan bir ortak yazar olmadan açtığında bir onay daha istiyor. require_last_push_approval da son push'u yapan kişinin kendi pull request'ini onaylamasını engelliyor.

Dosyayı GitHub'a göndermeden önce, GitHub API'sinin yayınladığı OpenAPI açıklamasındaki ruleset şemasına karşı yerelde doğruladık: hata yok. Aynı denemede zorunlu bir alanı (require_last_push_approval) silince şema dosyayı reddetti, ama yanlış yazılmış bir parametre adını (review_on_pushes) kabul etti; şema bilinmeyen alanlara izin veriyor. Yani yazım hatası yapılmış bir parametre sessizce yok sayılabilir; ruleset'i oluşturduktan sonra arayüzde kontrol et. Dosyayı GitHub CLI ile göndermek için (bu yazıda çalıştırmadık):

gh api --method POST repos/OWNER/bookstore/rulesets --input eng/rulesets/main-review.json

Bir ayar daha var: Copilot approvals. Repository ayarlarında Allow Copilot to approve pull requests ve Allow Copilot approvals to count toward merge requirements açılırsa, Copilot'un onayı zorunlu onay sayısını bir ekip arkadaşının onayı gibi karşılayabiliyor; en fazla 15 glob ile yalnız belirli dosyalara dokunan pull request'lerle sınırlanabiliyor ve yeni bir commit gelince onay düşüyor. Bookstore'da bu ayarı kapalı tuttuk; aşağıda nedenini göreceğiz.

Effort Seviyesi ve Maliyet

Code review'da model seçilemiyor; GitHub bunun bilinçli olduğunu, modeli değiştirmenin review kalitesini bozacağını söylüyor. Seçebildiğin şey effort seviyesi:

SeviyeNe yaparReview başına tipik aralıkOrtalama
LiteHızlı, yaygın hatalara odaklı review$0.05 – $1$0.25
Balanced (varsayılan)Karmaşık mantık ve güvenlik için daha yüksek reasoning'li model$0.25 – $5$1.50
Max (public preview)Birden fazla yüksek reasoning'li model$1.50 – $25$10

Tutarlar tahmin, AI Credits cinsinden ve GitHub Actions dakikalarını içermiyor: review'un repoyu tarayan agentic kısmı Actions runner'ında çalışıyor. Seviye şu sırayla belirleniyor: review istenirken seçilen, bu pull request'te daha önce kullanılan, isteyen kişinin ayarı, repository ayarı, organization ayarı ve son olarak Balanced. Repository yöneticileri bir üst sınır da koyabiliyor.

Faturayı kimin ödediği de belli: otomatik review'da kredi pull request'in yazarından düşüyor, review'u başkası elle istediyse ondan. Cloud agent'ın açtığı pull request'lerde önce insan ortak yazara, o da faturalanamıyorsa organization'a yazılıyor. Her push'ta Balanced review, 40 push'luk bir pull request'te ortalama 60 dolar demek; Review new pushes'ı her repoda açmadan önce bunu hesapla.

Talimatlarla Yönlendirmek

Dördüncü yazıda excludeAgent: "cloud-agent" ile yalnız review'a giden bir talimat dosyası yazmıştık. Bu yazıda onu genişlettik ve nasıl yorum yazılacağını da ekledik:

---
applyTo: "**"
excludeAgent: "cloud-agent"
---

When reviewing a pull request in this repository, check these first:

- A query inside a loop, or `Include` chains that load more than the endpoint returns.
- A new `decimal` property without `HasPrecision`, or a new string without `HasMaxLength`.
- A migration that drops or renames a column without a data step.
- A changed or deleted assertion in `tests/`; ask why the expected value changed.
- SQL built with string concatenation or interpolation in `FromSqlRaw` or `ExecuteSqlRaw`.
- An endpoint that returns an entity instead of a record, or a module that reads another module's tables.
- In Angular, a subscription without cleanup or state kept outside signals.

How to comment:

- Comment only when you can name the concrete failure (wrong result, data loss, exception, security issue).
- Do not comment on formatting; `dotnet format` and Prettier handle it.
- Put one finding in one comment, with a suggested change when the fix is local.

Son bölüm review yorgunluğu için: biçim yorumlarını formatter'lara bırakmak ve somut bir hata adı veremeyen yorumu hiç yazmamak, ekibin Copilot yorumlarını okumaya devam etmesini sağlıyor. Code review bunun yanında copilot-instructions.md, kök dizindeki AGENTS.md, varsa CLAUDE.md, GEMINI.md ve REVIEW.md dosyalarını ve skill'leri de okuyor. Doküman, review'a özel bir skill'in kullanılmasını garantilemek için klasör adını code-review koymayı öneriyor:

---
name: code-review
description: Review workflow for Bookstore pull requests. Use when reviewing a pull request or a branch diff in this repository, especially changes under src/Bookstore.Api/Data/Migrations or tests/.
---

Review the pull request in this order:

1. If the diff touches `src/Bookstore.Api/Data/Migrations`, run `dotnet run .github/skills/ef-migration-review/check-migrations.cs` and comment on every `RISK` line that the pull request description does not explain.
2. List every line removed from `tests/` (`git diff --unified=0 <base>...HEAD -- tests/`). A removed or changed `Assert` line needs a reason in the pull request; if there is none, comment and ask for it.
3. If the diff changes `.github/instructions`, `.github/skills`, `.github/copilot-instructions.md` or `AGENTS.md`, say so in the overview: this review used the changed versions.
4. Apply `.github/instructions/code-review.instructions.md` to the remaining changes.

Skill'in ilk adımı altıncı yazıdaki migration script'ini çağırıyor. Repository ayarlarına eklenen MCP (Model Context Protocol; agent'ın dış araçlara bağlandığı protokol) server'ları da review'a açık; yalnız readOnlyHint değeri true olan tool'lar kullanılıyor. Hangi skill'in ya da MCP server'ının kullanıldığı her yorumun altındaki atıflarda ve review oturumunun log'unda görünüyor.

Review Ne Okuyor: Head Branch ve Hariç Dosyalar

İki kural Önce Tahmin Et'in cevabını veriyor. Birincisi: Copilot talimatları, skill'leri ve agent talimatlarını pull request'in head branch'inden okuyor, main'den değil. Doküman bunu bir kolaylık olarak sunuyor: talimat değişikliğini birleştirmeden aynı pull request'te deneyebiliyorsun. Ama aynı kural, bir pull request'in kendi review kurallarını gevşetebileceği anlamına da geliyor. İkincisi: bazı dosyalar review'a hiç girmiyor; dokümandaki listede package-lock.json, tsconfig.json, .gitignore, *.lock, *.svg, *.log, dist ve node_modules var. Kavram sayfası örnek olarak package.json'u da sayıyor, referans listesi saymıyor; bu tür bir tutarsızlıkta kendi kontrolünü yap.

Bir pull request için bu iki kuralın sonucunu görmek üzere repoya küçük bir script ekledik:

// Kısaltılmış; Git(), Show(), Field() ve GlobToRegex() dosyanın sonundaki küçük yardımcılar.
string baseRef = args.ElementAtOrDefault(0) ?? "main";
string headRef = args.ElementAtOrDefault(1) ?? "HEAD";

// "Files excluded from GitHub Copilot code review" listesinin bir kısmı (docs.github.com, Kasım 2026).
string[] excludedNames = [".gitignore", "package-lock.json", "yarn.lock", "tsconfig.json", "jest.config.js", "requirements.txt", "paket.lock"];
string[] excludedGlobs = ["**/*.svg", "**/*.log", "**/*.lock", "**/dist/**", "**/node_modules/**"];
string[] guidanceFiles = [".github/copilot-instructions.md", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "REVIEW.md"];

string[] changed = await Git($"diff --name-only {baseRef}...{headRef}");
string[] headFiles = await Git($"ls-tree -r --name-only {headRef}");
string[] skipped = changed.Where(f => excludedNames.Contains(Path.GetFileName(f)) || excludedGlobs.Any(g => GlobToRegex(g).IsMatch(f))).ToArray();
string[] reviewed = changed.Except(skipped).ToArray();

Console.WriteLine($"{baseRef}...{headRef}: {changed.Length} changed file(s)");
Console.WriteLine($"Reviewed ({reviewed.Length}): {string.Join(", ", reviewed)}");
Console.WriteLine($"Skipped by Copilot code review ({skipped.Length}): {string.Join(", ", skipped)}");

Console.WriteLine($"Instructions read from {headRef}:");
int characters = 0;
foreach (string file in guidanceFiles.Where(headFiles.Contains))
{
    characters += (await Show(headRef, file)).Length;
    Console.WriteLine($"  always  {file}");
}
foreach (string file in headFiles.Where(f => f.StartsWith(".github/instructions/") && f.EndsWith(".instructions.md")))
{
    string content = await Show(headRef, file);
    string applyTo = Field(content, "applyTo");
    if (Field(content, "excludeAgent") == "code-review")
    {
        Console.WriteLine($"  skip    {file} (excludeAgent: code-review)");
        continue;
    }
    int matches = reviewed.Count(f => applyTo.Split(',', StringSplitOptions.TrimEntries).Any(g => GlobToRegex(g).IsMatch(f)));
    if (matches > 0)
    {
        characters += content.Length;
    }
    Console.WriteLine(matches > 0 ? $"  path    {file} (applyTo {applyTo} -> {matches} reviewed file(s))" : $"  skip    {file} (applyTo {applyTo} matches no reviewed file)");
}
Console.WriteLine($"  {characters:N0} characters of instructions");

string[] skills = headFiles.Where(f => f.StartsWith(".github/skills/") && f.EndsWith("/SKILL.md")).Select(f => f.Split('/')[2]).ToArray();
Console.WriteLine($"Skills: {string.Join(", ", skills.Select(s => s == "code-review" ? s + " (review-focused name)" : s))}");

foreach (string file in changed.Where(f => f.StartsWith(".github/instructions/") || f.StartsWith(".github/skills/") || guidanceFiles.Contains(f)))
{
    Console.WriteLine($"warning: this pull request changes {file}; Copilot reviews it with the changed version");
}

Kargo ücreti branch'inde:

$ dotnet run eng/review-context.cs main feature/order-shipping-fee
main...feature/order-shipping-fee: 7 changed file(s)
Reviewed (6): .github/instructions/code-review.instructions.md, src/Bookstore.Api/Data/Migrations/20261115004245_AddOrderShippingFee.Designer.cs, src/Bookstore.Api/Data/Migrations/20261115004245_AddOrderShippingFee.cs, src/Bookstore.Api/Data/Migrations/BookstoreContextModelSnapshot.cs, src/Bookstore.Api/Ordering/Order.cs, tests/Bookstore.Api.Tests/OrderingTests.cs
Skipped by Copilot code review (1): web/bookstore-web/tsconfig.json
Instructions read from feature/order-shipping-fee:
  always  .github/copilot-instructions.md
  always  AGENTS.md
  skip    .github/instructions/angular.instructions.md (applyTo web/bookstore-web/src/**/*.ts,web/bookstore-web/src/**/*.html matches no reviewed file)
  path    .github/instructions/code-review.instructions.md (applyTo ** -> 6 reviewed file(s))
  path    .github/instructions/dotnet.instructions.md (applyTo src/**/*.cs,tests/**/*.cs -> 5 reviewed file(s))
  3,980 characters of instructions
Skills: add-migration, code-review (review-focused name), ef-migration-review, new-endpoint
warning: this pull request changes .github/instructions/code-review.instructions.md; Copilot reviews it with the changed version

Script iki sorunu, Copilot daha hiçbir şey yazmadan gösteriyor. tsconfig.json review dışında: noImplicitReturns'ın kaldırılmasını Copilot code review görmeyecek. Ve bu pull request kendi review talimatını değiştiriyor: Copilot, "değişen assertion'ı sor" satırının silindiği hâliyle review edecek. Bu, Copilot'un gevşetilmiş testi kesinlikle kaçıracağı anlamına gelmiyor; diğer talimatlar ve kendi analizi onu yine yakalayabilir. Ama bu konuda yazdığımız kurala artık güvenemiyorsun.

Copilot Neyi Yakalar, Neyi Kaçırır

Copilot'un bu pull request'e ne yazacağını çalıştırmadan söyleyemeyiz; onun yerine aynı pull request'i kendi araçlarımızla inceledik. EF Core, migration üretilirken decimal sorununu zaten söylüyor:

$ dotnet ef migrations add AddOrderShippingFee --project src/Bookstore.Api --output-dir Data/Migrations
warn: Microsoft.EntityFrameworkCore.Model.Validation[30000]
      No store type was specified for the decimal property 'ShippingFee' on entity type 'Order'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.
Done. To undo this action, use 'ef migrations remove'

Migration decimal(18,2) ile oluşuyor ve uyarı, sığmayan değerlerin sessizce kesileceğini yazıyor. Bu tam olarak review talimatındaki ikinci madde; talimatı olan bir reviewer'ın yakalaması beklenen türden, kod okuyarak bulunabilen bir hata. Skill'in ikinci adımı gevşetilen testi gösteriyor:

$ git diff --unified=0 main...HEAD -- tests/
@@ -26 +26 @@ public sealed class OrderingTests(BookstoreApiFactory factory)
-        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+        Assert.True(response.IsSuccessStatusCode);
$ dotnet run .github/skills/ef-migration-review/check-migrations.cs AddOrderShippingFee
OK   20261115004245_AddOrderShippingFee: 1 operations

$ dotnet test
Test run summary: Passed!
  total: 22
  failed: 0
  succeeded: 22
  skipped: 0

Migration script'i sorun görmüyor: bir AddColumn veri kaybı riski taşımıyor ve script precision'a bakmıyor. Testler de 22/22 geçiyor; gevşetilmiş test, gevşetildiği için geçiyor. Bu yüzden Bookstore'da işbölümü şöyle: deterministik kontroller (EF Core uyarısı, migration script'i, tests/ diff'i) CI'da ve skill'de; Copilot review'u kod okuyarak bulunan hatalar için ikinci bir göz; insan reviewer ise iş kuralı, tasarım ve "bu değişiklik neden yapıldı" sorusu için. Code Review serisinin üçüncü yazısındaki checklist ve AI Destekli Geliştirme serisinin dördüncü yazısı bu işbölümünü ayrıntılı anlatıyor.

Copilot approvals'ı bu yüzden kapalı tuttuk. Bu pull request'te Copilot'un onayı merge şartını karşılasaydı, review dışındaki bir dosyadaki ve kendi kuralı silinmiş bir testteki değişiklik insan görmeden birleşebilirdi.

Üretimde Dikkat

Talimat dosyalarını CODEOWNERS ile bir sahibe bağla ve ruleset'te code owner onayını iste. Head branch kuralı nedeniyle .github/instructions, .github/skills, AGENTS.md ve REVIEW.md değişiklikleri kod değişikliği kadar önemli.

Review dışındaki dosyalar için ayrı kontrol koy. tsconfig.json ya da package-lock.json değişikliklerini bir insan ya da CI adımı görmeli; bağımlılık değişiklikleri için dependency review ve secret taraması ayrı araçlar.

Maliyeti izle. Review new pushes ve Balanced varsayılanı, aktif bir repoda ayda yüzlerce review demek. Lite'ı rutin repolarda, Balanced ya da Max'i güvenlik açısından kritik kodda kullan; repository için üst sınır koy.

Code review, organization policy'lerinde ayrı bir seçenek; Business ve Enterprise'da lisansı olmayan üyelere de açılabiliyor. Bunu onuncu yazıda ele alacağız.

Sık Düşülen Tuzaklar

Copilot onayını insan onayı yerine saymak. Copilot approvals açıksa bile insan review'u, iş kuralını ve değişikliğin nedenini kontrol eden tek adım.

Pull request'in kendi talimatını değiştirebileceğini unutmak. Review, head branch'teki talimatla yapılıyor.

Review dışındaki dosyaların da review edildiğini sanmak. tsconfig.json ve lock dosyaları listede.

Gereksiz yorum yükü. Biçim ve kişisel tercih yorumları ekibi Copilot yorumlarını okumayı bırakmaya iter; talimatta neyin yorum değeri taşıdığını yaz.

Her repoda her push'ta Max review açmak. Ortalama 10 dolarlık bir review'u günde onlarca kez tekrarlamak bütçeyi hızla bitirir.

Küçük Bir Deneme

★☆☆ Kendi reponda eng/review-context.cs'i son birleşen pull request'in base ve head commit'leriyle çalıştır. Review dışında kalan bir dosya var mı?

★★☆ check-migrations.cs'e, AddColumn<decimal> ile decimal(18,2) tipinde eklenen kolonlar için bir uyarı ekle ve kargo ücreti branch'inde dene. EF Core'un uyarısıyla aynı şeyi mi söylüyor?

★★★ Bookstore'un ruleset JSON'una main'deki bir CI workflow'unu zorunlu kılan bir required_status_checks kuralı ekle, şemaya karşı doğrula ve hangi parametrelerin zorunlu olduğunu not et. Sonra yanlış yazılmış bir parametre adıyla tekrar dene.

Copilot'u şimdiye kadar IDE'de ve GitHub'da kullandık. Bir sonraki yazıda terminale geçiyoruz: Copilot CLI'ın interactive oturumu, plan mode, sandbox, copilot -p ile otomasyon ve ACP (Agent Client Protocol; editörlerin agent'larla konuştuğu protokol) ile başka araçlara bağlanmak.