GitHub Actions ile Semantic Versioning ve Otomatik Release Yönetimi
Yazılım geliştirirken “bu sürümü nasıl numaralandıracağız?” sorusu kulağa basit geliyor ama üretim ortamında yanlış yapıldığında ciddi kaoslar yaratıyor. Bir servisi güncelliyorsunuz, bağımlı olan başka bir servis bozuluyor ve “ama biz sadece küçük bir şey değiştirdik” diyorsunuz. İşte tam bu noktada Semantic Versioning devreye giriyor. Bu yazıda SemVer’i GitHub Actions ile nasıl otomatikleştireceğinizi, release süreçlerini nasıl yöneteceğinizi ve bunları gerçek dünya senaryolarıyla nasıl hayata geçireceğinizi ele alacağız.
Semantic Versioning Nedir ve Neden Önemlidir?
Semantic Versioning, yani SemVer, sürüm numaralarını MAJOR.MINOR.PATCH formatında ifade eden bir standarttır. Ama bu sadece bir format meselesi değil, aynı zamanda bir iletişim protokolü.
- MAJOR: Geriye dönük uyumsuz değişiklikler yapıldığında artırılır
- MINOR: Geriye dönük uyumlu yeni özellikler eklendiğinde artırılır
- PATCH: Geriye dönük uyumlu hata düzeltmeleri yapıldığında artırılır
Örneğin 2.4.1 sürümünden 3.0.0‘a geçmek, tüm bağımlı sistemlerin dikkat etmesi gerektiği anlamına gelir. Ama 2.4.1‘den 2.4.2‘ye geçmek güvenle güncellenebilir demektir.
Bu kuralları elle takip etmeye çalışmak, takım büyüdükçe ve commit hızı arttıkça imkansız hale gelir. GitHub Actions ile bu süreci tamamen otomatikleştirebilirsiniz.
Conventional Commits ile Temeli Atmak
Otomatik versiyonlama için önce commit mesajlarını standartlaştırmanız gerekiyor. Conventional Commits standardı tam bu iş için var.
Temel commit türleri şunlar:
- feat: Yeni özellik (MINOR artışını tetikler)
- fix: Hata düzeltmesi (PATCH artışını tetikler)
- feat! veya BREAKING CHANGE: Geriye uyumsuz değişiklik (MAJOR artışını tetikler)
- chore: Rutin görevler, build sistemi değişiklikleri (versiyon artışı yok)
- docs: Sadece dokümantasyon değişiklikleri (versiyon artışı yok)
- refactor: Ne bug fix ne de feature, kod yeniden düzenleme (versiyon artışı yok)
Örnek commit mesajları:
git commit -m "feat: kullanicilara email bildirimi eklendi"
git commit -m "fix: login sayfasindaki null pointer hatasi duzeltildi"
git commit -m "feat!: authentication API tamamen yeniden yazildi"
git commit -m "fix: veritabani baglanti havuzu limiti duzeltildi
BREAKING CHANGE: connection pool konfigurasyonu degisti"
GitHub Actions ile Temel Versiyonlama Pipeline’ı
Şimdi asıl işe gelelim. Aşağıdaki workflow, her main branch’e push yapıldığında otomatik olarak versiyon hesaplayıp tag oluşturuyor.
# .github/workflows/versioning.yml
name: Semantic Versioning
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
version:
name: Versiyonla ve Tag Olustur
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.semver.outputs.new_version }}
version_changed: ${{ steps.semver.outputs.version_changed }}
steps:
- name: Kodu Checkout Et
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Git Gecmisini Getir
run: git fetch --tags origin
- name: Son Versiyonu Bul ve Yeni Versiyonu Hesapla
id: semver
run: |
# Son git tag'ini bul
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Son tag: $LAST_TAG"
# Son tagden bu yana commit mesajlarini al
COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" 2>/dev/null || git log --pretty=format:"%s")
# Versiyon parcalarini ayir
VERSION=${LAST_TAG#v}
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
PATCH=$(echo $VERSION | cut -d. -f3)
# Commit mesajlarina gore versiyon artir
BUMP="none"
if echo "$COMMITS" | grep -qiE "BREAKING CHANGE|^feat!|^fix!|^refactor!"; then
BUMP="major"
elif echo "$COMMITS" | grep -qE "^feat((.+))?:"; then
BUMP="minor"
elif echo "$COMMITS" | grep -qE "^fix((.+))?:|^perf((.+))?:"; then
BUMP="patch"
fi
echo "Versiyon artisi turu: $BUMP"
if [ "$BUMP" = "major" ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [ "$BUMP" = "minor" ]; then
MINOR=$((MINOR + 1))
PATCH=0
elif [ "$BUMP" = "patch" ]; then
PATCH=$((PATCH + 1))
fi
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
if [ "$BUMP" != "none" ]; then
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "version_changed=true" >> $GITHUB_OUTPUT
echo "Yeni versiyon: $NEW_VERSION"
else
echo "new_version=$LAST_TAG" >> $GITHUB_OUTPUT
echo "version_changed=false" >> $GITHUB_OUTPUT
echo "Versiyon degismedi"
fi
- name: Git Tag Olustur
if: steps.semver.outputs.version_changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.semver.outputs.new_version }}
git push origin ${{ steps.semver.outputs.new_version }}
Release Notes Otomatik Oluşturma
Versiyonu etiketlemek yeterli değil. Kullanıcıların ve diğer geliştiricilerin “bu sürümde ne var?” sorusunu yanıtlamak için otomatik changelog oluşturmak şart.
# .github/workflows/release.yml
name: Release Olustur
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: write
jobs:
release:
name: GitHub Release Olustur
runs-on: ubuntu-latest
steps:
- name: Kodu Checkout Et
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Changelog Olustur
id: changelog
run: |
CURRENT_TAG=${GITHUB_REF#refs/tags/}
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${CURRENT_TAG}^ 2>/dev/null || echo "")
echo "## Degisiklikler" > CHANGELOG_TEMP.md
echo "" >> CHANGELOG_TEMP.md
if [ -n "$PREVIOUS_TAG" ]; then
COMMITS=$(git log ${PREVIOUS_TAG}..${CURRENT_TAG} --pretty=format:"%s|%h|%an")
else
COMMITS=$(git log ${CURRENT_TAG} --pretty=format:"%s|%h|%an")
fi
# Kategorilere gore sirala
FEATURES=""
FIXES=""
BREAKING=""
OTHERS=""
while IFS='|' read -r subject hash author; do
if echo "$subject" | grep -qiE "BREAKING CHANGE|^feat!|^fix!"; then
BREAKING="${BREAKING}n- ${subject} (${hash}) - @${author}"
elif echo "$subject" | grep -qE "^feat((.+))?:"; then
FEATURES="${FEATURES}n- ${subject#feat*: } (${hash}) - @${author}"
elif echo "$subject" | grep -qE "^fix((.+))?:"; then
FIXES="${FIXES}n- ${subject#fix*: } (${hash}) - @${author}"
else
OTHERS="${OTHERS}n- ${subject} (${hash}) - @${author}"
fi
done <<< "$COMMITS"
if [ -n "$BREAKING" ]; then
echo "### Kritik Degisiklikler" >> CHANGELOG_TEMP.md
echo -e "$BREAKING" >> CHANGELOG_TEMP.md
echo "" >> CHANGELOG_TEMP.md
fi
if [ -n "$FEATURES" ]; then
echo "### Yeni Ozellikler" >> CHANGELOG_TEMP.md
echo -e "$FEATURES" >> CHANGELOG_TEMP.md
echo "" >> CHANGELOG_TEMP.md
fi
if [ -n "$FIXES" ]; then
echo "### Hata Duzeltmeleri" >> CHANGELOG_TEMP.md
echo -e "$FIXES" >> CHANGELOG_TEMP.md
echo "" >> CHANGELOG_TEMP.md
fi
if [ -n "$OTHERS" ]; then
echo "### Diger Degisiklikler" >> CHANGELOG_TEMP.md
echo -e "$OTHERS" >> CHANGELOG_TEMP.md
fi
echo "changelog_file=CHANGELOG_TEMP.md" >> $GITHUB_OUTPUT
- name: GitHub Release Olustur
uses: softprops/action-gh-release@v2
with:
body_path: ${{ steps.changelog.outputs.changelog_file }}
draft: false
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
Pre-release ve Beta Sürümleri Yönetmek
Gerçek dünya projelerinde sadece stable release yeterli değil. Feature branch’lerinden beta, alpha veya release candidate sürümleri de oluşturmanız gerekiyor.
# .github/workflows/prerelease.yml
name: Pre-release
on:
push:
branches:
- develop
- 'release/**'
- 'feature/**'
permissions:
contents: write
jobs:
prerelease:
name: Pre-release Olustur
runs-on: ubuntu-latest
steps:
- name: Kodu Checkout Et
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Pre-release Tipini Belirle
id: prerelease_type
run: |
BRANCH=${GITHUB_REF#refs/heads/}
echo "Branch: $BRANCH"
if echo "$BRANCH" | grep -q "^release/"; then
PRERELEASE_TYPE="rc"
elif [ "$BRANCH" = "develop" ]; then
PRERELEASE_TYPE="beta"
else
PRERELEASE_TYPE="alpha"
fi
echo "prerelease_type=$PRERELEASE_TYPE" >> $GITHUB_OUTPUT
- name: Pre-release Versiyonu Olustur
id: version
run: |
# Son stable versiyonu bul
LAST_STABLE=$(git tag -l "v[0-9]*.[0-9]*.[0-9]*" |
grep -v -E "alpha|beta|rc" |
sort -V | tail -1 || echo "v0.0.0")
PRERELEASE_TYPE=${{ steps.prerelease_type.outputs.prerelease_type }}
BUILD_NUMBER=${{ github.run_number }}
SHORT_SHA=$(git rev-parse --short HEAD)
# Pre-release versiyonu: v1.2.0-beta.42+abc1234
VERSION="${LAST_STABLE}-${PRERELEASE_TYPE}.${BUILD_NUMBER}+${SHORT_SHA}"
echo "Pre-release versiyonu: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Pre-release Tag ve Release Olustur
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
name: "Pre-release ${{ steps.version.outputs.version }}"
prerelease: true
generate_release_notes: true
token: ${{ secrets.GITHUB_TOKEN }}
Node.js Projesi icin Tam Entegrasyon
Şimdi bir adım ileri gidelim. Node.js projesi için package.json versiyonunu otomatik güncelleyen ve npm’e publish eden tam bir pipeline görelim.
# .github/workflows/npm-release.yml
name: NPM Release
on:
push:
branches:
- main
permissions:
contents: write
packages: write
jobs:
determine-version:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
should_release: ${{ steps.bump.outputs.should_release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Versiyon Hesapla
id: bump
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Mevcut versiyon: $CURRENT_VERSION"
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s")
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
if echo "$COMMITS" | grep -qiE "BREAKING CHANGE|!:"; then
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0
echo "should_release=true" >> $GITHUB_OUTPUT
elif echo "$COMMITS" | grep -qE "^feat"; then
MINOR=$((MINOR + 1)); PATCH=0
echo "should_release=true" >> $GITHUB_OUTPUT
elif echo "$COMMITS" | grep -qE "^fix|^perf"; then
PATCH=$((PATCH + 1))
echo "should_release=true" >> $GITHUB_OUTPUT
else
echo "should_release=false" >> $GITHUB_OUTPUT
fi
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Yeni versiyon: $NEW_VERSION"
release:
needs: determine-version
if: needs.determine-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Bagimliliklari Yukle ve Test Et
run: |
npm ci
npm test
- name: package.json Versiyonu Guncelle
run: |
NEW_VERSION=${{ needs.determine-version.outputs.new_version }}
npm version $NEW_VERSION --no-git-tag-version
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json
git commit -m "chore: versiyon $NEW_VERSION olarak guncellendi [skip ci]"
git push
- name: NPM'e Publish Et
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: GitHub Release Olustur
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.determine-version.outputs.new_version }}
generate_release_notes: true
token: ${{ secrets.GITHUB_TOKEN }}
Docker Image Versiyonlama
Mikro servis mimarilerinde Docker image versiyonlaması özellikle kritik. Şu senaryoyu düşünün: 5 farklı servisiniz var ve hepsinin hangi versiyonunun production’da çalıştığını bilmeniz gerekiyor.
# .github/workflows/docker-release.yml
name: Docker Image Release
on:
push:
tags:
- 'v*.*.*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
docker-build-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Docker Buildx Kur
uses: docker/setup-buildx-action@v3
- name: GitHub Container Registry'e Giris Yap
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Metadata Hazirla
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.title=Uygulama Adi
org.opencontainers.image.description=Servis aciklamasi
org.opencontainers.image.vendor=Sirket Adi
- name: Docker Image Build ve Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
GIT_COMMIT=${{ github.sha }}
Bu pipeline ile bir image push ettiğinizde otomatik olarak şu etiketler oluşuyor:
ghcr.io/org/repo:2.4.1(tam versiyon)ghcr.io/org/repo:2.4(major.minor)ghcr.io/org/repo:2(sadece major)ghcr.io/org/repo:latestghcr.io/org/repo:sha-abc1234
Hotfix Senaryosu
Gece yarısı production’da kritik bir bug çıktı. Develop branch’inde bir sürü yarım kalmış feature var ve bunları release edemezsiniz. İşte bu senaryoda hotfix branch’i kullanıyorsunuz.
# Hotfix branch'i olustur
git checkout -b hotfix/kritik-guvenlik-acigi v2.3.0
# Hatay duzelt
git commit -m "fix: SQL injection acigi kapatildi
Bu acik kullanici giris formunda bulunuyordu ve yetkisiz
erisime izin veriyordu."
# Main'e merge et
git checkout main
git merge --no-ff hotfix/kritik-guvenlik-acigi -m "fix: hotfix/kritik-guvenlik-acigi branch'i merge edildi"
# Develop'a da merge et
git checkout develop
git merge --no-ff hotfix/kritik-guvenlik-acigi
# Hotfix branch'ini temizle
git branch -d hotfix/kritik-guvenlik-acigi
Main’e merge edildiğinde versiyonlama pipeline’ı otomatik olarak v2.3.0‘dan v2.3.1‘e geçecek çünkü commit fix: ile başlıyor.
Branch Koruma Kuralları ve Versiyon Tutarlılığı
Versiyonlama sisteminin sağlıklı çalışması için branch koruma kurallarını da ayarlamanız gerekiyor. Bunu kod olarak da yönetebilirsiniz.
# .github/workflows/pr-validation.yml
name: PR Dogrulama
on:
pull_request:
branches:
- main
- develop
jobs:
validate-commits:
runs-on: ubuntu-latest
name: Commit Mesajlarini Dogrula
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Conventional Commits Kontrol Et
run: |
VALID_PATTERN="^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)((.+))?(!)?: .+"
# PR'daki tum commitleri kontrol et
COMMITS=$(git log origin/${{ github.base_ref }}..HEAD --pretty=format:"%s")
INVALID=false
while IFS= read -r commit; do
if ! echo "$commit" | grep -qE "$VALID_PATTERN"; then
# Merge commitlerini atla
if ! echo "$commit" | grep -qE "^Merge "; then
echo "Gecersiz commit mesaji: '$commit'"
echo "Beklenen format: feat: yeni ozellik eklendi"
INVALID=true
fi
fi
done <<< "$COMMITS"
if [ "$INVALID" = "true" ]; then
echo ""
echo "HATA: Bazi commit mesajlari Conventional Commits standardina uymuyor."
echo "Daha fazla bilgi icin: https://www.conventionalcommits.org"
exit 1
fi
echo "Tum commit mesajlari gecerli."
- name: Versiyon Etkisini Hesapla ve Yorum Birak
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const commits = execSync(
'git log origin/${{ github.base_ref }}..HEAD --pretty=format:"%s"'
).toString().split('n').filter(Boolean);
let impact = 'none';
let emoji = '📝';
for (const commit of commits) {
if (/BREAKING CHANGE|^feat!|^fix!/i.test(commit)) {
impact = 'MAJOR'; emoji = '🚨'; break;
} else if (/^feat/.test(commit) && impact !== 'MAJOR') {
impact = 'MINOR'; emoji = '✨';
} else if (/^fix|^perf/.test(commit) && impact === 'none') {
impact = 'PATCH'; emoji = '🐛';
}
}
const messages = {
'none': 'Bu PR versiyon degisikligine yol acmayacak.',
'PATCH': 'Bu PR bir PATCH versiyonu artisina neden olacak (ornek: v1.2.3 -> v1.2.4)',
'MINOR': 'Bu PR bir MINOR versiyonu artisina neden olacak (ornek: v1.2.3 -> v1.3.0)',
'MAJOR': 'Bu PR bir MAJOR versiyonu artisina neden olacak (ornek: v1.2.3 -> v2.0.0)'
};
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ${emoji} Versiyon Etkisinn${messages[impact]}nn**Etki seviyesi:** `${impact}``
});
Gerçek Dünya Tavsiyeleri
Teoriden pratiğe geçerken karşılaşacağınız bazı durumlar ve çözümleri:
Eski projelerde versiyonlamaya başlamak: v0.0.0 tag’ini elle oluşturun ve pipeline’ı devreye alın. Geçmiş commit’lerinizi yeniden yazmaya çalışmayın, orası derin bir kuyudur.
Monorepo’larda versiyonlama: Her paketin kendi workflow’u olsun ve path filter kullanın. on: push: paths: ['packages/api/**'] gibi konfigürasyonlar ile sadece ilgili servis versiyonlanır.
[skip ci] kullanımı: Versiyon bump commit’lerinin tekrar pipeline tetiklemesini önlemek için bu tag’i kullanın. Aksi halde sonsuz döngüye girersiniz.
Secrets yönetimi: GITHUB_TOKEN çoğu işlem için yeterli, ama git push işlemleri için genellikle PAT_TOKEN (Personal Access Token) oluşturmanız gerekiyor. Repository Settings’ten Actions > General > Workflow permissions bölümünü kontrol edin.
Release branch’leri: Büyük projelerde release/1.5.x gibi branch’ler açıp sadece patch release’ler için buraya commit atabilirsiniz. Bu sayede 1.5.x serisini desteklemeye devam ederken 1.6.x geliştirme yapılır.
Sonuç
Semantic Versioning ile GitHub Actions kombinasyonu, başlangıçta kurulum gerektiriyor ama bir kez çalışır hale geldiğinde inanılmaz bir zaman kazancı sağlıyor. Artık “hangi versiyonu deploy ettik?”, “bu değişiklik breaking mi?” veya “release notes nerede?” gibi sorularla uğraşmıyorsunuz.
Burada anlatılan yaklaşımın özü şu: commit mesajları birer kontrat. Her geliştirici feat: yazarken “bu bir özellik, MINOR artışı yapılacak” diyor. fix!: yazarken “dikkat, eski API değişiyor” diyor. Bu disiplini takıma oturtmak zaman alıyor, ama PR validation job’u ile otomasyonu hızlı yerleşiyor.
Küçük bir projede başlayın. Önce basit versiyonlama ve release oluşturma adımlarını çalıştırın. Sonra Docker image versiyonlamayı, pre-release’leri ve changelog otomasyonunu ekleyin. Her adımda pipeline’ı test edin ve takımınızın alışkanlıklarına göre şekillendirin. Mükemmel pipeline ilk seferde çıkmaz, iterasyonla olgunlaşır.
