Git Etiketleme Stratejisi: Sürüm Yönetimi ve Release Otomasyonu

Yazılım geliştirme süreçlerinde en çok ihmal edilen konulardan biri düzgün bir etiketleme stratejisi kurmaktır. “Git’e commit attım, branch’i merge ettim, bitti” mantığıyla çalışan ekiplerin production’da yaşadığı sürüm kargaşasını defalarca gördüm. Hangi commit’in canlıda olduğunu bilmemek, hotfix’i yanlış branch’ten almak ya da changelog oluşturmaya çalışırken saatlerce log karıştırmak… Bunların hepsinin çözümü iyi planlanmış bir Git tag stratejisinde yatıyor. Bu yazıda sıfırdan bir tag stratejisi nasıl kurulur, release otomasyonu nasıl sağlanır, bunları detaylıca ele alacağız.

Git Tag Temelleri: Lightweight vs Annotated

Git’te iki tür tag var ve bu farkı anlamadan strateji kurmak anlamsız.

Lightweight tag sadece bir commit’e işaret eden bir referanstır. Metadata içermez, imzalanamaz, tagger bilgisi tutulmaz.

Annotated tag ise Git veritabanında tam bir nesne olarak saklanır. Tagger adı, email, tarih ve mesaj içerir. GPG ile imzalanabilir.

# Lightweight tag - sadece referans
git tag v1.0.0

# Annotated tag - production için her zaman bu
git tag -a v1.0.0 -m "Release 1.0.0: Initial stable release"

# Tag bilgilerini görüntüle
git show v1.0.0

# Tüm tagları listele
git tag -l

# Pattern ile listele
git tag -l "v1.*"

# Remote'a tag gönder
git push origin v1.0.0

# Tüm tagları remote'a gönder
git push origin --tags

Production release’leri için her zaman annotated tag kullanın. Lightweight tag’leri geçici işaretlemeler için bırakın. CI/CD pipeline’larınızda da annotated tag’leri kontrol etmek daha güvenilir sonuç verir çünkü git describe komutu varsayılan olarak annotated tag’leri baz alır.

Semantic Versioning: Tag İsimlendirme Standardı

SemVer olmadan tag stratejisi kurmak, evin temelini atmadan duvar örmek gibi. MAJOR.MINOR.PATCH formatı artık endüstri standardı.

  • MAJOR: Geriye dönük uyumsuz API değişiklikleri
  • MINOR: Geriye dönük uyumlu yeni özellikler
  • PATCH: Geriye dönük uyumlu hata düzeltmeleri

Bunların yanı sıra pre-release suffix’leri de önemli:

  • v2.1.0-alpha.1: Erken test sürümü
  • v2.1.0-beta.2: Feature-complete ama test aşamasında
  • v2.1.0-rc.1: Release candidate, production’a gitmeden önceki son aşama
# Mevcut versiyonu bul
git describe --tags --abbrev=0

# Son tag'den bu yana kaç commit olduğunu gör
git describe --tags

# Belirli bir commit'e tag at (geçmiş release için)
git tag -a v1.2.3 abc1234 -m "Release 1.2.3"

# Tag mesajını düzenle (önce sil, sonra yeniden oluştur)
git tag -d v1.0.0
git tag -a v1.0.0 -m "Release 1.0.0: Improved stability and performance"
git push origin :refs/tags/v1.0.0
git push origin v1.0.0

Branch Stratejisiyle Tag’i Entegre Etmek

Tag stratejisi branch stratejinizden bağımsız düşünülemez. GitFlow kullananlar için tag’ler genellikle main veya release/* branch’lerinden atılır. Trunk-based development’ta ise doğrudan main üzerinde çalışılır.

Gerçek dünya senaryosu: Bir e-ticaret projesinde çalıştığımızda sprint sonu her iki haftada bir release yapıyorduk. Branch yapımız şöyleydi:

# Release branch oluştur
git checkout -b release/v2.3.0 develop

# Son düzeltmeleri yap, versiyon dosyasını güncelle
echo "2.3.0" > VERSION
git add VERSION
git commit -m "chore: bump version to 2.3.0"

# Main'e merge et
git checkout main
git merge --no-ff release/v2.3.0

# Tag at
git tag -a v2.3.0 -m "Release 2.3.0

Features:
- Yeni ödeme entegrasyonu
- Sepet performans iyileştirmeleri
- Mobil görünüm düzeltmeleri

Bug Fixes:
- Stok güncelleme race condition düzeltildi
- Kupon kodu hesaplama hatası giderildi"

# Develop'a geri merge et
git checkout develop
git merge --no-ff release/v2.3.0

# Remote'a push et
git push origin main develop
git push origin v2.3.0

# Release branch'i sil
git branch -d release/v2.3.0
git push origin --delete release/v2.3.0

Otomatik Versiyon Tespiti ve Tag Oluşturma

Manuel tag atmak hata yapar. Hele birden fazla kişi release yapıyorsa kargaşa kaçınılmaz. Bu yüzden versiyon tespitini ve tag oluşturmayı otomatize etmek şart.

Önce mevcut tag’i okuyup bir sonraki versiyonu hesaplayan basit bir script yazalım:

#!/bin/bash
# scripts/bump-version.sh

set -e

# Son tag'i al
CURRENT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Mevcut versiyon: $CURRENT_TAG"

# v prefix'ini kaldır
VERSION=${CURRENT_TAG#v}

# Major, minor, patch'i ayır
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"

# Bump türü parametreden al
BUMP_TYPE=${1:-patch}

case $BUMP_TYPE in
  major)
    MAJOR=$((MAJOR + 1))
    MINOR=0
    PATCH=0
    ;;
  minor)
    MINOR=$((MINOR + 1))
    PATCH=0
    ;;
  patch)
    PATCH=$((PATCH + 1))
    ;;
  *)
    echo "Hata: Gecerli bump turu: major, minor, patch"
    exit 1
    ;;
esac

NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "Yeni versiyon: $NEW_VERSION"

# Onay iste
read -p "Bu versiyonu tag olarak olusturmak istiyor musunuz? (y/n): " CONFIRM
if [ "$CONFIRM" != "y" ]; then
  echo "Iptal edildi."
  exit 0
fi

# Tag oluştur
git tag -a "$NEW_VERSION" -m "Release $NEW_VERSION"
echo "Tag olusturuldu: $NEW_VERSION"

# Push et
git push origin "$NEW_VERSION"
echo "Tag remote'a gonderildi."

Changelog Otomasyonu: Conventional Commits ile Entegrasyon

Tag stratejisinin en büyük faydalarından biri otomatik changelog üretebilmektir. Bunun için commit mesajlarınızın Conventional Commits formatında olması gerekiyor.

  • feat: Yeni özellik (MINOR bump)
  • fix: Hata düzeltmesi (PATCH bump)
  • feat! veya BREAKING CHANGE: Geriye dönük uyumsuz değişiklik (MAJOR bump)
  • docs: Sadece dokümantasyon
  • chore: Build süreci veya yardımcı araç değişiklikleri
  • perf: Performans iyileştirmesi
  • refactor: Ne hata düzeltmesi ne de özellik olan kod değişikliği
#!/bin/bash
# scripts/generate-changelog.sh

set -e

PREVIOUS_TAG=${1:-$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")}
CURRENT_TAG=${2:-HEAD}

echo "# Changelog"
echo ""
echo "## ${CURRENT_TAG} ($(date +%Y-%m-%d))"
echo ""

if [ -z "$PREVIOUS_TAG" ]; then
  RANGE="$CURRENT_TAG"
else
  RANGE="${PREVIOUS_TAG}..${CURRENT_TAG}"
fi

# Features
FEATURES=$(git log $RANGE --pretty=format:"%s|%h" | grep "^feat" | grep -v "^feat!" || true)
if [ -n "$FEATURES" ]; then
  echo "### Yeni Ozellikler"
  echo ""
  echo "$FEATURES" | while IFS='|' read -r MSG HASH; do
    echo "- ${MSG} (${HASH})"
  done
  echo ""
fi

# Bug fixes
FIXES=$(git log $RANGE --pretty=format:"%s|%h" | grep "^fix" || true)
if [ -n "$FIXES" ]; then
  echo "### Hata Duzeltmeleri"
  echo ""
  echo "$FIXES" | while IFS='|' read -r MSG HASH; do
    echo "- ${MSG} (${HASH})"
  done
  echo ""
fi

# Breaking changes
BREAKING=$(git log $RANGE --pretty=format:"%s|%h" | grep -E "^(feat|fix)!|BREAKING CHANGE" || true)
if [ -n "$BREAKING" ]; then
  echo "### Kirici Degisiklikler"
  echo ""
  echo "$BREAKING" | while IFS='|' read -r MSG HASH; do
    echo "- **BREAKING:** ${MSG} (${HASH})"
  done
  echo ""
fi

# Performance
PERFS=$(git log $RANGE --pretty=format:"%s|%h" | grep "^perf" || true)
if [ -n "$PERFS" ]; then
  echo "### Performans Iyilestirmeleri"
  echo ""
  echo "$PERFS" | while IFS='|' read -r MSG HASH; do
    echo "- ${MSG} (${HASH})"
  done
  echo ""
fi

GitHub Actions ile Tam Otomatik Release Pipeline

Tag stratejisinin tacı, tag push edildiğinde otomatik olarak devreye giren CI/CD pipeline’ı. GitHub Actions ile nasıl yapıldığını görelim:

# .github/workflows/release.yml
name: Release Pipeline

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'
      - 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'

jobs:
  validate-tag:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.extract.outputs.version }}
      is_prerelease: ${{ steps.extract.outputs.is_prerelease }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Tag bilgisini cikart
        id: extract
        run: |
          TAG=${GITHUB_REF#refs/tags/}
          VERSION=${TAG#v}
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          
          if [[ "$TAG" =~ -(alpha|beta|rc). ]]; then
            echo "is_prerelease=true" >> $GITHUB_OUTPUT
          else
            echo "is_prerelease=false" >> $GITHUB_OUTPUT
          fi
          
          echo "Tag: $TAG"
          echo "Version: $VERSION"

  build-and-test:
    needs: validate-tag
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: |
          echo "Building version ${{ needs.validate-tag.outputs.version }}"
          # Buraya gercek build komutlariniz gelecek
          # make build VERSION=${{ needs.validate-tag.outputs.version }}

      - name: Test
        run: |
          echo "Running tests..."
          # make test

  create-release:
    needs: [validate-tag, build-and-test]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Changelog olustur
        id: changelog
        run: |
          PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
          CURRENT_TAG=${GITHUB_REF#refs/tags/}
          
          if [ -n "$PREVIOUS_TAG" ]; then
            RANGE="${PREVIOUS_TAG}..${CURRENT_TAG}"
          else
            RANGE="$CURRENT_TAG"
          fi
          
          CHANGELOG=""
          
          FEATURES=$(git log $RANGE --pretty=format:"- %s (%h)" | grep "^- feat" || true)
          if [ -n "$FEATURES" ]; then
            CHANGELOG="${CHANGELOG}### Yeni Ozelliklern${FEATURES}nn"
          fi
          
          FIXES=$(git log $RANGE --pretty=format:"- %s (%h)" | grep "^- fix" || true)
          if [ -n "$FIXES" ]; then
            CHANGELOG="${CHANGELOG}### Hata Duzeltmelerin${FIXES}nn"
          fi
          
          echo "changelog<<EOF" >> $GITHUB_OUTPUT
          echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: GitHub Release olustur
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ github.ref_name }}
          name: Release ${{ needs.validate-tag.outputs.version }}
          body: ${{ steps.changelog.outputs.changelog }}
          prerelease: ${{ needs.validate-tag.outputs.is_prerelease == 'true' }}
          generate_release_notes: false

GitLab CI ile Tag Tabanlı Deployment

GitLab kullananlar için de benzer bir pipeline yapalım. Bu örnek, tag’in hangi ortama deploy edileceğini de belirliyor:

# .gitlab-ci.yml (ilgili kisim)

variables:
  DEPLOY_ENV: ""

.tag-rules:
  rules:
    - if: '$CI_COMMIT_TAG =~ /^vd+.d+.d+$/'
      variables:
        DEPLOY_ENV: "production"
    - if: '$CI_COMMIT_TAG =~ /^vd+.d+.d+-rc.d+$/'
      variables:
        DEPLOY_ENV: "staging"
    - if: '$CI_COMMIT_TAG =~ /^vd+.d+.d+-(alpha|beta).d+$/'
      variables:
        DEPLOY_ENV: "development"

deploy:
  extends: .tag-rules
  stage: deploy
  script:
    - echo "Deploying ${CI_COMMIT_TAG} to ${DEPLOY_ENV}"
    - |
      if [ "$DEPLOY_ENV" = "production" ]; then
        # Production deployment
        ansible-playbook -i inventory/production deploy.yml 
          --extra-vars "app_version=${CI_COMMIT_TAG}"
      elif [ "$DEPLOY_ENV" = "staging" ]; then
        # Staging deployment
        ansible-playbook -i inventory/staging deploy.yml 
          --extra-vars "app_version=${CI_COMMIT_TAG}"
      else
        echo "Development ortamina deploy ediliyor..."
        kubectl set image deployment/myapp 
          myapp=registry.example.com/myapp:${CI_COMMIT_TAG} 
          --namespace=development
      fi
  environment:
    name: $DEPLOY_ENV
    url: https://${DEPLOY_ENV}.example.com
  only:
    - tags

Tag Güvenliği: GPG ile İmzalama

Production ortamında kimin ne zaman release yaptığını kriptografik olarak doğrulamak istiyorsanız GPG imzalı taglar kullanmalısınız.

# GPG anahtarınızı Git'e tanıtın
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey YOUR_GPG_KEY_ID

# İmzalı tag oluştur
git tag -s v3.0.0 -m "Release 3.0.0: Guvenlik guncellesmesi"

# Tag imzasını doğrula
git tag -v v3.0.0

# Her zaman imzalı tag olusturmak icin global ayar
git config --global tag.gpgSign true

# Birinin tag'ini doğrulamak için önce public key'ini import et
gpg --import colleague-public-key.asc
git tag -v v3.0.0

Hotfix Senaryosu: Production’da Acil Müdahale

Gerçek hayatta en kritik tag senaryosu hotfix’tir. Production’da kritik bir bug var, son release’den bu yana develop’a onlarca commit girmiş, temiz bir hotfix release’i nasıl yaparsınız?

# Mevcut production tag'ini bul
git tag -l --sort=-version:refname | head -5

# Diyelim ki v2.5.1 production'da
# Hotfix branch'i o tag'den aç
git checkout -b hotfix/v2.5.2 v2.5.1

# Düzeltmeyi yap
git add .
git commit -m "fix: kritik odeme islem hatasi giderildi

Production'da belirli kart tipleriyle yapilan odemelerde
transaction ID cekilemiyordu. NULL check eklendi.

Fixes: #342"

# Test et, sorun yoksa main'e merge
git checkout main
git merge --no-ff hotfix/v2.5.2 -m "Merge hotfix/v2.5.2"

# Tag at
git tag -a v2.5.2 -m "Hotfix 2.5.2: Kritik odeme hatasi duzeltildi"

# Develop'a da merge et ki bu fix kaybolmasın
git checkout develop
git merge --no-ff hotfix/v2.5.2 -m "Merge hotfix/v2.5.2 into develop"

# Remote'a gonder
git push origin main develop
git push origin v2.5.2

# Temizlik
git branch -d hotfix/v2.5.2
git push origin --delete hotfix/v2.5.2

# Hangi commit'lerin bu hotfix'e girdiğini raporla
git log v2.5.1..v2.5.2 --oneline

Tag Stratejisi için Takım Kuralları

Teknik altyapıyı kurdunuz, ama takım kuralları olmadan hiçbir strateji tutmaz. Şu kuralları dokümante etmenizi ve herkesin benimsemesini öneririm:

  • Tag’ler sadece CI/CD üzerinden atılır: Manuel tag atmak sadece release manager yetkisindedir, pipeline otomasyonu esas alınır
  • Her tag bir CHANGELOG girişine karşılık gelir: Changelog güncellemeden release yapılmaz
  • Pre-release tag’ler staging’i tetikler, stable tag’ler production’ı: Bu kural pipeline’da enforced edilmeli
  • Tag mesajları Türkçe veya İngilizce tutarlı olmalı: Ekip hangisini seçerse o dilde kalınmalı
  • Hotfix tag’leri için lead onayı zorunlu: Slack veya Teams üzerinden bildirim mekanizması kurulmalı
  • Tag silmek yasaktır: Remote’tan tag silmek production’da hangi sürümün deploy edildiğini belirsizleştirir. Yanlış tag için yeni bir tag açılır
  • Her release’de git describe çıktısı uygulama versiyonu olarak serve edilir: /api/version endpoint’i bu bilgiyi döner

Sonuç

Git tag stratejisi sadece bir git tag -a v1.0.0 komutundan ibaret değil. Doğru yapılandırılmış bir tag stratejisi; ekipler arası koordinasyonu kolaylaştırır, production’da ne olduğunu her an bilmenizi sağlar, hotfix süreçlerini panik yerine prosedüre dönüştürür ve deployment otomasyonunun temel taşı olur.

Özetlemek gerekirse: annotated tag kullanın, SemVer standardına uyun, Conventional Commits ile entegre edin, tag push’larını CI/CD trigger olarak kullanın ve takım kurallarını yazılı hale getirin. Bu beş adımı hayata geçirdiğinizde “hangi versiyon production’da?” sorusunun cevabı her zaman git describe --tags kadar uzakta olacak.

Tag stratejisi bir kez kurulur ama sürekli bakım ister. Her birkaç ayda bir tag geçmişinizi gözden geçirin, otomasyonunuzun hala çalışıp çalışmadığını test edin ve takıma bu sistemin neden var olduğunu anlatmayı ihmal etmeyin. Çünkü anlayarak uygulanan bir strateji, kural koyarak dayatılandan her zaman daha sağlam durur.

Bir yanıt yazın

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