Artefak bernama sapa-final-fix2.zip yang dikirim lewat pesan singkat bukanlah rilis — itu kiriman yang tidak bisa dilacak, tidak bisa diverifikasi, dan tidak bisa dibatalkan. Rilis yang sehat punya tiga sifat: bernomor versi yang bermakna, berisi artefak yang bisa diperiksa keasliannya, dan disertai catatan perubahan yang menjelaskan apa saja yang berbeda dari versi sebelumnya. Chapter ini menerapkan ketiganya untuk sapa: penomoran Semantic Versioning, tag git sebagai pemicu rilis, checksum SHA-256 sebagai bukti keaslian artefak, changelog dengan format standar, dan sekilas otomatisasi dengan GoReleaser.
Semantic Versioning #
Semantic Versioning (semver) menomori rilis dengan tiga angka: MAJOR.MINOR.PATCH. Naikkan PATCH untuk perbaikan cacat yang kompatibel, MINOR untuk fitur baru yang tetap kompatibel, dan MAJOR untuk perubahan yang merusak kompatibilitas (breaking change). Nomor ini bukan hiasan — orchestrator, migrator database, dan tim lain membacanya untuk memutuskan seberapa hati-hati upgrade dilakukan.
Perbandingan dua versi punya jebakan klasik: sebagai string, "1.10.0" dianggap lebih kecil daripada "1.9.0" karena karakter '1' < '9'. Program release-demo.go pada bagian bawah halaman membandingkan versi dengan mengurai komponennya menjadi angka, lalu menyiapkan checksum artefak rilis:
package main
import (
"crypto/sha256"
"fmt"
"strconv"
"strings"
)
// versi menyimpan komponen semver: MAJOR.MINOR.PATCH.
type versi struct {
mayor int
minor int
patch int
}
// urai mengubah string semver menjadi struct versi.
func urai(s string) (versi, error) {
bagian := strings.SplitN(s, ".", 3)
if len(bagian) != 3 {
return versi{}, fmt.Errorf("format salah: %q", s)
}
var angka [3]int
for i, b := range bagian {
n, err := strconv.Atoi(b)
if err != nil {
return versi{}, fmt.Errorf("komponen %q bukan angka", b)
}
angka[i] = n
}
return versi{angka[0], angka[1], angka[2]}, nil
}
// bandingkan mengembalikan -1, 0, atau 1 seperti strings.Compare.
func bandingkan(a, b versi) int {
if a.mayor != b.mayor {
if a.mayor < b.mayor {
return -1
}
return 1
}
if a.minor != b.minor {
if a.minor < b.minor {
return -1
}
return 1
}
if a.patch != b.patch {
if a.patch < b.patch {
return -1
}
return 1
}
return 0
}
// artefak adalah satu berkas rilis dengan isi representatifnya.
type artefak struct {
nama string
isi string
}
func main() {
// Jebakan klasik: perbandingan string menganggap 1.10.0 lebih tua dari 1.9.0.
fmt.Println(`perbandingan string "1.10.0" < "1.9.0":`, "1.10.0" < "1.9.0")
pasangan := [][2]string{
{"1.2.3", "1.2.3"},
{"1.10.0", "1.9.0"},
{"2.0.0", "1.99.9"},
}
for _, p := range pasangan {
a, errA := urai(p[0])
b, errB := urai(p[1])
if errA != nil || errB != nil {
fmt.Println("gagal mengurai pasangan", p[0], p[1])
continue
}
tanda := "="
switch bandingkan(a, b) {
case -1:
tanda = "<"
case 1:
tanda = ">"
}
fmt.Printf("perbandingan semver %s %s %s\n", p[0], tanda, p[1])
}
// Checksum tiap artefak rilis — isi sama menghasilkan checksum sama.
kumpulan := []artefak{
{"sapa-v1.2.3-linux-amd64", "binary linux/amd64 untuk sapa v1.2.3"},
{"sapa-v1.2.3-linux-arm64", "binary linux/arm64 untuk sapa v1.2.3"},
{"sapa-v1.2.3-darwin-arm64", "binary darwin/arm64 untuk sapa v1.2.3"},
}
fmt.Println("SHA256SUMS:")
for _, a := range kumpulan {
sum := sha256.Sum256([]byte(a.isi))
fmt.Printf("%x %s\n", sum, a.nama)
}
}Output yang Dihasilkan:
perbandingan string "1.10.0" < "1.9.0": true
perbandingan semver 1.2.3 = 1.2.3
perbandingan semver 1.10.0 > 1.9.0
perbandingan semver 2.0.0 > 1.99.9
SHA256SUMS:
09ed19c76e0e69d33e79da03bb0b36dbf334e7aed8776b4db62f5cd4916889b8 sapa-v1.2.3-linux-amd64
de027d21982b556392a0188e523439d26e867910b38c930d4510084a97bc7cf2 sapa-v1.2.3-linux-arm64
4a2c11ee4d1b5d800b6512218222704048d67ea3de3cd585c119214566e6c975 sapa-v1.2.3-darwin-arm64Mari kita telaah bagian penting program di atas:
- Baris 16–30 (
urai): memecah string pada tanda titik maksimal tiga bagian, lalu mengubah tiap komponen menjadi angka lewatstrconv.Atoi— komponen non-angka (mis.1.2.3-rc.1) dilaporkan sebagai galat, bukan ditebak. - Baris 33–55 (
bandingkan): membandingkanmayordulu, laluminor, lalupatch— urutan yang membuat1.10.0benar dianggap lebih baru dari1.9.0. - Baris 76 (
fmt.Printlnperbandingan string): membuktikan jebakannya — hasiltruetersebut salah secara semver, dan menjadi alasanura+bandingkanwajib ada. - Baris 97–101 (
sha256.Sum256): menghitung checksum 256-bit dari isi artefak;%xmencetaknya sebagai 64 karakter heksadesimal — dua baris checksum identik berarti isi identik, properti yang dijamin kriptografi hash.
Git Tag sebagai Pemicu Rilis #
Versi lahir sebagai tag git, bukan konstanta di source — nilai versi disuntikkan saat build lewat -ldflags "-X main.version=..." (chapter Build Flags
). Tag beranotasi menyimpan penanda, tanggal, dan pesan; tag ringan (lightweight) hanya penunjuk commit:
git tag -a v1.2.3 -m "Rilis 1.2.3"
git tag -l
git cat-file -t v1.2.3
git push origin v1.2.3Output yang Dihasilkan (tiga perintah pertama):
v1.2.3
tagObjek bertipe tag menandakan tag beranotasi — git show v1.2.3 menampilkan penanda, tanggal, pesan, dan commit yang ditunjuknya. Pipa CI modern memantau tag berbentuk v*: saat git push origin v1.2.3 terjadi, pipeline membaca tag, membangun matriks target dari chapter Cross Compilation
dengan versi yang disuntik, dan melampirkan artefak ke halaman rilis.
Artefak dan Checksum #
Setiap artefak diberi nama yang memuat aplikasi, versi, dan platform — sapa-v1.2.3-linux-amd64, sapa-v1.2.3-linux-arm64, sapa-v1.2.3-darwin-arm64 — sehingga berkas yang diunduh bisa dibedakan tanpa membuka isinya. Bersama artefak, rilis menyertakan berkas SHA256SUMS berisi checksum tiap berkas. Praktik nyatanya pada binary hasil build kita:
cp sapa-server sapa-v1.2.3-linux-amd64
sha256sum sapa-v1.2.3-linux-amd64 > SHA256SUMS
sha256sum -c SHA256SUMSOutput yang Dihasilkan:
sapa-v1.2.3-linux-amd64: OKPerintah -c membaca ulang checksum dari berkas, menghitung ulang checksum berkas aktual, dan membandingkan keduanya — status OK berarti artefak tidak berubah sepata bit sejak dirilis. Sebelum menjalankan binary di server, pembina unduhan melakukan verifikasi yang sama persis.
Changelog dengan Format Keep a Changelog #
Catatan rilis yang baik ditulis untuk manusia, dikelompokkan per jenis perubahan. Format Keep a Changelog menjadi standar de facto:
# Changelog
## [1.2.3] - 2026-01-15
### Fixed
- Kebocoran koneksi database saat graceful shutdown (bab Deployment).
### Added
- Endpoint `/healthz` untuk readiness probe load balancer.
## [1.2.2] - 2026-01-02
### Fixed
- Validasi port pada variabel `APP_ADDR`.Bagian Fixed, Added, dan Changed menjawab pertanyaan pertama pembaca versi baru: apa yang berubah dan apakah saya perlu waspada. Tautan per versi mengarah ke perbandingan git (v1.2.2...v1.2.3) supaya pembaca penasaran bisa membedah commit-nya.
Otomatisasi dengan GoReleaser #
Alur manual di atas — baca tag, bangun matriks platform, injeksi versi, hitung checksum, susun changelog, unggah artefak — berulang di setiap rilis dan setiap langkahnya rawan salah urutan. GoReleaser mengotomatiskan seluruh daftar itu dari satu berkas konfigurasi di repository: matriks GOOS/GOARCH, flag build (termasuk -s -w dan -trimpath), pembuatan SHA256SUMS, arsip per platform, dan halaman rilis lengkap dengan catatan. Tim kecil sering memulai dengan alur manual seperti di chapter ini, lalu berpindah ke GoReleaser begitu platform target bertambah.
Rilis yang terdokumentasi membuat pemulihan jadi cepat: saat versi baru bermasalah, kita tahu persis artefak mana yang stabil dan checksumnya. Chapter Rollback dan Recovery menyiapkan jalur pulangnya.
Referensi #
- Dokumentasi package crypto/sha256
— referensi resmi
sha256.Sum256. - Dokumentasi perintah go — dasar resmi flag build yang dipakai pipeline rilis.
- Dokumentasi package fmt
— pola
Printfuntuk format checksum dan nomor versi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
release-demo.go
Unduh berkaspackage main
import (
"crypto/sha256"
"fmt"
"strconv"
"strings"
)
// versi menyimpan komponen semver: MAJOR.MINOR.PATCH.
type versi struct {
mayor int
minor int
patch int
}
// urai mengubah string semver menjadi struct versi.
func urai(s string) (versi, error) {
bagian := strings.SplitN(s, ".", 3)
if len(bagian) != 3 {
return versi{}, fmt.Errorf("format salah: %q", s)
}
var angka [3]int
for i, b := range bagian {
n, err := strconv.Atoi(b)
if err != nil {
return versi{}, fmt.Errorf("komponen %q bukan angka", b)
}
angka[i] = n
}
return versi{angka[0], angka[1], angka[2]}, nil
}
// bandingkan mengembalikan -1, 0, atau 1 seperti strings.Compare.
func bandingkan(a, b versi) int {
if a.mayor != b.mayor {
if a.mayor < b.mayor {
return -1
}
return 1
}
if a.minor != b.minor {
if a.minor < b.minor {
return -1
}
return 1
}
if a.patch != b.patch {
if a.patch < b.patch {
return -1
}
return 1
}
return 0
}
// artefak adalah satu berkas rilis dengan isi representatifnya.
type artefak struct {
nama string
isi string
}
func main() {
// Jebakan klasik: perbandingan string menganggap 1.10.0 lebih tua dari 1.9.0.
fmt.Println(`perbandingan string "1.10.0" < "1.9.0":`, "1.10.0" < "1.9.0")
pasangan := [][2]string{
{"1.2.3", "1.2.3"},
{"1.10.0", "1.9.0"},
{"2.0.0", "1.99.9"},
}
for _, p := range pasangan {
a, errA := urai(p[0])
b, errB := urai(p[1])
if errA != nil || errB != nil {
fmt.Println("gagal mengurai pasangan", p[0], p[1])
continue
}
tanda := "="
switch bandingkan(a, b) {
case -1:
tanda = "<"
case 1:
tanda = ">"
}
fmt.Printf("perbandingan semver %s %s %s\n", p[0], tanda, p[1])
}
// Checksum tiap artefak rilis — isi sama menghasilkan checksum sama.
kumpulan := []artefak{
{"sapa-v1.2.3-linux-amd64", "binary linux/amd64 untuk sapa v1.2.3"},
{"sapa-v1.2.3-linux-arm64", "binary linux/arm64 untuk sapa v1.2.3"},
{"sapa-v1.2.3-darwin-arm64", "binary darwin/arm64 untuk sapa v1.2.3"},
}
fmt.Println("SHA256SUMS:")
for _, a := range kumpulan {
sum := sha256.Sum256([]byte(a.isi))
fmt.Printf("%x %s\n", sum, a.nama)
}
}