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. api ve write_repository kapsamına sahip olmalı. Bu token’ı Masked ve Protected olarak 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: 0 ayarı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‘ın api scope’una sahip olduğundan emin olun. Sadece read_api yeterli değil.
  • Türkçe karakter bozulması: Script’lerde export LANG=tr_TR.UTF-8 veya export LANG=en_US.UTF-8 ekleyin.

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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir