Skip to content
Santekno.com | Level Up Your Engineering Skills
ID
📖 0%
21 Oct 2026 · 14 mnt baca ·Artikel 71 / 208
Go

Auto Changelog Golang CI: AI Generate Release Notes dari Commit Messages

Otomatisasi pembuatan changelog dan release notes untuk Go project menggunakan AI. Dari commit messages ke CHANGELOG.md yang informatif dengan GitHub Actions.

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

Auto Changelog Golang CI: AI Generate Release Notes dari Commit

Auto changelog untuk Go project dengan AI mengubah dokumen yang paling sering diabaikan menjadi aset yang selalu up-to-date. Release notes adalah dokumen yang jarang ditulis tapi paling dicari saat ada masalah: “Apa yang berubah di release kemarin?” adalah pertanyaan klasik setelah production incident. Dengan Conventional Commits sebagai fondasi dan AI sebagai penulis, kita bisa generate release notes yang informatif secara otomatis — dari commit messages ke dokumen yang bisa dibaca developer, QA, bahkan product manager.

Di artikel ini kita rangkai pipeline lengkapnya di GitHub Actions: format commit, generator berbasis Go, semantic version auto-bump, sampai notifikasi Slack.


11.1 Conventional Commits: Fondasi untuk Auto-Changelog

Automation changelog hanya sebaik commit message yang diparsnya. Conventional Commits adalah format yang membuat setiap commit bisa dikategorikan mesin. Referensi berikut memetakan tipe commit ke dampak version bump-nya.

text
 1Format: <type>(<scope>): <description>
 2
 3Types:
 4  feat:     new feature (triggers MINOR version bump)
 5  fix:      bug fix (triggers PATCH version bump)
 6  chore:    maintenance (no version bump)
 7  refactor: code refactoring (no version bump)
 8  perf:     performance improvement (triggers PATCH)
 9  security: security fix (triggers PATCH, highlighted)
10  BREAKING CHANGE: breaks existing API (triggers MAJOR)
11
12Contoh commits yang baik:
13  feat(order): implement cancel order with stock restore
14  fix(payment): handle Midtrans callback timeout correctly
15  security(auth): rotate JWT secret rotation mechanism
16  BREAKING CHANGE: rename OrderStatus.Pending to OrderStatus.PENDING
17
18Tanpa Conventional Commits:
19  "fix bug" / "update stuff" / "changes"
20  ← tidak bisa di-parse oleh AI dengan akurat

Perhatikan hubungan langsung antara tipe commit dan version bump — inilah yang nanti membuat semantic versioning bisa otomatis. Tanpa disiplin format ini, seluruh pipeline runtuh menjadi daftar hash tak bermakna.


11.2 Setup Conventional Commits di Go Project

Format hanya berguna kalau ditegakkan. Konfigurasi commitlint berikut membatasi tipe dan scope yang valid untuk Santekno Shop, lalu dipasang sebagai git hook agar commit non-standar ditolak sebelum masuk.

yaml
 1# .commitlintrc.yml
 2extends: ['@commitlint/config-conventional']
 3
 4rules:
 5  type-enum:
 6    - 2  # error
 7    - always
 8    - [feat, fix, chore, refactor, test, docs, perf, security, ci, build]
 9  scope-enum:
10    - 1  # warning only
11    - always
12    - [order, payment, notification, catalog, user, auth, ci, deps, infra]
13  subject-max-length:
14    - 2
15    - always
16    - 72

Setelah config ada, pasang tooling dan hook-nya agar validasi berjalan otomatis di setiap commit.

bash
1# Install commitlint
2npm install -g @commitlint/cli @commitlint/config-conventional
3
4# Git hook (via husky atau pre-commit):
5# .git/hooks/commit-msg
6commitlint --edit "$1"

Dengan type-enum di level error, commit yang tidak sesuai format akan langsung ditolak — memaksa disiplin sejak titik paling awal. Sekarang commit history kita siap dibaca generator.


11.3 Release Notes Generator Script

Jantung sistem adalah binary Go yang mengambil commit sejak tag terakhir, mem-parse-nya, lalu memilih strategi generasi. Program berikut memakai AI untuk rilis kecil (kualitas) dan template untuk rilis besar (kecepatan), dengan fallback otomatis jika AI gagal.

go
  1// cmd/release-notes/main.go
  2package main
  3
  4import (
  5    "context"
  6    "fmt"
  7    "os"
  8    "os/exec"
  9    "strings"
 10    "text/template"
 11    "time"
 12
 13    anthropic "github.com/anthropics/anthropic-sdk-go"
 14    "github.com/anthropics/anthropic-sdk-go/option"
 15)
 16
 17type ReleaseContext struct {
 18    Version string
 19    Date    string
 20    PrevTag string
 21    Commits []CommitInfo
 22    Stats   ReleaseStats
 23}
 24
 25type CommitInfo struct {
 26    Hash       string
 27    Type       string
 28    Scope      string
 29    Subject    string
 30    IsBreaking bool
 31}
 32
 33type ReleaseStats struct {
 34    Features, Fixes, Security, Breaking, Other int
 35}
 36
 37func main() {
 38    version := os.Args[1] // e.g., "v1.5.0"
 39    prevTag := getPreviousTag()
 40    commits := getCommitsSince(prevTag)
 41    stats := calculateStats(commits)
 42
 43    ctx := ReleaseContext{
 44        Version: version,
 45        Date:    time.Now().Format("2006-01-02"),
 46        PrevTag: prevTag,
 47        Commits: commits,
 48        Stats:   stats,
 49    }
 50
 51    // Pilih strategy berdasarkan jumlah commits
 52    var notes string
 53    var err error
 54    if len(commits) < 50 {
 55        notes, err = generateWithAI(ctx)
 56    } else {
 57        notes, err = generateFromTemplate(ctx)
 58    }
 59    if err != nil {
 60        fmt.Fprintln(os.Stderr, "Error:", err)
 61        notes, _ = generateFromTemplate(ctx) // Fallback ke template
 62    }
 63
 64    fmt.Print(notes)
 65}
 66
 67func getPreviousTag() string {
 68    out, err := exec.Command("git", "describe", "--tags", "--abbrev=0", "HEAD^").Output()
 69    if err != nil {
 70        return "initial"
 71    }
 72    return strings.TrimSpace(string(out))
 73}
 74
 75func getCommitsSince(since string) []CommitInfo {
 76    rangeArg := "HEAD"
 77    if since != "initial" {
 78        rangeArg = since + "..HEAD"
 79    }
 80    out, err := exec.Command("git", "log", rangeArg, "--no-merges", "--format=%h|%s").Output()
 81    if err != nil {
 82        return nil
 83    }
 84
 85    var commits []CommitInfo
 86    for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
 87        parts := strings.SplitN(line, "|", 2)
 88        if len(parts) != 2 {
 89            continue
 90        }
 91        commits = append(commits, parseConventionalCommit(parts[0], parts[1]))
 92    }
 93    return commits
 94}
 95
 96func parseConventionalCommit(hash, subject string) CommitInfo {
 97    isBreaking := strings.Contains(subject, "BREAKING CHANGE")
 98    parts := strings.SplitN(subject, ":", 2)
 99    if len(parts) != 2 {
100        return CommitInfo{Hash: hash, Type: "other", Subject: subject}
101    }
102    typeScope := parts[0]
103    desc := strings.TrimSpace(parts[1])
104
105    commitType, scope := typeScope, ""
106    if idx := strings.Index(typeScope, "("); idx != -1 {
107        commitType = typeScope[:idx]
108        scope = typeScope[idx+1 : len(typeScope)-1]
109    }
110    return CommitInfo{Hash: hash, Type: commitType, Scope: scope, Subject: desc, IsBreaking: isBreaking}
111}
112
113func calculateStats(commits []CommitInfo) ReleaseStats {
114    var s ReleaseStats
115    for _, c := range commits {
116        switch c.Type {
117        case "feat":
118            s.Features++
119        case "fix":
120            s.Fixes++
121        case "security":
122            s.Security++
123        }
124        if c.IsBreaking {
125            s.Breaking++
126        }
127    }
128    s.Other = len(commits) - s.Features - s.Fixes - s.Security
129    return s
130}
131
132func generateWithAI(ctx ReleaseContext) (string, error) {
133    client := anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
134
135    commitList := make([]string, len(ctx.Commits))
136    for i, c := range ctx.Commits {
137        line := fmt.Sprintf("%s: %s", c.Type, c.Subject)
138        if c.Scope != "" {
139            line = fmt.Sprintf("%s (%s): %s", c.Type, c.Scope, c.Subject)
140        }
141        if c.IsBreaking {
142            line += " [BREAKING]"
143        }
144        commitList[i] = line
145    }
146
147    prompt := fmt.Sprintf(`Generate release notes for Santekno Shop Go service.
148
149Version: %s (previous: %s)
150Commits (%d total):
151%s
152
153Stats: %d features, %d fixes, %d security, %d breaking
154
155Write release notes in Bahasa Indonesia with sections:
156## 🚀 What's New in %s
157### ✨ New Features (group feat, explain user impact)
158### 🐛 Bug Fixes (group fix, what was broken)
159### 🔧 Technical Improvements (brief)
160
161Be specific: mention feature names, affected services, user impact.
162Skip test/docs/ci commits unless significant.`,
163        ctx.Version, ctx.PrevTag, len(ctx.Commits), strings.Join(commitList, "\n"),
164        ctx.Stats.Features, ctx.Stats.Fixes, ctx.Stats.Security, ctx.Stats.Breaking,
165        ctx.Version)
166
167    msg, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
168        Model:     anthropic.F(anthropic.ModelClaudeHaiku4_5),
169        MaxTokens: anthropic.F(int64(1200)),
170        Messages: anthropic.F([]anthropic.MessageParam{
171            anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
172        }),
173    })
174    if err != nil {
175        return "", fmt.Errorf("generateWithAI: %w", err)
176    }
177    return msg.Content[0].Text, nil
178}
179
180var releaseTemplate = `## {{ .Version }} ({{ .Date }})
181
182### ✨ New Features
183{{ range .Commits }}{{ if eq .Type "feat" }}- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
184{{ end }}{{ end }}
185### 🐛 Bug Fixes
186{{ range .Commits }}{{ if eq .Type "fix" }}- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
187{{ end }}{{ end }}`
188
189func generateFromTemplate(ctx ReleaseContext) (string, error) {
190    tmpl, err := template.New("release").Parse(releaseTemplate)
191    if err != nil {
192        return "", err
193    }
194    var buf strings.Builder
195    err = tmpl.Execute(&buf, ctx)
196    return buf.String(), err
197}

Dua keputusan arsitektur menonjol di sini: parseConventionalCommit mengubah teks commit jadi data terstruktur yang bisa dihitung, dan strategi dua-jalur (generateWithAI vs generateFromTemplate) memastikan rilis tidak pernah tertunda karena AI API down. Binary ini yang akan dipanggil workflow rilis.


11.4 GitHub Actions: Release Workflow

Generator perlu dipicu saat tag semver di-push. Workflow berikut build binary, generate notes dengan fallback ke git log, membuat GitHub Release, lalu meng-commit CHANGELOG.md yang diperbarui.

yaml
 1# .github/workflows/release.yml
 2name: Create Release with Auto Notes
 3
 4on:
 5  push:
 6    tags:
 7      - 'v[0-9]+.[0-9]+.[0-9]+'  # semver tags: v1.2.3
 8
 9env:
10  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
11
12jobs:
13  create-release:
14    runs-on: ubuntu-latest
15    permissions:
16      contents: write
17    steps:
18      - uses: actions/checkout@v4
19        with:
20          fetch-depth: 0  # Butuh full history untuk git log
21
22      - uses: actions/setup-go@v5
23        with:
24          go-version: '1.22'
25          cache: true
26
27      - name: Build release notes generator
28        run: go build -o /tmp/release-notes ./cmd/release-notes/
29
30      - name: Generate release notes
31        id: notes
32        run: |
33          VERSION="${{ github.ref_name }}"
34          NOTES=$(/tmp/release-notes "$VERSION" 2>&1) || {
35            echo "AI generation failed, using git log fallback"
36            NOTES=$(git log $(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")..HEAD \
37              --no-merges --pretty=format:"- %s (%h)" | head -50)
38          }
39          echo "notes<<EOF" >> $GITHUB_OUTPUT
40          echo "$NOTES" >> $GITHUB_OUTPUT
41          echo "EOF" >> $GITHUB_OUTPUT
42
43      - name: Create GitHub Release
44        uses: actions/github-script@v7
45        with:
46          script: |
47            const tag = context.ref.replace('refs/tags/', '');
48            await github.rest.repos.createRelease({
49              owner: context.repo.owner,
50              repo: context.repo.repo,
51              tag_name: tag,
52              name: `Release ${tag}`,
53              body: `${{ steps.notes.outputs.notes }}`,
54              draft: false,
55              prerelease: tag.includes('-'),
56            });
57
58      - name: Commit CHANGELOG.md
59        run: |
60          git config user.name "github-actions[bot]"
61          git config user.email "github-actions[bot]@users.noreply.github.com"
62          git add CHANGELOG.md
63          git diff --cached --quiet || \
64            git commit -m "chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
65          git push origin HEAD:main

Perhatikan fetch-depth: 0 yang wajib — tanpa full history, git describe tidak bisa menemukan tag sebelumnya. Fallback ke git log juga penting: rilis harus tetap terbit walau AI gagal. Tapi siapa yang menentukan angka versi pada tag itu?


11.5 Semantic Versioning dengan Auto-Bump

Menentukan versi berikutnya secara manual itu rawan lupa dan tidak konsisten. Script berikut menganalisis tipe commit sejak tag terakhir untuk memutuskan major/minor/patch secara otomatis.

bash
 1#!/bin/bash
 2# scripts/bump-version.sh
 3set -euo pipefail
 4
 5CURRENT=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
 6IFS='.' read -r -a PARTS <<< "${CURRENT#v}"
 7MAJOR=${PARTS[0]}; MINOR=${PARTS[1]}; PATCH=${PARTS[2]}
 8
 9COMMITS=$(git log "${CURRENT}..HEAD" --no-merges --pretty=format:"%s")
10BUMP="patch"  # default
11
12echo "$COMMITS" | grep -q "^BREAKING CHANGE\|^feat!" && BUMP="major"
13echo "$COMMITS" | grep -qE "^feat(\(.+\))?:" && [ "$BUMP" != "major" ] && BUMP="minor"
14
15case $BUMP in
16  major) NEXT="v$((MAJOR+1)).0.0" ;;
17  minor) NEXT="v${MAJOR}.$((MINOR+1)).0" ;;
18  patch) NEXT="v${MAJOR}.${MINOR}.$((PATCH+1))" ;;
19esac
20
21echo "version=$NEXT" >> "${GITHUB_OUTPUT:-/dev/stdout}"
22echo "Next version: $NEXT (bump: $BUMP)"

Logika prioritas di sini penting: BREAKING CHANGE menang atas feat, feat menang atas fix. Dengan begitu, aturan semantic versioning ditegakkan mesin — developer tidak perlu ingat menaikkan angka versi. Hasilnya dirapikan ke dalam file changelog.


11.6 Format CHANGELOG.md

Release notes butuh rumah permanen yang mudah dibaca dan diparsing. Format berikut mengikuti standar Keep a Changelog, dengan section beremoji yang konsisten agar tooling maupun manusia sama-sama nyaman.

markdown
 1# Changelog — Santekno Shop Go Service
 2
 3Format: [Keep a Changelog](https://keepachangelog.com)
 4Versioning: [Semantic Versioning](https://semver.org)
 5
 6---
 7
 8## [v1.5.0] — 2026-07-20
 9
10### ✨ New Features
11- **order:** Implement cancel order dengan atomic stock restore
12  Customer sekarang bisa cancel order PENDING dalam 30 menit
13- **payment:** Tambahkan support refund otomatis saat order di-cancel
14
15### 🐛 Bug Fixes
16- **order:** Fix race condition saat concurrent cancel request
17
18### 🔒 Security
19- Upgrade pgx ke v5.6.0 yang fix CVE-2024-xxxxx
20
21### 🔧 Technical
22- Refactor CancelOrderUseCase untuk extract domain logic ke entity methods

Konsistensi section (Features/Fixes/Security/Technical) inilah yang membuat changelog bisa dijadikan sumber audit: pertanyaan “kapan CVE-X di-patch?” bisa dijawab dengan grep. Selanjutnya, kabarkan rilis ini ke tim.


11.7 Slack/Discord Notification untuk Releases

Changelog yang bagus tak berguna kalau tim tidak tahu ada rilis baru. Step berikut mengirim ringkasan release notes ke Slack dengan tombol menuju halaman release.

yaml
 1- name: Notify Slack about release
 2  if: success()
 3  run: |
 4    VERSION="${{ github.ref_name }}"
 5    NOTES=$(head -20 <<< "${{ steps.notes.outputs.notes }}")
 6
 7    curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
 8      -H "Content-Type: application/json" \
 9      -d "$(jq -n \
10        --arg v "$VERSION" --arg n "$NOTES" \
11        --arg url "https://github.com/santekno/santekno-shop/releases/tag/$VERSION" \
12        '{blocks: [
13          {type: "header", text: {type: "plain_text", text: ("🚀 New Release: " + $v)}},
14          {type: "section", text: {type: "mrkdwn", text: $n}},
15          {type: "actions", elements: [{
16            type: "button", text: {type: "plain_text", text: "View Release"}, url: $url
17          }]}
18        ]}'
19      )"

Dengan notifikasi ini, product manager dan QA tahu apa yang berubah tanpa perlu memantau tag di repository. Pola yang sama bisa diperluas ke arsitektur monorepo.


11.8 Changelog untuk Multiple Services di Monorepo

Di monorepo, satu tag bisa mencakup banyak service, jadi kita butuh changelog per service. Script berikut menyaring commit yang menyentuh path service tertentu, lalu memanggil AI untuk merangkumnya.

bash
 1#!/bin/bash
 2# scripts/generate-service-changelog.sh
 3set -euo pipefail
 4
 5SERVICE="${1}"
 6VERSION="${2}"
 7PREV_TAG="${3:-}"
 8
 9# Ambil commit yang menyentuh service ini saja
10if [ -z "$PREV_TAG" ]; then
11  COMMITS=$(git log --no-merges --pretty=format:"%h|%s" -- "services/${SERVICE}/**")
12else
13  COMMITS=$(git log "${PREV_TAG}..HEAD" --no-merges \
14    --pretty=format:"%h|%s" -- "services/${SERVICE}/**")
15fi
16
17[ -z "$COMMITS" ] && { echo "No commits for $SERVICE"; exit 0; }
18
19./scripts/claude-call.sh "Generate changelog untuk service '${SERVICE}':
20$COMMITS
21
22Format: ## ${SERVICE} v${VERSION} dengan sections Features/Fixes/Security" 500

Filter path -- "services/${SERVICE}/**" adalah kuncinya: setiap service mendapat changelog yang hanya berisi perubahan relevan baginya. Sebelum rilis benar-benar terbit, ada baiknya menambah gerbang kualitas.


11.9 Release Quality Check

Rilis sebaiknya tidak dibuat kalau test gagal atau ada bug kritis yang belum tertutup. Job berikut menjalankan pengecekan pra-rilis: semua test lulus dan tidak ada critical bug yang open.

yaml
 1pre-release-check:
 2  runs-on: ubuntu-latest
 3  steps:
 4    - uses: actions/checkout@v4
 5
 6    - name: Verify all tests pass
 7      run: go test -race -count=1 ./...
 8
 9    - name: Verify no open critical bugs
10      uses: actions/github-script@v7
11      with:
12        script: |
13          const { data: issues } = await github.rest.issues.listForRepo({
14            owner: context.repo.owner, repo: context.repo.repo,
15            state: 'open', labels: 'critical,production-bug',
16          });
17          if (issues.length > 0) {
18            core.warning(`⚠️ ${issues.length} critical bug(s) still open before release`);
19          } else {
20            console.log('✅ No critical open bugs');
21          }

Perhatikan penggunaan core.warning alih-alih exit 1 pada cek bug — ini keputusan sadar: tim diberi peringatan, tapi keputusan blokir tetap manusiawi. Terakhir, kita bahas satu peningkatan AI yang sering diremehkan.


11.10 AI Enhancement: Release Notes untuk Non-Technical

Technical changelog bagus untuk developer, tapi membingungkan product manager. Fungsi berikut mengubah changelog teknis menjadi pengumuman yang berpusat pada dampak ke user.

javascript
 1// Generate versi user-friendly dari changelog teknis
 2async function generateUserFacingNotes(technicalChangelog) {
 3  const prompt = `Convert technical changelog to user-friendly Bahasa Indonesia.
 4
 5Technical:
 6${technicalChangelog}
 7
 8Rules:
 9- Target: end users (non-technical)
10- Highlight user impact, not implementation
11- Skip: refactoring, chore, ci, test commits
12- Max: 150 words, bullet points, casual tone
13
14Example:
15  Technical: "feat(order): implement CancelOrderUseCase with atomic stock restore"
16  User-facing: "✨ Kamu sekarang bisa batalkan pesanan yang baru dibuat dalam 30 menit!"`;
17
18  return await callClaude(prompt, 300);
19}

Inti transformasinya ada pada contoh di prompt: dari nama fungsi teknis menjadi kalimat yang dipahami pembeli. Satu commit list yang sama bisa melahirkan dua dokumen untuk dua audiens — cukup ganti prompt-nya.


11.11 Tips & Gotchas

Beberapa pelajaran praktis berikut akan menghindarkanmu dari jebakan paling umum saat menerapkan auto-changelog.

💡 Tip 1: Conventional Commits adalah investasi. Setup commit linting di pre-commit hook. Tanpa format ini, kualitas auto-generated changelog sangat rendah.

💡 Tip 2: Generate dua versi. Technical changelog untuk developer, user-facing notes untuk product manager. Commit sama, audiens berbeda.

💡 Tip 3: AI untuk summarize, template untuk format. AI unggul untuk grouping dan menjelaskan impact; template lebih reliable untuk formatting yang konsisten.

⚠️ Gotcha 1: Merge commits yang cluttered. Selalu pakai --no-merges di git log agar merge commit yang tidak informatif tidak masuk changelog.

⚠️ Gotcha 2: Squash commits. Jika tim pakai squash merge, pesan squash harus informatif karena ia yang jadi satu-satunya sumber.

⚠️ Gotcha 3: fetch-depth: 0 wajib. Tanpa full git history, git describe --tags --abbrev=0 tidak bisa menemukan previous tag.

Gotcha 3 adalah penyebab kegagalan pipeline yang paling membingungkan — script jalan sempurna di lokal tapi gagal di CI hanya karena checkout dangkal.


11.12 Ringkasan

Auto-changelog dan release notes adalah automation yang bernilai tinggi untuk tiga pihak sekaligus: engineering tidak perlu menulis manual, product manager dapat notifikasi otomatis, dan tim compliance mendapat history yang terdokumentasi.

Key enabler: Conventional Commits format + git tags yang konsisten = automation yang powerful.

Investasi: setup commitlint pre-commit hook (30 menit) + latih tim soal format (1 standup). Return: changelog otomatis, version bump otomatis, release notes tanpa effort manual.

Setiap commit dengan format yang benar berkontribusi ke dokumentasi historis yang makin berguna — searchable, terstruktur, dan akurat. Changelog menjadi jurnal pengembangan tim yang terpelihara sendiri.

Di artikel selanjutnya, kita geser fokus ke operasi paling berisiko dalam deployment: validasi database migration sebelum menyentuh production.

Artikel Terkait

💬 Komentar