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:

go
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:

text
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-arm64

Mari kita telaah bagian penting program di atas:

  1. Baris 16–30 (urai): memecah string pada tanda titik maksimal tiga bagian, lalu mengubah tiap komponen menjadi angka lewat strconv.Atoi — komponen non-angka (mis. 1.2.3-rc.1) dilaporkan sebagai galat, bukan ditebak.
  2. Baris 33–55 (bandingkan): membandingkan mayor dulu, lalu minor, lalu patch — urutan yang membuat 1.10.0 benar dianggap lebih baru dari 1.9.0.
  3. Baris 76 (fmt.Println perbandingan string): membuktikan jebakannya — hasil true tersebut salah secara semver, dan menjadi alasan ura + bandingkan wajib ada.
  4. Baris 97–101 (sha256.Sum256): menghitung checksum 256-bit dari isi artefak; %x mencetaknya 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:

bash
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.3

Output yang Dihasilkan (tiga perintah pertama):

text
v1.2.3
tag

Objek 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:

bash
cp sapa-server sapa-v1.2.3-linux-amd64
sha256sum sapa-v1.2.3-linux-amd64 > SHA256SUMS
sha256sum -c SHA256SUMS

Output yang Dihasilkan:

text
sapa-v1.2.3-linux-amd64: OK

Perintah -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:

markdown
# 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

release-demo.go

Unduh berkas
go
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)
	}
}