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.
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.
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 akuratPerhatikan 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.
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 - 72Setelah config ada, pasang tooling dan hook-nya agar validasi berjalan otomatis di setiap commit.
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.
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.
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:mainPerhatikan 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.
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.
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 methodsKonsistensi 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.
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.
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" 500Filter 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.
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.
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.