Conventional Commits dan semantic-release adalah kombinasi yang umum dipakai untuk mengotomatiskan proses rilis: menentukan kenaikan versi dari isi commit, membuat changelog, lalu mem-publish release tanpa hitung manual. Untuk tim yang sering merilis perubahan kecil atau bekerja paralel di banyak branch, pendekatan ini membantu mengurangi human error seperti salah bump version, changelog yang tertinggal, atau release yang tidak konsisten.

Dalam praktiknya, otomasi ini hanya efektif jika tim punya aturan commit yang konsisten, validasi yang dijalankan sejak awal, dan pipeline CI yang jelas. Artikel ini membahas implementasi yang realistis: format commit, validasi dengan commitlint, konfigurasi semantic-release, integrasi GitHub Actions untuk branch utama, strategi dry-run di staging, serta jebakan umum seperti squash merge, commit yang tidak konsisten, dan prerelease.

Mengapa Conventional Commits dan semantic-release cocok untuk alur rilis tim

Masalah utama pada proses rilis manual biasanya bukan pada satu langkah besar, melainkan akumulasi detail kecil:

  • versi dinaikkan dengan tipe yang salah,
  • perubahan penting tidak masuk changelog,
  • rilis dibuat dari commit yang belum sesuai standar,
  • maintainer harus menilai satu per satu apakah perubahan termasuk patch, minor, atau major.

Conventional Commits memberi format commit yang bisa diparsing mesin. semantic-release membaca commit tersebut untuk memutuskan apakah rilis berikutnya adalah patch, minor, atau major, lalu menjalankan langkah lanjutan seperti membuat Git tag, GitHub Release, memperbarui changelog, atau publish paket.

Secara umum, pemetaan yang sering dipakai adalah:

  • fix: → patch
  • feat: → minor
  • BREAKING CHANGE: atau tanda breaking change → major

Contoh commit:

feat(auth): tambah refresh token endpoint
fix(api): perbaiki validasi header authorization
feat!: ubah format respons error

BREAKING CHANGE: field error_code diganti menjadi code

Dengan pola ini, tim tidak perlu lagi menebak versi saat release. Keputusan diambil dari histori commit yang sudah distandardisasi.

Dasar adopsi di repository tim

Sebelum menyalakan semantic-release, pastikan repository memenuhi beberapa syarat minimum. Jika tidak, hasilnya biasanya tidak stabil.

1. Aturan branch yang jelas

Tentukan branch mana yang benar-benar dianggap sumber rilis, misalnya main atau master. semantic-release biasanya dijalankan hanya pada branch tersebut agar tidak membuat release dari branch fitur atau branch eksperimen.

2. Commit message harus konsisten

Kalau tim masih bebas menulis commit seperti update, fix bug, atau final, semantic-release tidak bisa menentukan versi dengan benar. Ini sebabnya validasi commit perlu diterapkan, bukan hanya didokumentasikan.

3. Riwayat git harus bisa dibaca CI

Pipeline release memerlukan akses ke tag dan histori commit. Pada CI, kesalahan umum adalah clone dangkal sehingga semantic-release tidak menemukan tag sebelumnya. Di GitHub Actions, gunakan fetch-depth: 0.

4. Token untuk publish release

Jika ingin membuat GitHub Release atau publish paket ke registry, pipeline harus memiliki token yang sesuai. Hak akses token perlu dibatasi secukupnya, tetapi tetap cukup untuk membuat release atau publish artefak.

5. Kesepakatan tipe commit dalam tim

Minimal, dokumentasikan tipe yang dipakai bersama. Misalnya:

  • feat: fitur baru
  • fix: perbaikan bug
  • docs: perubahan dokumentasi
  • refactor: refactor tanpa perubahan perilaku eksternal
  • test: perubahan test
  • chore: maintenance non-fungsional
  • build: perubahan build/dependency
  • ci: perubahan pipeline CI/CD

Tidak semua tipe menghasilkan release. Umumnya hanya feat, fix, dan breaking changes yang memicu bump versi, tergantung konfigurasi.

Standar format Conventional Commits yang sebaiknya dipakai

Format dasarnya:

<type>(<scope>): <subject>

Contoh yang baik:

feat(payments): tambah dukungan virtual account
fix(users): tangani null pada profil
docs(readme): perbarui langkah instalasi
refactor(cache): sederhanakan pembacaan konfigurasi

Jika ada breaking change, Anda bisa menandainya di header:

feat(api)!: ubah struktur payload login

Atau menambah footer:

feat(api): ubah struktur payload login

BREAKING CHANGE: field user diganti menjadi data

Praktik yang memudahkan tim

  • Gunakan scope bila proyek cukup besar. Ini membantu changelog lebih mudah dibaca.
  • Jaga subject singkat dan spesifik. Hindari subjek generik seperti fix issue.
  • Gunakan breaking change secara disiplin. Jika perubahan memaksa pengguna mengubah integrasi, tandai dengan jelas.

Kesalahan umum dalam penulisan commit

  • feat: update → terlalu umum
  • fixed auth → tidak sesuai format
  • chore: upgrade dependency padahal mengubah perilaku publik → tipe salah, versi bisa tidak naik
  • feat untuk perbaikan bug → minor release terpicu padahal seharusnya patch

Validasi commit message dengan commitlint

Dokumentasi saja tidak cukup. Agar format commit benar-benar dipatuhi, validasi harus otomatis. Cara umum adalah memakai commitlint, lalu menghubungkannya ke Git hook agar commit yang tidak valid ditolak sebelum masuk repository.

Contoh konfigurasi commitlint

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'refactor', 'test', 'chore', 'build', 'ci', 'perf', 'revert']
    ]
  }
};

Konfigurasi di atas memakai aturan Conventional Commits yang umum, lalu membatasi tipe commit yang diizinkan.

Menjalankan validasi di lokal

Validasi commit biasanya dipasang lewat Git hooks, misalnya pada hook commit-msg. Implementasinya bisa memakai alat seperti Husky atau mekanisme hook lain yang dipakai tim. Prinsipnya sama: setiap commit diperiksa sebelum disimpan.

npx commitlint --edit $1

Jika tim belum siap memasang validasi di lokal, minimal jalankan commitlint di CI pada pull request. Ini tidak seketat hook lokal, tetapi tetap mencegah commit buruk masuk ke branch utama.

Mengapa commitlint penting meski sudah ada code review

Reviewer manusia sering fokus pada kode, bukan format commit. Akibatnya commit message yang tidak sesuai lolos, lalu semantic-release gagal menentukan versi atau melewatkan release. commitlint menutup celah ini dengan validasi deterministik.

Konfigurasi semantic-release yang ringkas dan praktis

semantic-release membaca histori commit sejak tag release terakhir, menghitung versi berikutnya, membuat catatan rilis, lalu mem-publish hasilnya melalui plugin yang dipilih. Anda tidak perlu menulis versi secara manual di commit release.

Contoh file konfigurasi semantic-release

// .releaserc.json
{
  "branches": [
    "main",
    { "name": "beta", "prerelease": true }
  ],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    [
      "@semantic-release/changelog",
      {
        "changelogFile": "CHANGELOG.md"
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["CHANGELOG.md", "package.json"],
        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
      }
    ],
    "@semantic-release/github"
  ]
}

Penjelasan singkat plugin:

  • @semantic-release/commit-analyzer membaca commit untuk menentukan level release.
  • @semantic-release/release-notes-generator membuat catatan rilis dari commit yang relevan.
  • @semantic-release/changelog memperbarui file CHANGELOG.md.
  • @semantic-release/git melakukan commit terhadap file yang diperbarui, misalnya changelog.
  • @semantic-release/github membuat GitHub Release.

Jika proyek Anda adalah paket yang dipublish ke registry, tambahkan plugin publish yang sesuai untuk ekosistem tersebut. Jika tidak, konfigurasi di atas sudah cukup untuk mengelola tag, changelog, dan GitHub Release.

Mengapa semantic-release bekerja

semantic-release tidak menebak dari nama branch fitur atau label pull request. Ia membaca commit sejak release terakhir, lalu menerapkan aturan analisis commit. Dengan demikian, sumber kebenaran ada di histori git, bukan pada spreadsheet release atau ingatan maintainer.

Integrasi GitHub Actions untuk trigger setelah merge ke branch utama

Tempat paling aman menjalankan semantic-release adalah di CI setelah perubahan sudah masuk ke branch utama. Ini mencegah release dibuat dari branch yang belum final.

Workflow GitHub Actions

name: release

on:
  push:
    branches:
      - main
      - beta

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      issues: write
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 'lts/*'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx semantic-release

Ada beberapa detail penting pada workflow ini:

  • Trigger pada push ke branch utama: artinya release terjadi setelah merge selesai.
  • fetch-depth: 0: histori dan tag lengkap diperlukan untuk menghitung release berikutnya.
  • Test dijalankan sebelum release: jangan publish release jika build atau test gagal.
  • Permission ditetapkan eksplisit: membantu menghindari kegagalan saat membuat release.

Apakah lebih baik trigger di pull_request atau push?

Untuk release final, push ke branch utama biasanya pilihan yang tepat. Trigger pada pull_request cocok untuk validasi, bukan publish. Jika semantic-release dijalankan terlalu awal, Anda berisiko membuat release dari kode yang belum benar-benar masuk ke branch produksi.

Strategi dry-run di staging sebelum produksi

Sebelum mengaktifkan release otomatis di branch produksi, lakukan simulasi di staging atau branch uji. Tujuannya bukan sekadar melihat apakah command berhasil, tetapi memverifikasi bahwa:

  • commit yang ada menghasilkan bump versi yang benar,
  • changelog terbentuk sesuai ekspektasi,
  • token CI memiliki izin yang cukup,
  • workflow hanya berjalan pada branch yang dimaksud.

Cara menjalankan dry-run

semantic-release menyediakan mode simulasi yang menampilkan apa yang akan dilakukan tanpa benar-benar membuat release. Ini cocok untuk branch staging atau untuk pengujian awal di CI.

npx semantic-release --dry-run

Anda bisa menambahkan workflow terpisah untuk branch staging:

name: release-dry-run

on:
  push:
    branches:
      - staging

jobs:
  dry-run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 'lts/*'
      - run: npm ci
      - run: npx semantic-release --dry-run

Apa yang perlu diperiksa saat dry-run

  • Apakah semantic-release mendeteksi release patch, minor, atau major sesuai commit yang ada?
  • Apakah commit seperti docs atau chore tidak memicu release jika memang itu yang diinginkan?
  • Apakah breaking change benar-benar terdeteksi?
  • Apakah branch prerelease seperti beta diperlakukan berbeda dari main?

Dry-run sebaiknya dilakukan beberapa kali dengan skenario commit yang berbeda sebelum pipeline produksi diaktifkan penuh.

Handling prerelease untuk branch beta atau candidate

Tidak semua tim ingin setiap fitur baru langsung menjadi release stabil. Jika Anda punya branch seperti beta, next, atau rc, semantic-release dapat dipakai untuk menghasilkan versi prerelease.

Pada contoh konfigurasi sebelumnya:

{ "name": "beta", "prerelease": true }

Artinya branch beta akan menghasilkan versi prerelease, bukan release stabil. Ini berguna untuk:

  • uji internal sebelum merge ke main,
  • distribusi kandidat rilis ke QA,
  • menguji perubahan breaking besar tanpa langsung merilis stabil.

Hal yang perlu diwaspadai pada prerelease

  • Jangan campur branch prerelease dan stable tanpa aturan merge yang jelas.
  • Pastikan konsumen internal paham bahwa versi beta bukan untuk produksi.
  • Jika publish ke registry, atur channel atau dist-tag sesuai kebutuhan agar versi prerelease tidak menjadi default.

Jebakan umum saat adopsi di tim

1. Squash merge mengubah histori commit

Ini salah satu sumber masalah paling sering. Jika tim menggunakan squash merge, semantic-release biasanya hanya melihat satu commit hasil squash, bukan seluruh commit dalam pull request. Artinya, yang penting bukan commit individual di branch fitur, melainkan pesan commit hasil squash.

Jika pesan squash ditulis asal-asalan seperti update feature branch, maka release bisa tidak terpicu atau bump versinya salah.

Solusinya:

  • wajibkan format Conventional Commits pada pesan squash merge,
  • atau hindari squash merge jika tim ingin mempertahankan commit granular yang sudah rapi,
  • tambahkan panduan untuk editor merge di platform Git hosting.

2. Commit tidak konsisten antar anggota tim

Jika sebagian tim memakai feat dan sebagian lagi menulis bebas, hasil release menjadi tidak dapat diprediksi. commitlint membantu, tetapi tim tetap perlu memahami makna tiap tipe commit.

3. Salah menandai breaking change

Perubahan yang merusak kompatibilitas publik tetapi ditulis sebagai fix atau feat biasa akan menghasilkan versi yang terlalu rendah. Akibatnya pengguna bisa menerima perubahan breaking tanpa kenaikan major version.

4. CI tidak mengambil seluruh tag

Jika checkout dangkal dipakai, semantic-release bisa gagal menentukan release terakhir atau menganggap seluruh histori sebagai perubahan baru. Gejalanya sering berupa versi yang tidak sesuai atau error terkait tag.

5. Mencampur commit maintenance dan fitur besar dalam satu commit

Satu commit yang terlalu banyak isi menyulitkan klasifikasi. Pisahkan perubahan besar agar histori release tetap bermakna dan changelog mudah dibaca.

6. Mengandalkan changelog manual bersamaan dengan changelog otomatis

Jika tim masih mengedit changelog manual tanpa aturan jelas, konflik merge dan inkonsistensi mudah terjadi. Tentukan apakah changelog sepenuhnya otomatis atau hanya ada bagian tertentu yang ditulis manual.

Tips debugging saat release tidak berjalan sesuai harapan

  • Periksa log CI secara lengkap: semantic-release biasanya menjelaskan mengapa tidak ada release yang dibuat.
  • Pastikan branch cocok dengan konfigurasi: jika branch tidak terdaftar, release akan dilewati.
  • Cek format commit sejak tag terakhir: satu commit valid di masa lalu tidak cukup; semantic-release menganalisis rentang commit terbaru.
  • Verifikasi token dan permission: khususnya jika GitHub Release gagal dibuat.
  • Jalankan dry-run secara lokal atau di staging: ini membantu melihat keputusan release tanpa efek samping publish.
  • Periksa pesan squash merge: sering kali di sinilah akar masalah sebenarnya.

Jika semantic-release mengatakan tidak ada release baru, itu belum tentu error. Bisa jadi memang tidak ada commit yang termasuk tipe pemicu release sejak tag terakhir.

Trade-off dan batasan yang perlu dipahami

Pendekatan ini sangat berguna, tetapi bukan tanpa kompromi.

  • Disiplin commit menjadi syarat utama: tanpa itu, hasil otomasi akan salah.
  • Perlu sedikit overhead awal: tim harus belajar format commit, menyiapkan hook, dan menyesuaikan workflow CI.
  • Tidak semua jenis perubahan mudah dipetakan: kadang perubahan internal besar tidak memengaruhi publik API, sehingga tipe commit harus dipilih dengan hati-hati.
  • Release notes otomatis tetap perlu ditinjau: bagus untuk konsistensi, tetapi belum tentu cukup naratif untuk pengumuman produk atau migrasi kompleks.

Meski demikian, untuk banyak tim engineering, trade-off ini layak karena mengurangi pekerjaan manual yang berulang dan rawan salah.

Checklist implementasi

  1. Tentukan branch rilis utama, misalnya main.
  2. Sepakati tipe commit yang dipakai tim.
  3. Tambahkan dokumentasi singkat Conventional Commits di repository.
  4. Pasang commitlint untuk validasi commit message.
  5. Tambahkan validasi di hook lokal atau minimal di CI pull request.
  6. Konfigurasikan semantic-release dengan plugin yang dibutuhkan.
  7. Pastikan CI mengambil histori dan tag penuh dengan fetch-depth: 0.
  8. Jalankan test sebelum langkah release.
  9. Siapkan token dan permission untuk membuat release atau publish artefak.
  10. Uji --dry-run di branch staging.
  11. Definisikan strategi prerelease jika ada branch beta atau sejenisnya.
  12. Putuskan aturan squash merge dan pastikan pesan merge tetap mengikuti Conventional Commits.

Penutup

Conventional Commits dan semantic-release untuk rilis otomatis bukan sekadar alat bantu CI, tetapi mekanisme untuk menjadikan proses release lebih konsisten, bisa diaudit, dan tidak tergantung pada satu maintainer. Kunci keberhasilannya ada pada disiplin format commit, validasi sejak awal dengan commitlint, serta workflow CI yang hanya merilis dari branch yang memang disepakati.

Jika Anda menerapkannya bertahap, mulailah dari standarisasi commit dan dry-run di staging. Setelah pola commit tim stabil dan hasil simulasi sesuai harapan, baru aktifkan publish release otomatis di branch produksi. Pendekatan ini jauh lebih aman daripada langsung mengotomatiskan release pada repository yang histori commit-nya masih berantakan.