Changelog ve Release Notes: Conventional Commits'ten Otomatik CHANGELOG'a, Deprecation Politikası
CHANGELOG'u Conventional Commits'ten otomatik üret, release notes'u ise elle yaz: araç sürümü ve değişiklik listesini mekanik olarak çıkarır, ama deprecation'ı, geçiş adımlarını ve destek takvimini bilmez. Bookstore.Contracts'ı commit-and-tag-version ile 1.1.0 ve 2.0.0'a taşıyor, [Obsolete] ve DiagnosticId ile bir sürüm önceden uyarıyor, 2.0 geçiş notunu yazıyor ve release-please manifest mode ile PR tabanlı alternatifi karşılaştırıyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), Node.js 24, commit-and-tag-version 13.3, MinVer 8.0.0 ve actionlint 1.7.12 ile denenmiştir. release-please config'i şemasıyla, workflow'lar actionlint ile doğrulandı; GitHub'da koşulmadı.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Bookstore.Contracts paketi 1.0.0'da. O tag'den bu yana repository'ye şu commit'ler girdi:
$ git log --oneline contracts/v1.0.0..HEAD
bea8659 docs: add VERSIONING.md
d42aa12 fix(contracts): show currency in Money.ToString
ff112f5 feat(api): filter books by maxPrice
766285f feat(contracts): add Money and BookDto.ListPrice
024a0f1 build: skip empty contracts releases
070f58b build: add commit-and-tag-version for Contracts
f31d2a2 ci: comment placement
Commit mesajları Conventional Commits'e uyuyor ve release aracı yalnız src/Bookstore.Contracts klasörüne dokunan commit'lere bakacak şekilde ayarlı. İlk feat commit'inin gövdesinde bir not daha var: "BookDto.Price is deprecated (BOOK001) and will be removed in 2.0.0." Araç bir sonraki sürümü kaç yapar, üretilen CHANGELOG'da hangi satırlar olur? Özellikle şu iki soruyu düşün: feat(api) commit'i listede olur mu, deprecation notu görünür mü?
$ npx commit-and-tag-version --dry-run
✔ created src/Bookstore.Contracts/CHANGELOG.md
✔ outputting changes to src/Bookstore.Contracts/CHANGELOG.md
---
## [1.1.0](https://github.com/example/bookstore/compare/contracts/v1.0.0...contracts/v1.1.0) (2026-11-14)
### Features
* **contracts:** add Money and BookDto.ListPrice ([766285f](https://github.com/example/bookstore/commit/766285f989e99a17c2e9d0d12592d0e6d5b321de))
### Bug Fixes
* **contracts:** show currency in Money.ToString ([d42aa12](https://github.com/example/bookstore/commit/d42aa125eb9b9607d6cef343db72f7f762e97c04))
---
✔ committing src/Bookstore.Contracts/CHANGELOG.md
✔ tagging release contracts/v1.1.0
ℹ Run `git push --follow-tags origin main` to publish
Sürüm 1.1.0: bir feat minor, bir fix patch istiyor, yüksek olan kazanıyor. feat(api) commit'i yok, çünkü Contracts klasörüne dokunmuyor; docs, build ve ci commit'leri yapılandırmada gizli. Deprecation notu da yok: araç yalnız commit'in başlık satırını alıyor. Paketi kullanan biri bu CHANGELOG'u okuyup Price'ın bir sonraki major'da kalkacağını öğrenemez. Bu yazıda changelog, release notes ve deprecation iletişiminin hangi kısmının otomatikleşebileceğine, hangi kısmının insan eli istediğine bakıyoruz.
Changelog, Release Notes ve Commit Log
Üçü sık karıştırılıyor, ama okuru farklı. Commit log, kodu değiştiren geliştirici için: neyi neden değiştirdiğini adım adım anlatıyor. Changelog, paketi ya da uygulamayı kullanan geliştirici için: sürümden sürüme neyin değiştiğini, gruplanmış ve kısaltılmış hâlde. Release notes ise belirli bir sürümün duyurusu: değişikliğin etkisini, geçiş adımlarını ve destek bilgisini anlatıyor; okuru bazen bir geliştirici, bazen bir ürün yöneticisi ya da müşteri.
Keep a Changelog, bu üçünden changelog için yaygın bir biçim öneriyor. Dosya insanlar için yazılıyor; en yeni sürüm en üstte, her sürümün tarihi ISO (Uluslararası Standardizasyon Örgütü) 8601 tarih biçiminde (2026-11-14). Değişiklikler altı başlık altında gruplanıyor: Added, Changed, Deprecated, Removed, Fixed, Security. En üstte, henüz yayınlanmamış değişikliklerin biriktiği bir Unreleased bölümü duruyor; yayında bu bölümün içeriği yeni sürümün başlığı altına taşınıyor. Sitenin önerdiği kötü pratikler listesinde ilk sırada commit log'unu changelog yerine kullanmak var; deprecation'ları yazmamak da listede.
Bu yazıdaki ayrım şöyle olacak: CHANGELOG'u commit mesajlarından otomatik üreteceğiz, çünkü "hangi sürümde hangi değişiklik var" sorusu mekanik bir soru. Release notes'u ise elle yazacağız, çünkü geçiş adımları ve destek takvimi commit mesajına sığmıyor.
Conventional Commits'ten Sürüme
Git & GitHub serisinin "Git ile Ekip Çalışması" yazısında Conventional Commits biçimini anlatmıştık: type(scope): description, isteğe bağlı gövde ve footer. Release otomasyonu bu yapıdan üç kural çıkarıyor:
fix(ve çoğu araçtaperf): PATCH artar.feat: MINOR (ara sürüm numarası) artar.- Type'tan sonra
!ya da footer'daBREAKING CHANGE:: MAJOR (ana sürüm numarası) artar.
Diğer type'lar (docs, build, ci, refactor, test, chore) sürümü değiştirmiyor. Birden fazla commit varsa en yüksek artış kazanıyor. Bu kuralların işe yaraması tamamen commit mesajlarının dürüstlüğüne bağlı; bir breaking change'i fix: diye işaretleyen geliştirici, araca yanlış bir PATCH sürümü ürettirir. Bu yüzden ekipler mesaj biçimini bir commit-msg hook'u ya da pull request (PR) başlığını kontrol eden bir CI adımıyla zorluyor; squash merge kullanan ekiplerde PR başlığı commit mesajına dönüştüğü için kontrolü orada yapmak yeterli.
Yerelde Sürüm ve CHANGELOG: commit-and-tag-version
Bu kuralları uygulayan üç yaygın araç var: semantic-release, release-please ve commit-and-tag-version. İlk ikisi CI'da ve GitHub üzerinde çalışmak için tasarlanmış; üçüncüsü yalnız yerel repository'yi değiştiriyor: sürümü hesaplıyor, CHANGELOG'u güncelliyor, release commit'ini ve tag'i oluşturuyor, push'u sana bırakıyor. Mekaniği görmek için en kolayı bu. Repository köküne yalnız release araçları için küçük bir package.json ekledik:
{
"name": "bookstore-release",
"private": true,
"description": "Release tooling for Bookstore.Contracts",
"scripts": {
"release:contracts": "commit-and-tag-version"
},
"devDependencies": {
"commit-and-tag-version": "13.3.0"
}
}
Yapılandırma .versionrc.json'da:
{
"tagPrefix": "contracts/v",
"path": "src/Bookstore.Contracts",
"infile": "src/Bookstore.Contracts/CHANGELOG.md",
"packageFiles": [],
"bumpFiles": [],
"releaseCommitMessageFormat": "chore(release): contracts {{currentTag}}",
"noBumpWhenEmptyChanges": true,
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance"
},
{
"type": "refactor",
"hidden": true
},
{
"type": "docs",
"hidden": true
},
{
"type": "test",
"hidden": true
},
{
"type": "build",
"hidden": true
},
{
"type": "ci",
"hidden": true
},
{
"type": "chore",
"hidden": true
}
]
}
Monorepo için önemli üç ayar var. tagPrefix, bir önceki yazıda MinVer'e verdiğimiz önekle aynı: araç son sürümü contracts/v* tag'lerinden okuyor, yeni tag'i de aynı biçimde atıyor; MinVer de paketin sürümünü bu tag'den hesaplıyor. path, yalnız Contracts klasörüne dokunan commit'lerin sayılmasını sağlıyor; tahmin bölümündeki feat(api) commit'i bu yüzden dışarıda kaldı. packageFiles ve bumpFiles boş, çünkü sürüm hiçbir dosyada yazılı değil; araç güncel sürümü son tag'den alıyor. noBumpWhenEmptyChanges olmadan ilgili commit olmasa da boş bir patch sürümü üretiliyordu; ilk denemede tam olarak bu oldu.
Gerçek çalıştırma ve ardından paket:
$ npx commit-and-tag-version
✔ created src/Bookstore.Contracts/CHANGELOG.md
✔ outputting changes to src/Bookstore.Contracts/CHANGELOG.md
✔ committing src/Bookstore.Contracts/CHANGELOG.md
✔ tagging release contracts/v1.1.0
ℹ Run `git push --follow-tags origin main` to publish
$ git log --oneline --decorate -1
1e561ba (HEAD -> main, tag: contracts/v1.1.0) chore(release): contracts 1.1.0
$ dotnet pack src/Bookstore.Contracts -c Release -o artifacts
Successfully created package 'artifacts/Bookstore.Contracts.1.1.0.nupkg'.
Release commit'i yalnız CHANGELOG'u değiştiriyor ve tag'i taşıyor; paketin sürümü MinVer'den geliyor. Böylece sürümün yazılı olduğu tek yer tag, ve tag'i tek bir araç atıyor.
Breaking Change'i Önceden Duyurmak: [Obsolete] ve Deprecation Takvimi
1.1.0'daki asıl değişiklik fiyatın para birimiyle birlikte taşınması. decimal Price'ı doğrudan Money'ye çevirmek breaking change olurdu; onun yerine yeni property'yi ekleyip eskisini bir sürüm boyunca deprecated bıraktık:
using System.Globalization;
namespace Bookstore.Contracts;
public sealed record Money(decimal Amount, string Currency)
{
public override string ToString() => $"{Amount.ToString("0.00", CultureInfo.InvariantCulture)} {Currency}";
}
namespace Bookstore.Contracts;
public sealed record BookDto(string Isbn, string Title, string Author, decimal Price)
{
[Obsolete("Use ListPrice, which carries the currency. Price will be removed in 2.0.0.",
DiagnosticId = "BOOK001",
UrlFormat = "https://example.com/bookstore/contracts/deprecations#{0}")]
public decimal Price { get; init; } = Price;
public Money ListPrice { get; init; } = new(Price, "TRY");
}
DiagnosticId, uyarıya kendi kodunu veriyor; kullanıcı BOOK001'i bilinçli olarak bastırabilir ya da TreatWarningsAsErrors ile hataya çevirebilir. UrlFormat, uyarıya geçiş sayfasının adresini ekliyor. Microsoft'un .NET library rehberi de kaldırmayı planladığın üyelerde ObsoleteAttribute kullanmayı ve mesajda ne yapılması gerektiğini söylemeyi öneriyor. Bookstore API'si, fiyat filtresinde eski property'yi okuyor ve build bunu hemen gösteriyor:
$ dotnet build src/Bookstore.Api
src/Bookstore.Api/Program.cs(28,45): warning BOOK001: 'BookDto.Price' is obsolete: 'Use ListPrice, which carries the currency. Price will be removed in 2.0.0.' (https://example.com/bookstore/contracts/deprecations#BOOK001)
Aynı geçiş HTTP contract'ında da görünüyor. Contracts 1.1.0 ile derlenen API, JSON'da hem eski hem yeni alanı döndürüyor; istemciler kendi hızlarında listPrice'a geçebiliyor:
$ curl -s "http://localhost:5000/api/books?maxPrice=300"
[{"isbn":"9780141439587","title":"Emma","author":"Jane Austen","price":185.50,"listPrice":{"amount":185.50,"currency":"TRY"}}]
Deprecation'ın bir takvimi olmalı: hangi sürümde duyuruldu, hangi sürümde ya da hangi tarihte kalkacak. SemVer FAQ'ı (Frequently Asked Questions; sık sorulan sorular) bu konuda, bir özelliği kaldıran major'dan önce en az bir minor'da deprecation'ı yayınlamayı öneriyor. Bookstore'un kuralı VERSIONING.md'de yazılı: bir üye en az bir minor boyunca [Obsolete] taşır, ancak sonra kaldırılır. HTTP API tarafında aynı iletişimin standart yolu Deprecation ve Sunset header'ları; RFC (Request for Comments; internet standartlarını tanımlayan belgeler) 9745 ve RFC 8594'teki bu header'ları API Güvenliği serisinin yanlış yapılandırma ve envanter yazısında ayrıntılı ele almıştık.
2.0.0: Breaking Change'i Commit'te İşaretlemek
Bir minor geçtikten sonra Price'ı kaldırıyoruz. Commit hem ! işaretini hem de footer'ı taşıyor:
$ git commit -m "feat(contracts)!: remove BookDto.Price" \
-m "BREAKING CHANGE: BookDto.Price is removed. Read BookDto.ListPrice.Amount instead; the positional constructor now takes a Money value."
$ npx commit-and-tag-version
✔ outputting changes to src/Bookstore.Contracts/CHANGELOG.md
✔ committing src/Bookstore.Contracts/CHANGELOG.md
✔ tagging release contracts/v2.0.0
ℹ Run `git push --follow-tags origin main` to publish
Üretilen CHANGELOG bölümü:
## [2.0.0](https://github.com/example/bookstore/compare/contracts/v1.1.0...contracts/v2.0.0) (2026-11-14)
### ⚠ BREAKING CHANGES
* **contracts:** BookDto.Price is removed. Read BookDto.ListPrice.Amount instead; the positional constructor now takes a Money value.
### Features
* **contracts:** remove BookDto.Price ([4bb4dfc](https://github.com/example/bookstore/commit/4bb4dfcf2a6ff27574f8094e4313d5b03301e168))
Footer'daki açıklama BREAKING CHANGES başlığının altına taşındı; değişikliğin ne olduğunu ve ne yapılması gerektiğini buradan okuyabiliyorsun. Bir tuhaflık da var: "remove BookDto.Price" satırı "Features" başlığının altında, çünkü commit'in type'ı feat. Conventional Commits'te bir şeyi kaldırmak için ayrı bir type yok. Otomatik CHANGELOG mekanik olarak doğru ama Keep a Changelog'un Removed ve Deprecated başlıklarını üretemiyor. API'nin yeni JSON'unda eski alan artık yok:
[{"isbn":"9780141439587","title":"Emma","author":"Jane Austen","listPrice":{"amount":185.50,"currency":"TRY"}}]
Release Notes: 1.x'ten 2.0'a Geçiş
CHANGELOG'un eksik bıraktığı kısmı release notes tamamlıyor. Bookstore.Contracts 2.0.0 için GitHub Release açıklamasına ya da paket sayfasına koyduğumuz metin:
## [2.0.0] - 2026-11-14
Bookstore.Contracts 2.0 removes the deprecated `BookDto.Price`. Every price now carries a currency.
### Removed
- `BookDto.Price` (deprecated in 1.1.0 as BOOK001). Use `BookDto.ListPrice`.
### Changed
- The `BookDto` constructor takes a `Money` value instead of a `decimal`.
- JSON: the `price` field is gone; read `listPrice.amount` and `listPrice.currency`.
### Migrating from 1.x to 2.0
1. Update to 1.1.0 first and fix every BOOK001 warning.
2. Replace `book.Price` with `book.ListPrice.Amount`.
3. Replace `new BookDto(isbn, title, author, 420.00m)` with
`new BookDto(isbn, title, author, new Money(420.00m, "TRY"))`.
4. API clients: switch from `price` to `listPrice.amount` before the API moves to Contracts 2.0.
### Support
- 1.x gets security fixes only, until 2027-05-14.
Bu metnin hiçbir satırı commit mesajından türetilemezdi. Geçiş adımları sıralı ve kontrol edilebilir: önce 1.1.0'a geç, uyarıları temizle, sonra 2.0'a yükselt. JSON tüketicileri ayrı bir madde olarak yazılı, çünkü onlar C# derleyicisinden uyarı almıyor. Destek bölümü 1.x kullanıcısına ne kadar vakti olduğunu söylüyor; bu politikanın şablonunu serinin son yazısında yazacağız.
GitHub'da Release PR'ı: release-please
commit-and-tag-version yerelde ve herhangi bir CI'da çalışıyor, ama bir insanın onu çalıştırması gerekiyor. GitHub üzerinde daha yaygın yaklaşım release-please: main'e gelen her push'ta commit'leri okuyor ve sürümü, CHANGELOG'u güncelleyen bir "release PR" açıyor ya da günceller. PR merge edilince tag'i ve GitHub Release'i oluşturuyor. Manifest mode, monorepo'da her paketi kendi klasörüyle tanımlıyor:
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"include-v-in-tag": true,
"tag-separator": "/",
"packages": {
"src/Bookstore.Contracts": {
"release-type": "simple",
"component": "contracts",
"package-name": "Bookstore.Contracts"
}
}
}
Güncel sürümler ayrı bir manifest dosyasında:
{
"src/Bookstore.Contracts": "2.0.0"
}
tag-separator ile component ile sürüm arasına / koyarak tag'leri yine contracts/v2.0.0 biçiminde tutuyoruz; MinVer aynı tag'leri okumaya devam ediyor. simple release type'ı, paket klasörüne sürümü yazan bir version.txt tutuyor. Workflow:
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
- run: echo "Released ${{ steps.release.outputs['src/Bookstore.Contracts--tag_name'] }}"
if: ${{ steps.release.outputs['src/Bookstore.Contracts--release_created'] }}
Config ve manifest dosyalarını release-please'in JSON şemasıyla, workflow'u actionlint ile doğruladık; GitHub'da koşulmadı. Dokümanda önemli bir not var: varsayılan GITHUB_TOKEN ile açılan PR'lar başka workflow'ları tetiklemiyor, yani release PR'ında CI kontrolleri koşmuyor. Bunu istiyorsan bir token'ı secret olarak vermen gerekiyor.
İki yaklaşım arasındaki seçim süreçle ilgili. release-please'de sürüm bir PR'la görünür hâle geliyor; release'i merge ile onaylıyorsun ve CHANGELOG'u merge'den önce elle düzeltebiliyorsun. Bedeli GitHub'a bağlı olması ve PR akışının kurulumu. commit-and-tag-version ya da semantic-release gibi araçlar ise CI'da tek adımda release çıkarıyor; daha az tören, ama yayından önce bir insanın CHANGELOG'u görme fırsatı yok. Bookstore'da şimdilik ikinci yolu bir workflow_dispatch workflow'uyla kullanıyoruz:
name: release-contracts
on:
workflow_dispatch:
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- name: Configure Git
run: |
git config user.name "release-bot"
git config user.email "release-bot@example.com"
- name: Bump version, update CHANGELOG, tag
run: npx commit-and-tag-version
- run: git push --follow-tags origin main
Üretimde Dikkat
CHANGELOG'u ve release notes'u ayrı tut. CHANGELOG otomatik ve eksiksiz olmalı; release notes ise seçici ve kullanıcının diliyle yazılmalı. İkisini tek dosyada birleştirmeye çalışınca ya otomasyon bozuluyor ya da metin okunmaz hâle geliyor.
Release commit'i ve tag'i tek yerden at. Yukarıdaki workflow main'e doğrudan push ediyor; main bir ruleset ile korunuyorsa bu push reddedilir. Ya release bot'una bir bypass izni ver ya da PR tabanlı akışa (release-please) geç. İki geliştiricinin aynı anda yerelde release çıkarıp ikisinin de contracts/v1.1.0 tag'ini atması, push'ta ikincisinin reddedilmesiyle biter; release'i CI'a bırakmak bunu önlüyor.
Güvenlik düzeltmelerini görünür yaz. Bir güvenlik açığını kapatan değişiklik fix: olarak CHANGELOG'a girer ama Security başlığı altında görünmez. Release notes'a ayrı bir Security bölümü ekle, varsa CVE (Common Vulnerabilities and Exposures; bilinen güvenlik açıklarına verilen standart kimlik) numarasını ve etkilenen sürüm aralığını yaz.
Deprecation'ı ölç. [Obsolete] uyarısı yalnız derleme yapan kullanıcıya ulaşıyor. HTTP API'de eski alanı hâlâ kullanan istemcileri görmek için log ya da metric'e bak; Contracts 2.0'ı API'ye almadan önce price alanını okuyan istemci kalmadığından emin ol.
Sık Düşülen Tuzaklar
Commit log'unu olduğu gibi release notes diye yayınlamak. "Merge branch 'feature/stock-dto'", "fix typo", "wip" satırlarıyla dolu bir metin kullanıcıya hiçbir şey anlatmıyor; Keep a Changelog'un kaçınılacaklar listesinin ilk maddesi de bu.
Güvenlik düzeltmesini sessiz geçmek. "fix(contracts): validate isbn length" başlıklı bir commit gerçekte bir injection açığını kapatıyorsa ve release notes bunu söylemiyorsa, kullanıcılar güncellemenin aciliyetini anlamaz.
Deprecation'ı yalnız commit gövdesine yazmak. Tahmin bölümünde gördüğümüz gibi otomatik CHANGELOG gövdeyi göstermiyor. Deprecation'ı [Obsolete] ile koda, VERSIONING ya da release notes ile metne yaz.
Breaking change'i ! olmadan commit'lemek. feat(contracts): remove BookDto.Price mesajı minor bir sürüm üretir; kod kırılır, sürüm numarası bunu söylemez. PR başlığını kontrol eden bir kural, en azından "remove" ya da "rename" içeren başlıklarda ! soran bir uyarı, bu hatanın çoğunu yakalıyor.
Monorepo'da path filtresini unutmak. path ayarı olmadan API'deki her feat Contracts'ın minor'ını artırır ve kullanıcılar değişmeyen bir paketin yeni sürümünü indirir.
Küçük Bir Deneme
★☆☆ .versionrc.json'dan path satırını kaldırıp --dry-run çalıştır. feat(api) commit'i CHANGELOG'a giriyor mu, sürüm değişiyor mu?
★★☆ types listesine { "type": "deprecate", "section": "Deprecated" } ekle ve bir deprecate(contracts): ... commit'i at. CHANGELOG'da yeni başlık çıkıyor mu, sürüm artıyor mu? Ekibin bu type'ı kullanmasının bedeli ne olur?
★★★ Bookstore.Contracts'a 2.1.0 için bir Unreleased bölümünü elle yönetmeyi dene: değişiklikleri Keep a Changelog başlıklarıyla CHANGELOG.md'ye yaz ve yayında aracın ürettiği bölümle karşılaştır. Hangi bilgiler yalnız elle yazılan metinde var?
Contracts'ın sürümü, CHANGELOG'u ve release notes'u hazır; geriye paketin kendisi kalıyor. Bir sonraki yazıda dotnet pack ile paket metadata'sını, package validation ile kazara breaking change'i build'de yakalamayı ve nuget.org'a API key yerine trusted publishing ile yayınlamayı ele alıyoruz.