Commit Lint + Semantic Release adalah kombinasi yang efektif untuk mengatasi empat masalah yang sering muncul di tim: format commit yang tidak konsisten, changelog yang ditulis manual, penentuan versi yang berantakan, dan proses rilis yang rawan human error. Dengan aturan commit yang jelas dan pipeline rilis yang otomatis, tim bisa membuat release yang dapat diprediksi tanpa harus mengingat langkah-langkah manual setiap kali merge ke branch utama.

Pendekatannya sederhana: gunakan Conventional Commits sebagai bahasa baku untuk pesan commit, validasi pesan tersebut dengan commitlint dan Husky, lalu biarkan semantic-release menentukan version bump, menghasilkan changelog, membuat git tag, dan membuat GitHub Release secara otomatis dari CI. Hasilnya bukan hanya lebih rapi, tetapi juga lebih aman untuk tim kecil hingga menengah yang ingin merilis lebih sering tanpa menambah beban operasional.

Mengapa alur rilis manual cepat bermasalah

Pada banyak repository, proses rilis biasanya bergantung pada disiplin manual:

  • developer menulis commit dengan format bebas,
  • seseorang mengumpulkan perubahan untuk changelog,
  • versi dinaikkan secara manual,
  • tag dibuat dengan tangan,
  • release notes ditulis ulang di platform seperti GitHub.

Masalahnya, setiap langkah di atas mudah gagal karena hal-hal kecil. Misalnya, commit fix login bug dan bugfix auth sama-sama bermakna, tetapi sulit diproses otomatis. Ketika format commit tidak baku, tool tidak bisa menentukan apakah perubahan tersebut termasuk patch, minor, atau major. Akibatnya, tim kehilangan jejak perubahan yang konsisten.

Di sinilah Conventional Commits menjadi fondasi. Semantic-release tidak membaca niat tim dari commit yang ambigu; ia membaca pola yang jelas. Karena itu, disiplin commit bukan sekadar estetika, tetapi input utama untuk sistem rilis otomatis.

Fondasi: Conventional Commits dan mapping ke version bump

Conventional Commits adalah konvensi penulisan pesan commit dengan struktur umum seperti berikut:

type(scope): subject

Contoh:

feat(auth): tambah login dengan Google
fix(api): perbaiki validasi token kosong
docs(readme): perbarui panduan instalasi

Type commit inilah yang nantinya dibaca semantic-release untuk menentukan kenaikan versi.

Mapping umum commit ke semantic versioning

  • fix -> patch (1.2.3 menjadi 1.2.4)
  • feat -> minor (1.2.3 menjadi 1.3.0)
  • breaking change -> major (1.2.3 menjadi 2.0.0)

Perubahan besar biasanya ditandai dengan salah satu dari dua cara berikut:

feat(api)!: ubah format response user

atau dengan footer:

feat(api): ubah format response user

BREAKING CHANGE: field name diganti menjadi fullName

Beberapa type lain seperti docs, chore, style, test, atau refactor umumnya tidak memicu rilis, kecuali Anda mengubah aturan default semantic-release. Ini penting dipahami sejak awal: tidak semua commit akan menghasilkan versi baru.

Catatan: type commit adalah kontrak antaranggota tim. Jika tim menulis feat untuk perubahan kecil yang sebenarnya bugfix, versi minor akan naik meski tidak semestinya. Tool hanya seakurat input yang diterimanya.

Validasi commit dengan commitlint + Husky

Langkah pertama adalah memastikan commit yang masuk ke repository sudah mengikuti format yang benar. Untuk itu, gunakan commitlint untuk memeriksa pesan commit, dan Husky untuk menjalankannya sebagai hook Git sebelum commit selesai dibuat.

Dependency yang dibutuhkan

Untuk proyek Node.js atau repository yang sudah memiliki package.json, instal paket berikut sebagai dev dependency:

npm install -D @commitlint/cli @commitlint/config-conventional husky semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github

Jika repository bukan aplikasi Node tetapi Anda ingin memakai tool ini di level repo, tetap boleh menambahkan package.json minimal khusus untuk tooling.

Contoh package.json

Berikut contoh konfigurasi yang praktis untuk memulai:

{
  "name": "my-repo",
  "private": true,
  "scripts": {
    "prepare": "husky install",
    "commitlint": "commitlint --edit"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0",
    "@semantic-release/changelog": "^6.0.0",
    "@semantic-release/git": "^10.0.0",
    "@semantic-release/github": "^10.0.0",
    "husky": "^9.0.0",
    "semantic-release": "^24.0.0"
  }
}

Nomor versi di atas hanya contoh pola penulisan. Di implementasi nyata, gunakan versi yang kompatibel dengan environment Anda dan kunci melalui lockfile agar hasil CI konsisten.

Konfigurasi commitlint

Buat file commitlint.config.cjs atau format lain yang didukung:

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

Konfigurasi ini memperluas aturan default Conventional Commits dan mengizinkan type yang umum dipakai di tim. Rule subject-case dimatikan karena dalam praktik, banyak tim menulis subjek commit dengan format campuran yang tetap jelas dibaca.

Menambahkan hook Git dengan Husky

Inisialisasi Husky lalu tambahkan hook commit-msg:

npm run prepare
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Isi file .husky/commit-msg biasanya seperti ini:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit "$1"

Hook ini berjalan saat developer membuat commit. Jika format salah, commit akan ditolak sebelum masuk ke history lokal.

Contoh commit yang lolos dan gagal

Commit yang valid:

feat(auth): tambah endpoint refresh token
fix(ui): perbaiki tombol submit yang tidak aktif

Commit yang akan ditolak:

update login
bug fix
WIP

Ini bekerja karena commitlint memeriksa struktur pesan commit berdasarkan aturan yang sudah ditetapkan. Dengan begitu, tim tidak perlu mengandalkan review manual hanya untuk urusan format.

Mengotomatiskan release dengan semantic-release

Setelah commit punya format yang konsisten, semantic-release bisa mengambil alih proses rilis. Tool ini biasanya berjalan di CI setelah perubahan masuk ke branch rilis, misalnya main. Ia akan:

  • membaca commit sejak tag release terakhir,
  • menentukan jenis kenaikan versi,
  • menghasilkan atau memperbarui changelog,
  • membuat git tag,
  • membuat GitHub Release,
  • dan bila diperlukan, mem-publish artefak ke registry.

Konfigurasi semantic-release

Buat file .releaserc.json seperti berikut:

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

Penjelasan singkat tiap plugin:

  • @semantic-release/commit-analyzer: membaca commit dan menentukan major/minor/patch.
  • @semantic-release/release-notes-generator: menyusun catatan rilis dari commit yang relevan.
  • @semantic-release/changelog: menulis hasilnya ke CHANGELOG.md.
  • @semantic-release/github: membuat release di GitHub dan mengaitkan tag.
  • @semantic-release/git: melakukan commit terhadap file hasil generate seperti changelog.

Jika Anda tidak ingin semantic-release melakukan commit balik ke repository, plugin @semantic-release/git bisa dihilangkan. Beberapa tim memilih pendekatan ini agar branch utama hanya berisi perubahan aplikasi, bukan commit hasil tooling. Trade-off-nya, CHANGELOG.md tidak ikut tersimpan di repo kecuali Anda mengelolanya dengan cara lain.

Bagaimana semantic-release menentukan versi

Semantic-release menganalisis semua commit sejak tag terakhir. Jika ditemukan:

  • minimal satu fix -> patch release,
  • minimal satu feat tanpa breaking change -> minor release,
  • minimal satu breaking change -> major release.

Jika dalam satu rentang commit ada fix dan feat, hasil akhirnya adalah minor, karena level tertinggi yang dipilih. Jika ada breaking change, maka hasilnya major.

Ini menjelaskan mengapa konsistensi penamaan commit sangat penting. Satu commit feat yang sebenarnya hanya refactor kecil dapat menaikkan minor version untuk seluruh release.

Menjalankan semantic-release di GitHub Actions

Tempat yang aman untuk release otomatis adalah CI, bukan laptop developer. Dengan GitHub Actions, Anda bisa memastikan release hanya dibuat setelah commit masuk ke branch yang disepakati.

Contoh workflow CI untuk release

name: Release

on:
  push:
    branches:
      - main

permissions:
  contents: write
  issues: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - name: Install dependencies
        run: npm ci

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

Ada dua detail yang sering terlupakan:

  • fetch-depth: 0 diperlukan agar history dan tag lengkap tersedia. Jika checkout dangkal, semantic-release bisa gagal menemukan tag sebelumnya.
  • GITHUB_TOKEN diperlukan agar workflow bisa membuat tag dan GitHub Release. Pastikan permission workflow cukup untuk menulis ke repository.

Kapan release akan dibuat

Workflow di atas berjalan setiap ada push ke main. Artinya, release terjadi setelah perubahan resmi masuk ke branch utama. Ini umumnya lebih aman dibanding mencoba release dari branch feature, karena branch utama biasanya sudah menjadi sumber kebenaran untuk artefak produksi.

Jika tim menggunakan merge via pull request, maka commit yang sampai ke main harus tetap mengikuti Conventional Commits. Ini berarti strategi merge juga perlu dipikirkan.

Strategi branch dan merge yang aman

Rilis otomatis akan stabil jika branch policy dan gaya merge tim mendukung pembacaan commit yang bersih.

Gunakan satu branch rilis yang jelas

Untuk tim kecil-menengah, pendekatan paling sederhana adalah satu branch rilis, misalnya main. Semantic-release hanya berjalan di branch ini. Branch feature, bugfix, atau eksperimen tidak memicu release.

Keuntungannya:

  • alur lebih mudah dipahami,
  • release hanya keluar dari sumber yang terkontrol,
  • risiko versi loncat akibat branch samping lebih rendah.

Hati-hati dengan squash merge

Jika tim memakai squash and merge, commit final di main biasanya hanya satu commit hasil squash. Artinya, pesan commit hasil squash itulah yang dibaca semantic-release, bukan semua commit di branch feature.

Ini bisa bagus karena history lebih ringkas, tetapi ada syarat penting: judul atau pesan squash commit harus mengikuti Conventional Commits. Jika tidak, semantic-release mungkin menganggap tidak ada perubahan yang layak dirilis.

Pada repository aktif, kebijakan yang praktis adalah:

  • izinkan squash merge,
  • wajibkan judul PR atau squash commit mengikuti Conventional Commits,
  • atau gunakan merge commit jika tim ingin mempertahankan commit individual yang sudah tervalidasi.

Batasi release dari branch yang tidak stabil

Jika Anda punya branch seperti develop, pikirkan baik-baik sebelum mengaktifkan release di sana. Semantic-release memang mendukung beberapa branch dengan channel berbeda, tetapi ini menambah kompleksitas. Untuk awal implementasi, lebih aman fokus pada satu branch release sampai tim benar-benar nyaman dengan alurnya.

Pitfalls umum saat migrasi di repository aktif

Migrasi ke Commit Lint + Semantic Release pada repo yang sudah berjalan hampir selalu menemui kendala. Beberapa masalah berikut paling sering terjadi.

1. History lama tidak mengikuti Conventional Commits

Ini normal. Semantic-release biasanya menghitung dari tag terakhir, bukan dari seluruh sejarah repo. Karena itu, strategi aman saat migrasi adalah membuat titik awal yang jelas:

  • pastikan ada tag rilis terakhir yang valid, atau
  • mulai semantic-release dari kondisi sekarang tanpa mencoba “merapikan” seluruh commit lama.

Jangan memaksa rewrite history di repo aktif kecuali benar-benar perlu, karena risikonya besar untuk kolaborasi tim.

2. Semua commit diberi type chore

Sering terjadi saat developer belum terbiasa. Dampaknya, semantic-release tidak membuat release meski sebenarnya ada perubahan penting. Solusinya bukan sekadar menambah dokumentasi, tetapi memberi contoh nyata type yang digunakan tim:

  • feat untuk fitur baru yang terlihat oleh pengguna atau konsumen API,
  • fix untuk perbaikan bug,
  • refactor untuk perubahan internal tanpa perubahan perilaku,
  • chore untuk pekerjaan maintenance yang memang tidak layak memicu release.

3. Hook lokal lolos, tetapi CI gagal

Ini biasanya terjadi karena environment developer dan CI berbeda, atau karena workflow tidak mengambil tag dan history penuh. Pastikan:

  • npm ci bisa berjalan dari lockfile yang sama,
  • fetch-depth: 0 dipakai di checkout,
  • token dan permission GitHub Actions sudah benar.

4. Release notes tidak sesuai ekspektasi

Biasanya karena pesan commit terlalu umum. Commit seperti fix: update code memang valid secara format, tetapi buruk sebagai dokumentasi perubahan. Commit message harus singkat tetapi tetap spesifik. Semantic-release bisa mengotomatisasi penulisan, tetapi tidak bisa memperbaiki kualitas deskripsi yang kabur.

5. Konflik karena CHANGELOG.md di-commit otomatis

Jika banyak PR terbuka dan semuanya menyentuh changelog manual, konflik merge mudah terjadi. Salah satu keuntungan semantic-release adalah changelog dihasilkan saat release, bukan diedit dari tiap PR. Namun jika Anda memakai plugin git untuk meng-commit CHANGELOG.md, konflik tetap bisa muncul pada situasi tertentu. Jika ini terlalu sering terjadi, pertimbangkan untuk tidak meng-commit changelog ke repo dan cukup mengandalkan GitHub Release notes.

6. Release terpicu oleh commit yang seharusnya tidak ikut produksi

Masalah ini biasanya berasal dari branch policy yang longgar. Jika branch utama menerima merge yang belum siap, semantic-release akan tetap memproses commit itu sebagai kandidat release. Solusinya tetap dasar: lindungi branch rilis dengan review, status check, dan aturan merge yang jelas.

Debugging saat semantic-release tidak membuat rilis

Jika workflow sukses tetapi tidak ada release baru, periksa hal berikut:

  1. Apakah ada commit yang memenuhi syarat?
    Jika semua commit sejak tag terakhir bertipe docs, chore, atau tipe non-release lain, semantic-release bisa memutuskan tidak ada rilis.
  2. Apakah tag sebelumnya terbaca?
    Checkout dangkal atau tag yang tidak sinkron sering membuat analisis commit salah.
  3. Apakah workflow berjalan di branch yang dikonfigurasi?
    Jika .releaserc hanya mengizinkan main, push ke branch lain tidak akan membuat release.
  4. Apakah token punya izin tulis?
    Tanpa izin yang cukup, semantic-release bisa gagal saat membuat tag atau GitHub Release.
  5. Apakah pesan merge commit atau squash commit valid?
    Ini penting jika history di branch utama tidak mempertahankan commit asli dari feature branch.

Untuk investigasi, jalankan semantic-release dalam mode log yang lebih verbose di CI atau lokal pada clone terpisah. Jangan menguji release sungguhan di branch produksi tanpa memahami output analisisnya.

Implementasi bertahap yang realistis untuk tim kecil-menengah

Tim jarang berhasil jika langsung memaksakan seluruh otomatisasi sekaligus. Pendekatan bertahap biasanya lebih aman.

Tahap 1: Standarkan format commit

  • sepakat memakai Conventional Commits,
  • tambahkan commitlint,
  • aktifkan hook Husky di lokal,
  • tambahkan validasi commit juga di CI bila perlu.

Tujuannya adalah membangun kebiasaan dan memperbaiki kualitas history terlebih dahulu.

Tahap 2: Uji semantic-release tanpa publish kompleks

  • konfigurasikan semantic-release untuk branch main,
  • buat GitHub Release dan tag otomatis,
  • opsional: generate CHANGELOG.md.

Pada tahap ini, jangan dulu menambah publish ke registry paket jika belum diperlukan. Fokus pada keandalan alur dasar.

Tahap 3: Rapikan branch policy

  • lindungi branch main,
  • wajibkan pull request review,
  • pastikan squash/merge message mengikuti Conventional Commits,
  • batasi siapa yang boleh merge.

Ini penting agar release otomatis tidak menjadi pintu masuk perubahan yang belum siap.

Tahap 4: Tambahkan publish artefak bila memang ada kebutuhan

Jika repository adalah library atau package, Anda bisa melanjutkan ke publish registry setelah alur tag dan release stabil. Jangan menambah kompleksitas distribusi sebelum dasar versioning dan release notes benar-benar berjalan baik.

Checklist implementasi

Berikut checklist yang bisa langsung dipakai:

  1. Tambahkan package.json untuk tooling jika repo belum punya.
  2. Instal commitlint, @commitlint/config-conventional, husky, dan semantic-release.
  3. Buat commitlint.config.cjs dengan type yang disepakati tim.
  4. Aktifkan hook .husky/commit-msg untuk validasi commit lokal.
  5. Edukasi tim tentang mapping fix, feat, dan BREAKING CHANGE.
  6. Buat konfigurasi .releaserc untuk branch release utama.
  7. Tambahkan workflow GitHub Actions dengan fetch-depth: 0 dan GITHUB_TOKEN.
  8. Pastikan branch policy dan strategi merge mendukung Conventional Commits.
  9. Uji dengan beberapa commit contoh: fix, feat, dan breaking change.
  10. Monitor release pertama dengan hati-hati sebelum menjadikannya alur baku tim.

Jika diterapkan dengan disiplin, kombinasi commitlint, Husky, dan semantic-release akan mengubah release dari proses manual yang rapuh menjadi pipeline yang konsisten, dapat diaudit, dan minim kejutan. Bukan karena semua langkah menjadi “ajaib”, tetapi karena setiap tahap dibuat eksplisit: commit punya struktur, versi ditentukan dari history, dan release dijalankan oleh sistem, bukan ingatan manusia.