GitLab ile Otomatik Sürüm Notları Oluşturma
Sürüm notları yazmak, çoğu geliştirici ekibinin sürekli ertelediği ama müşterilerin ve paydaşların her release sonrası sorduğu şeylerden biridir. “Bu sürümde ne değişti?” sorusunu manuel olarak cevaplamak yerine, GitLab’ın güçlü CI/CD altyapısını kullanarak bu süreci tamamen otomatize edebilirsiniz. Bu yazıda gerçek dünya senaryolarıyla birlikte sıfırdan çalışan bir sürüm notu sistemi kuracağız.
Temel Kavramlar ve Strateji
Otomatik sürüm notları oluşturmak için önce commit mesajlarınızın belirli bir standartta olması gerekiyor. Bu standart Conventional Commits formatıdır. Bu format olmadan, pipeline’ınız hangi commit’in özellik, hangisinin hata düzeltmesi, hangisinin breaking change olduğunu bilemez.
Conventional Commits formatı şöyle çalışır:
- feat: Yeni özellik
- fix: Hata düzeltmesi
- docs: Sadece dokümantasyon değişiklikleri
- chore: Build süreci veya yardımcı araç değişiklikleri
- refactor: Ne hata düzeltmesi ne de özellik ekleyen kod değişikliği
- BREAKING CHANGE: Geriye dönük uyumsuz API değişikliği
Ekibinizi bu standarda alıştırmak başlangıçta biraz zorlayıcı olabilir ama bir kez oturduktan sonra hem commit geçmişi çok daha okunabilir hale gelir hem de otomasyonunuz mükemmel çalışır.
Proje Yapısı
Senaryomuzda bir e-ticaret backend API projesi düşünelim. Ekip her hafta birden fazla deployment yapıyor ve her sürümden sonra müşteriye changelog göndermek zorundalar. Manuel süreç çok zaman alıyor ve bazen değişiklikler atlanıyor.
Proje dizin yapımız şöyle olacak:
.
├── .gitlab-ci.yml
├── scripts/
│ ├── generate-changelog.sh
│ ├── create-release.sh
│ └── notify-teams.sh
├── templates/
│ └── release-notes.md.tpl
└── src/
└── ...
Bu yapıyı oluşturmak için:
mkdir -p scripts templates
touch .gitlab-ci.yml
touch scripts/generate-changelog.sh
touch scripts/create-release.sh
touch scripts/notify-teams.sh
touch templates/release-notes.md.tpl
chmod +x scripts/*.sh
GitLab CI/CD Pipeline Yapılandırması
Ana .gitlab-ci.yml dosyamızı oluşturalım. Bu pipeline’da birden fazla stage olacak ve sürüm notları sadece main branch’e merge yapıldığında veya tag oluşturulduğunda tetiklenecek.
stages:
- test
- build
- version
- release
- notify
variables:
GITLAB_TOKEN: $CI_JOB_TOKEN
RELEASE_BRANCH: "main"
CHANGELOG_FILE: "CHANGELOG.md"
GIT_DEPTH: 0
# Test aşaması - her push'ta çalışır
unit-tests:
stage: test
image: node:18-alpine
script:
- npm ci
- npm test
only:
- merge_requests
- main
- tags
# Versiyon hesaplama
calculate-version:
stage: version
image: alpine:latest
before_script:
- apk add --no-cache git curl jq
script:
- |
# Son tag'i al
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Son versiyon: $LAST_TAG"
# Commit mesajlarını analiz et
FEAT_COUNT=$(git log ${LAST_TAG}..HEAD --oneline --grep="^feat" | wc -l)
FIX_COUNT=$(git log ${LAST_TAG}..HEAD --oneline --grep="^fix" | wc -l)
BREAKING=$(git log ${LAST_TAG}..HEAD --oneline --grep="BREAKING CHANGE" | wc -l)
# Semantic versioning hesapla
MAJOR=$(echo $LAST_TAG | cut -d. -f1 | tr -d 'v')
MINOR=$(echo $LAST_TAG | cut -d. -f2)
PATCH=$(echo $LAST_TAG | cut -d. -f3)
if [ "$BREAKING" -gt "0" ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [ "$FEAT_COUNT" -gt "0" ]; then
MINOR=$((MINOR + 1))
PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "Yeni versiyon: $NEW_VERSION"
echo "NEW_VERSION=$NEW_VERSION" >> version.env
echo "LAST_TAG=$LAST_TAG" >> version.env
artifacts:
reports:
dotenv: version.env
only:
- main
# Changelog oluşturma
generate-changelog:
stage: release
image: alpine:latest
needs:
- job: calculate-version
artifacts: true
before_script:
- apk add --no-cache git
script:
- chmod +x scripts/generate-changelog.sh
- ./scripts/generate-changelog.sh "$LAST_TAG" "$NEW_VERSION"
artifacts:
paths:
- CHANGELOG.md
- release-notes.md
expire_in: 1 week
only:
- main
# GitLab Release oluşturma
create-gitlab-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs:
- job: calculate-version
artifacts: true
- job: generate-changelog
artifacts: true
script:
- echo "GitLab release oluşturuluyor: $NEW_VERSION"
release:
name: "Release $NEW_VERSION"
description: "./release-notes.md"
tag_name: "$NEW_VERSION"
ref: "$CI_COMMIT_SHA"
only:
- main
Changelog Oluşturma Script’i
Bu script’in kalbi, commit mesajlarını parse edip okunabilir bir changelog formatına dönüştürmektir. Gerçek projelerden öğrendiğim bir şey var: Script ne kadar basit ve okunabilir olursa, sonradan bakım o kadar kolay olur.
#!/bin/bash
# scripts/generate-changelog.sh
set -e
LAST_TAG=$1
NEW_VERSION=$2
DATE=$(date +"%Y-%m-%d")
OUTPUT_FILE="release-notes.md"
CHANGELOG="CHANGELOG.md"
echo "Changelog oluşturuluyor: $LAST_TAG -> $NEW_VERSION"
# Geçici dosyalar
TEMP_FEATURES=$(mktemp)
TEMP_FIXES=$(mktemp)
TEMP_DOCS=$(mktemp)
TEMP_BREAKING=$(mktemp)
TEMP_OTHER=$(mktemp)
# Commit mesajlarını kategorize et
git log ${LAST_TAG}..HEAD --pretty=format:"%s|%h|%an" | while IFS='|' read -r msg hash author; do
case "$msg" in
feat(*):*|feat:*)
# Scope varsa çıkar
clean_msg=$(echo "$msg" | sed 's/^feat([^)]*): //' | sed 's/^feat: //')
echo "- $clean_msg (`$hash`) - *$author*" >> $TEMP_FEATURES
;;
fix(*):*|fix:*)
clean_msg=$(echo "$msg" | sed 's/^fix([^)]*): //' | sed 's/^fix: //')
echo "- $clean_msg (`$hash`) - *$author*" >> $TEMP_FIXES
;;
docs(*):*|docs:*)
clean_msg=$(echo "$msg" | sed 's/^docs([^)]*): //' | sed 's/^docs: //')
echo "- $clean_msg (`$hash`) - *$author*" >> $TEMP_DOCS
;;
*"BREAKING CHANGE"*)
echo "- **$msg** (`$hash`) - *$author*" >> $TEMP_BREAKING
;;
chore:*|refactor:*|perf:*)
echo "- $msg (`$hash`) - *$author*" >> $TEMP_OTHER
;;
esac
done
# Release notes dosyasını oluştur
cat > $OUTPUT_FILE << EOF
## $NEW_VERSION ($DATE)
EOF
# Breaking changes varsa en üste ekle
if [ -s "$TEMP_BREAKING" ]; then
cat >> $OUTPUT_FILE << EOF
### Dikkat: Geriye Dönük Uyumsuz Değişiklikler
$(cat $TEMP_BREAKING)
EOF
fi
# Yeni özellikler
if [ -s "$TEMP_FEATURES" ]; then
cat >> $OUTPUT_FILE << EOF
### Yeni Özellikler
$(cat $TEMP_FEATURES)
EOF
fi
# Hata düzeltmeleri
if [ -s "$TEMP_FIXES" ]; then
cat >> $OUTPUT_FILE << EOF
### Hata Düzeltmeleri
$(cat $TEMP_FIXES)
EOF
fi
# Dokümantasyon
if [ -s "$TEMP_DOCS" ]; then
cat >> $OUTPUT_FILE << EOF
### Dokümantasyon
$(cat $TEMP_DOCS)
EOF
fi
# İstatistikler ekle
TOTAL_COMMITS=$(git log ${LAST_TAG}..HEAD --oneline | wc -l)
CONTRIBUTORS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%an" | sort -u | tr 'n' ', ' | sed 's/,$//')
cat >> $OUTPUT_FILE << EOF
---
**Toplam commit:** $TOTAL_COMMITS
**Katkıda bulunanlar:** $CONTRIBUTORS
EOF
# CHANGELOG.md dosyasını güncelle
if [ -f "$CHANGELOG" ]; then
TEMP_COMBINED=$(mktemp)
cat $OUTPUT_FILE > $TEMP_COMBINED
echo "" >> $TEMP_COMBINED
cat $CHANGELOG >> $TEMP_COMBINED
mv $TEMP_COMBINED $CHANGELOG
else
cp $OUTPUT_FILE $CHANGELOG
fi
# Temizlik
rm -f $TEMP_FEATURES $TEMP_FIXES $TEMP_DOCS $TEMP_BREAKING $TEMP_OTHER
echo "Changelog başarıyla oluşturuldu: $OUTPUT_FILE"
cat $OUTPUT_FILE
GitLab API ile Gelişmiş Release Oluşturma
release-cli aracı temel kullanım için yeterli, ama bazen daha fazla kontrole ihtiyaç duyuyorsunuz. Özellikle release’e asset eklemek, belirli milestone’larla ilişkilendirmek veya özel metadata eklemek istediğinizde doğrudan API’yi kullanmak daha mantıklı.
#!/bin/bash
# scripts/create-release.sh
set -e
NEW_VERSION=$1
RELEASE_NOTES_FILE=$2
PROJECT_ID=$CI_PROJECT_ID
GITLAB_API="https://gitlab.com/api/v4"
# Release notes içeriğini oku
RELEASE_BODY=$(cat $RELEASE_NOTES_FILE)
# Önce tag oluştur
echo "Tag oluşturuluyor: $NEW_VERSION"
curl --silent --fail
--header "PRIVATE-TOKEN: $RELEASE_TOKEN"
--header "Content-Type: application/json"
--data "{
"tag_name": "$NEW_VERSION",
"ref": "$CI_COMMIT_SHA",
"message": "Release $NEW_VERSION"
}"
"$GITLAB_API/projects/$PROJECT_ID/repository/tags"
# Release oluştur
echo "Release oluşturuluyor..."
RESPONSE=$(curl --silent --fail
--request POST
--header "PRIVATE-TOKEN: $RELEASE_TOKEN"
--header "Content-Type: application/json"
--data "{
"name": "Release $NEW_VERSION",
"tag_name": "$NEW_VERSION",
"description": $(echo "$RELEASE_BODY" | jq -Rs .),
"milestones": [],
"released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}"
"$GITLAB_API/projects/$PROJECT_ID/releases")
RELEASE_URL=$(echo $RESPONSE | jq -r '._links.self')
echo "Release başarıyla oluşturuldu: $RELEASE_URL"
# Release URL'ini artifact olarak kaydet
echo "RELEASE_URL=$RELEASE_URL" >> release.env
Slack/Teams Bildirimi Entegrasyonu
Sürüm notları oluşturulduktan sonra ekibi ve paydaşları otomatik olarak haberdar etmek, sürecin en kritik parçalarından biri. Özellikle müşteri odaklı projelerde bu adımı atlamamak gerekiyor.
#!/bin/bash
# scripts/notify-teams.sh
set -e
NEW_VERSION=$1
RELEASE_URL=$2
RELEASE_NOTES_FILE=$3
# Slack bildirimi
send_slack_notification() {
local webhook_url=$SLACK_WEBHOOK_URL
local release_notes=$(head -50 $RELEASE_NOTES_FILE)
local payload=$(cat << EOF
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Yeni Sürüm: $NEW_VERSION"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "$release_notes"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Release Notlarını Görüntüle"
},
"url": "$RELEASE_URL"
}
]
}
]
}
EOF
)
curl --silent --fail
--header "Content-Type: application/json"
--data "$payload"
"$webhook_url"
echo "Slack bildirimi gönderildi"
}
# Microsoft Teams bildirimi
send_teams_notification() {
local webhook_url=$TEAMS_WEBHOOK_URL
curl --silent --fail
--header "Content-Type: application/json"
--data "{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": "Yeni Sürüm: $NEW_VERSION",
"sections": [{
"activityTitle": "Yeni Sürüm Yayınlandı: $NEW_VERSION",
"activitySubtitle": "$CI_PROJECT_NAME",
"activityText": "Detaylar için release sayfasını ziyaret edin.",
"facts": [{
"name": "Sürüm",
"value": "$NEW_VERSION"
}, {
"name": "Tarih",
"value": "$(date +%Y-%m-%d)"
}]
}],
"potentialAction": [{
"@type": "OpenUri",
"name": "Release Notları",
"targets": [{"os": "default", "uri": "$RELEASE_URL"}]
}]
}"
"$webhook_url"
echo "Teams bildirimi gönderildi"
}
# Hangi platformların webhook'u tanımlıysa ona gönder
[ -n "$SLACK_WEBHOOK_URL" ] && send_slack_notification
[ -n "$TEAMS_WEBHOOK_URL" ] && send_teams_notification
CHANGELOG.md’yi Git’e Geri Commit Etme
Sürüm notlarının oluşturulmasının ardından CHANGELOG.md dosyasını repository’ye geri commit etmek isteyebilirsiniz. Bu, özellikle projeyi GitHub mirror’ında da tutanlar için önemli. Ama dikkatli olmak gerekiyor; bu işlem yanlış yapıldığında sonsuz pipeline döngüsüne girebilirsiniz.
# .gitlab-ci.yml'e eklenecek job
commit-changelog:
stage: release
image: alpine:latest
needs:
- job: calculate-version
artifacts: true
- job: generate-changelog
artifacts: true
before_script:
- apk add --no-cache git
- git config --global user.email "[email protected]"
- git config --global user.name "CI Release Bot"
script:
- |
# [skip ci] etiketi sonsuz döngüyü önler
git remote set-url origin "https://ci-bot:${RELEASE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
git add CHANGELOG.md
git diff --staged --quiet || git commit -m "chore: $NEW_VERSION changelog guncellendi [skip ci]"
git push origin HEAD:main
only:
- main
GitLab Variables Yapılandırması
Pipeline’ın düzgün çalışması için GitLab projesinde bazı değişkenler tanımlamanız gerekiyor. Bunları Settings > CI/CD > Variables kısmından ekleyebilirsiniz.
Tanımlanması gereken değişkenler:
- RELEASE_TOKEN: GitLab Personal Access Token.
apivewrite_repositorykapsamına sahip olmalı. Bu token’ıMaskedveProtectedolarak işaretleyin. - SLACK_WEBHOOK_URL: Slack incoming webhook URL’i. Slack App’inizden alabilirsiniz.
- TEAMS_WEBHOOK_URL: Microsoft Teams webhook URL’i. Eğer Teams kullanmıyorsanız boş bırakın.
- CI_JOB_TOKEN: Bu GitLab tarafından otomatik sağlanır, elle tanımlamaya gerek yok.
Protected branch kullanıyorsanız değişkenlerinizi de Protected olarak işaretlemeniz gerekiyor, aksi halde main branch’teki pipeline bu değişkenlere erişemez.
Gerçek Dünya Senaryosu: Monorepo Yapısında Sürüm Yönetimi
Birden fazla servisin aynı repository’de bulunduğu monorepo yapılarında her servis için ayrı sürüm takibi yapmanız gerekebilir. Bu senaryoda commit mesajlarına scope ekleyerek hangi değişikliğin hangi servise ait olduğunu belirtebilirsiniz.
#!/bin/bash
# scripts/monorepo-changelog.sh
set -e
LAST_TAG=$1
NEW_VERSION=$2
SERVICE=$3 # api, frontend, worker gibi
echo "## $SERVICE - $NEW_VERSION ($(date +%Y-%m-%d))" > "changelogs/${SERVICE}-${NEW_VERSION}.md"
echo "" >> "changelogs/${SERVICE}-${NEW_VERSION}.md"
# Sadece belirli servise ait commit'leri filtrele
# feat(api): yeni endpoint -> api servisine ait
# feat(frontend): login sayfası -> frontend'e ait
git log ${LAST_TAG}..HEAD --pretty=format:"%s|%h" |
grep "($SERVICE)" |
while IFS='|' read -r msg hash; do
clean_msg=$(echo "$msg" | sed "s/^[a-z]*($SERVICE): //")
commit_type=$(echo "$msg" | grep -oP '^[a-z]+')
case "$commit_type" in
feat)
echo "- [OZELLIK] $clean_msg (`$hash`)" >> "changelogs/${SERVICE}-${NEW_VERSION}.md"
;;
fix)
echo "- [DUZELTME] $clean_msg (`$hash`)" >> "changelogs/${SERVICE}-${NEW_VERSION}.md"
;;
*)
echo "- $clean_msg (`$hash`)" >> "changelogs/${SERVICE}-${NEW_VERSION}.md"
;;
esac
done
echo "Servis changelog oluşturuldu: changelogs/${SERVICE}-${NEW_VERSION}.md"
cat "changelogs/${SERVICE}-${NEW_VERSION}.md"
Pipeline’ı Test Etme
Her şeyi yapılandırdıktan sonra yerel ortamda test etmek için GitLab Runner’ı kullanabilirsiniz. Önce runner’ı yükleyin:
# GitLab Runner kurulumu (Ubuntu/Debian)
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner
# Pipeline'ı yerel olarak çalıştır (dry-run)
gitlab-runner exec docker calculate-version
# Changelog script'ini manuel test et
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
./scripts/generate-changelog.sh "$LAST_TAG" "v1.2.0"
cat release-notes.md
Eğer git log komutu beklediğiniz sonucu vermiyorsa GIT_DEPTH: 0 ayarının pipeline’da aktif olduğundan emin olun. Shallow clone yapıldığında tüm commit geçmişi indirilmez ve changelog eksik oluşur.
Sık Karşılaşılan Sorunlar
Uygulamada en çok şu problemlerle karşılaşılıyor:
- “tag already exists” hatası: Pipeline birden fazla kez tetiklendiğinde aynı tag’i oluşturmaya çalışır. Bunu önlemek için tag oluşturmadan önce mevcut olup olmadığını kontrol edin.
- Boş changelog:
GIT_DEPTH: 0ayarını yapmayı unuttunuzda sadece son commit görünür, changelog boş çıkar. - Sonsuz pipeline döngüsü: CHANGELOG.md commit’inde
[skip ci]etiketini koymayı unutmak en klasik hata. - Token izin hatası:
RELEASE_TOKEN‘ınapiscope’una sahip olduğundan emin olun. Sadeceread_apiyeterli değil. - Türkçe karakter bozulması: Script’lerde
export LANG=tr_TR.UTF-8veyaexport LANG=en_US.UTF-8ekleyin.
Sonuç
GitLab CI/CD ile otomatik sürüm notları oluşturmak, başlangıçta biraz kurulum gerektiriyor ama bir kez çalıştırdıktan sonra ekibinizin saatlerini geri kazandırıyor. En önemli nokta, ekibin commit mesajı standartlarını benimsemesi. Bu olmadan otomasyon işe yaramaz.
Sistem kurulduktan sonra her main branch push’unda şunları otomatik olarak alıyorsunuz: hesaplanmış semantic version, kategorize edilmiş changelog, GitLab release sayfası ve Slack/Teams bildirimi. Müşteri “bu sürümde ne değişti?” diye sorduğunda artık release sayfasının linkini paylaşmak yeterli.
Monorepo kullanıyorsanız scope bazlı filtreleme yaklaşımını kesinlikle uygulamanızı öneririm. Her servis kendi changelog’una sahip olunca deployment kararları da çok daha kolay veriliyor. Küçük bir ekip için bile bu sistem, aylık birkaç saat manuel iş yükünü ortadan kaldırıyor ve daha da önemlisi, hiçbir değişikliğin gözden kaçmamasını sağlıyor.
