Jika tim Anda merilis aplikasi CLI Rust secara manual, masalah yang sering muncul biasanya bukan pada proses build, tetapi pada konsistensi release: versi di Cargo.toml terlupa diperbarui, tag Git tidak sinkron, changelog tidak jelas, atau artefak biner tidak terunggah lengkap. Kombinasi cargo-release dan GitHub Actions membantu menutup celah ini dengan alur yang dapat diulang, diaudit, dan lebih aman untuk kolaborasi tim.

Artikel ini fokus pada implementasi praktis otomasi release Rust dengan cargo-release dan GitHub Actions: mulai dari semver bump, update metadata proyek, pembuatan tag, validasi lint/test, publish ke crates.io bila relevan, sampai membangun artefak biner lintas platform. Saya juga akan membahas risiko umum seperti tag ganda, versi tidak sinkron, pipeline yang tidak idempoten, dan strategi rollback ketika publish gagal di tengah jalan.

Kapan cargo-release cocok dipakai

cargo-release cocok ketika Anda ingin menjadikan release sebagai proses standar, bukan serangkaian langkah manual yang berbeda-beda antar anggota tim. Tool ini membantu mengorkestrasi beberapa langkah yang biasanya dilakukan terpisah:

  • menaikkan versi berdasarkan semantic versioning,
  • memperbarui Cargo.toml dan file terkait,
  • membuat commit release,
  • membuat Git tag,
  • menjalankan hook sebelum/sesudah release,
  • opsional melakukan publish ke crates.io.

Keuntungan utamanya bukan sekadar otomatisasi, tetapi determinisme proses. Tim memiliki satu sumber kebenaran untuk release, sehingga lebih mudah ditinjau dan dipulihkan bila terjadi kegagalan.

Untuk proyek CLI internal yang tidak dipublikasikan ke crates.io, Anda tetap bisa memakai cargo-release hanya untuk mengelola versi, tag, dan commit release.

Alur release yang direkomendasikan

Untuk DX tim yang baik, pisahkan proses release menjadi dua tahap logis:

  1. Tahap persiapan release: validasi branch, jalankan test/lint, hitung versi, perbarui Cargo.toml, buat commit dan tag.
  2. Tahap distribusi: publish crate ke crates.io bila diperlukan, buat GitHub Release, lalu build dan unggah artefak biner lintas platform.

Pemisahan ini penting karena tidak semua langkah punya sifat yang sama. Misalnya, publish ke crates.io sulit dibatalkan setelah berhasil, sedangkan upload artefak ke GitHub relatif mudah diulang. Dengan memisahkan tahap, Anda bisa mengurangi efek samping saat pipeline gagal di tengah jalan.

Urutan yang aman

Urutan umum yang cukup aman untuk banyak tim adalah:

  1. jalankan cargo fmt --check, cargo clippy, dan cargo test,
  2. jalankan cargo release --dry-run,
  3. jalankan release sebenarnya untuk membuat commit dan tag,
  4. push commit dan tag ke remote,
  5. workflow berbasis tag membangun artefak dan membuat GitHub Release,
  6. publish ke crates.io dilakukan hanya jika proyek memang library/crate publik.

Kenapa tag sebaiknya menjadi pemicu tahap distribusi? Karena tag adalah penanda versi yang stabil. Ini mengurangi risiko artefak dibangun dari commit yang belum benar-benar menjadi release resmi.

Menyiapkan proyek Rust untuk cargo-release

Instalasi lokal untuk maintainer release biasanya sederhana:

cargo install cargo-release

Konfigurasi dapat diletakkan di Cargo.toml. Contoh berikut realistis untuk CLI atau library sederhana:

[package]
name = "mycli"
version = "0.4.2"
edition = "2021"
description = "CLI contoh"
license = "MIT"
repository = "https://github.com/acme/mycli"

[workspace.metadata.release]
shared-version = true
tag-name = "v{{version}}"
commit-message = "release: v{{version}}"
tag-message = "Release v{{version}}"
push = false
publish = false
pre-release-commit-message = "chore: prepare release v{{version}}"
pre-release-replacements = [
  { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}} - {{date}}", min = 1 }
]

Beberapa poin penting dari konfigurasi di atas:

  • tag-name = "v{{version}}" menjaga format tag konsisten, misalnya v0.4.3.
  • push = false sengaja dipilih agar pipeline atau maintainer dapat mengontrol kapan commit/tag didorong ke remote. Ini mengurangi efek samping bila langkah berikutnya gagal.
  • publish = false berguna bila Anda ingin memisahkan pembuatan tag dari publish ke registry. Untuk banyak tim, ini lebih aman daripada semua dilakukan dalam satu perintah.
  • pre-release-replacements dapat dipakai untuk mengganti bagian tertentu di changelog secara otomatis.

Jika proyek Anda berbentuk workspace, pastikan strategi versi jelas sejak awal: apakah semua crate berbagi versi yang sama, atau masing-masing punya versi sendiri. Masalah versi tidak sinkron sering muncul ketika workspace berkembang, tetapi kebijakan release belum ditegaskan.

Semver bump yang umum dipakai

cargo-release mendukung pola bump versi berdasarkan semver. Contoh perintah yang umum:

cargo release patch --dry-run
cargo release minor --dry-run
cargo release major --dry-run

Dry-run penting karena Anda bisa melihat perubahan yang akan dilakukan tanpa benar-benar membuat commit, tag, atau publish. Dalam tim, ini berguna untuk memverifikasi bahwa versi berikutnya memang sesuai dengan perubahan API dan kompatibilitasnya.

Menghasilkan changelog ringkas tanpa membuat pipeline rapuh

cargo-release bukan generator changelog penuh. Ia lebih cocok untuk mengorkestrasi update placeholder atau metadata release. Untuk changelog ringkas, pendekatan yang aman adalah:

  • menjaga file CHANGELOG.md dengan bagian Unreleased,
  • mengganti label tersebut menjadi versi dan tanggal saat release,
  • membiarkan isi changelog tetap ditulis manusia agar ringkas dan relevan.

Pendekatan ini lebih mudah dikendalikan daripada mencoba menghasilkan changelog otomatis sepenuhnya dari commit yang belum tentu konsisten formatnya. Untuk tim kecil hingga menengah, ini biasanya memberi hasil lebih rapi dengan kompleksitas lebih rendah.

Contoh struktur sederhana:

# Changelog

## Unreleased
- Tambah opsi --json pada CLI
- Perbaiki error handling saat file konfigurasi tidak ditemukan
- Tingkatkan validasi argumen input

Saat release dijalankan, bagian Unreleased dapat diubah menjadi versi final, misalnya 0.4.3 - 2026-06-19.

Struktur GitHub Actions yang disarankan

Struktur yang stabil biasanya memakai dua workflow:

  1. workflow manual release untuk bump versi, commit, dan tag,
  2. workflow berbasis tag untuk build artefak, publish, dan GitHub Release.

Kenapa tidak dijadikan satu workflow? Karena pipeline satu tahap cenderung tidak idempoten. Jika gagal setelah tag dibuat tetapi sebelum artefak selesai diunggah, Anda akan kesulitan mengulang tanpa memikirkan efek terhadap tag atau publish sebelumnya.

Workflow 1: release manual dengan validasi dan dry-run

Workflow berikut dipicu manual lewat workflow_dispatch. Ia menerima input jenis bump semver, menjalankan validasi, lalu membuat commit release dan tag.

name: release

on:
  workflow_dispatch:
    inputs:
      level:
        description: "Semver bump: patch, minor, major"
        required: true
        default: "patch"

permissions:
  contents: write

jobs:
  prepare-release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Install cargo-release
        run: cargo install cargo-release --locked

      - name: Validate formatting
        run: cargo fmt --all --check

      - name: Validate lint
        run: cargo clippy --all-targets --all-features -- -D warnings

      - name: Validate tests
        run: cargo test --all-features

      - name: Dry run release
        run: cargo release ${{ github.event.inputs.level }} --dry-run --execute

      - name: Configure git user
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Run release
        run: cargo release ${{ github.event.inputs.level }} --execute

      - name: Push commit and tag
        run: |
          git push origin HEAD
          git push origin --tags

Catatan penting:

  • fetch-depth: 0 diperlukan agar riwayat Git dan tag tersedia penuh. Banyak masalah tag ganda atau deteksi versi salah berawal dari checkout dangkal.
  • cargo release ... --dry-run --execute memang terlihat janggal, tetapi pola ini umum untuk memerintahkan tool menjalankan simulasi penuh tanpa benar-benar melakukan perubahan permanen. Jika Anda ragu pada perilaku CLI yang terpasang, verifikasi lewat cargo release --help di proyek Anda.
  • Workflow ini sebaiknya dijalankan hanya dari branch release utama, misalnya main. Anda bisa menambahkan guard eksplisit bila perlu.

Contoh guard branch:

- name: Ensure main branch
  run: |
    test "${GITHUB_REF}" = "refs/heads/main" || \
      (echo "Release hanya boleh dari branch main" && exit 1)

Workflow 2: build artifact lintas platform dari tag

Setelah tag seperti v0.4.3 didorong, workflow kedua akan aktif. Tugasnya membangun biner untuk beberapa target populer dan mengunggahnya sebagai artefak release.

name: build-and-publish

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            archive: tar.gz
          - os: macos-latest
            target: x86_64-apple-darwin
            archive: tar.gz
          - os: macos-latest
            target: aarch64-apple-darwin
            archive: tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            archive: zip
    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }}

      - name: Package archive (Unix)
        if: runner.os != 'Windows'
        run: |
          mkdir -p dist
          cp target/${{ matrix.target }}/release/mycli dist/
          tar -C dist -czf mycli-${{ github.ref_name }}-${{ matrix.target }}.tar.gz mycli

      - name: Package archive (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force dist | Out-Null
          Copy-Item target/${{ matrix.target }}/release/mycli.exe dist/
          Compress-Archive -Path dist/mycli.exe -DestinationPath mycli-${{ github.ref_name }}-${{ matrix.target }}.zip

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: mycli-${{ matrix.target }}
          path: |
            *.tar.gz
            *.zip

  github-release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: artifacts/**/*

Workflow ini sengaja dipicu dari tag, bukan branch. Dengan demikian, artefak selalu terasosiasi langsung dengan versi release tertentu.

Publish ke crates.io bila relevan

Untuk library atau crate yang memang ingin dipublikasikan, ada dua pendekatan:

  • publish di workflow release yang sama,
  • publish di workflow berbasis tag.

Untuk stabilitas, pendekatan kedua biasanya lebih baik karena publish terjadi hanya setelah tag resmi dibuat. Namun, ada trade-off: jika publish gagal, tag sudah terlanjur ada. Karena itu, Anda perlu strategi rollback yang jelas.

Contoh job publish sederhana:

  publish-crate:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}

Secret yang umumnya dibutuhkan:

  • CARGO_REGISTRY_TOKEN untuk publish ke crates.io,
  • token GitHub bawaan workflow untuk membuat release dan mengunggah artefak,
  • bila menggunakan registry privat, secret tambahan sesuai registry Anda.

Simpan token crates.io sebagai repository secret dan batasi siapa yang boleh menjalankan workflow release. Jangan menaruh token di file konfigurasi atau environment yang tercetak ke log.

Dry-run, validasi, dan proteksi sebelum release

Bagian ini sering dianggap tambahan, padahal justru menentukan apakah pipeline release aman dipakai sehari-hari.

1. Selalu jalankan lint dan test sebelum bump/tag

Jangan membuat tag release lalu baru mengetahui build rusak. Validasi minimum yang masuk akal:

  • cargo fmt --all --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test --all-features

Jika proyek punya smoke test CLI, jalankan juga. Banyak bug release terjadi bukan pada unit test, tetapi pada perilaku biner hasil build.

2. Gunakan dry-run sebagai tahap wajib

Dry-run membantu menemukan:

  • placeholder changelog yang tidak cocok,
  • tag yang akan bentrok dengan tag lama,
  • versi berikutnya yang salah,
  • workspace yang belum sinkron.

Di tim, dry-run membuat review release lebih mudah karena anggota lain bisa melihat perubahan yang akan dihasilkan tanpa efek permanen.

3. Lindungi branch utama

Release sebaiknya hanya boleh dijalankan dari branch yang sudah lolos review dan CI normal. Aktifkan branch protection agar commit release tidak menimpa perubahan yang belum tervalidasi.

Masalah umum dan cara menghindarinya

Tag ganda atau bentrok

Masalah ini muncul saat:

  • release dijalankan dua kali untuk versi yang sama,
  • tag sudah ada di remote tetapi runner belum mengambil seluruh tag,
  • pipeline diulang tanpa pengecekan idempoten.

Pencegahannya:

  • gunakan fetch-depth: 0,
  • cek keberadaan tag sebelum membuat release,
  • jadikan workflow release hanya bisa dipicu manual oleh maintainer tertentu.

Contoh pengecekan sederhana:

- name: Fail if tag already exists remotely
  run: |
    VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
    git fetch --tags
    git rev-parse "v${VERSION}" >/dev/null 2>&1 && \
      (echo "Tag v${VERSION} sudah ada" && exit 1) || true

Jika Anda tidak ingin bergantung pada jq, gunakan pendekatan lain untuk membaca versi, atau cukup mengandalkan output cargo-release saat dry-run.

Versi tidak sinkron di workspace

Pada workspace multi-crate, satu crate bisa ter-bump sementara crate lain tidak, atau dependensi internal masih menunjuk versi lama. Solusinya adalah menetapkan kebijakan sejak awal:

  • satu versi bersama untuk semua crate, atau
  • versi independen dengan prosedur release per crate.

Pilihan pertama lebih mudah untuk tim kecil. Pilihan kedua lebih fleksibel, tetapi konfigurasi dan review-nya lebih kompleks.

Pipeline tidak idempoten

Ini terjadi saat sebagian langkah sudah memberi efek permanen, lalu workflow gagal dan diulang. Contohnya:

  • tag sudah dibuat tetapi artefak belum selesai diunggah,
  • crate sudah terpublikasi tetapi GitHub Release gagal dibuat,
  • commit release sudah terdorong tetapi changelog di artefak berbeda.

Cara mengurangi risiko:

  • pisahkan workflow pembuatan tag dan workflow distribusi,
  • hindari melakukan git push sebelum validasi selesai,
  • buat langkah yang bisa diulang tanpa efek samping,
  • cek keberadaan release/artifact sebelum membuat ulang bila perlu.

Publish crates.io gagal setelah tag terbuat

Ini skenario yang harus dipikirkan dari awal. Setelah versi terpublikasi, Anda tidak bisa sekadar menimpa versi yang sama. Jika publish gagal sebelum sukses penuh, langkah penanganannya bergantung pada titik gagal:

  • Tag sudah ada, publish belum terjadi: perbaiki masalah lalu rerun job publish atau distribusi berbasis tag.
  • Publish berhasil, artefak GitHub gagal: jangan ubah tag atau versi. Cukup rerun workflow distribusi atau unggah artefak yang gagal.
  • Commit/tag sudah dibuat, tetapi ternyata isi release salah: buat versi berikutnya dengan patch bump. Menghapus tag dan memaksa ulang versi yang sama biasanya lebih berisiko, terutama jika ada sistem eksternal yang sudah mengonsumsi tag tersebut.

Untuk crates.io, rollback paling realistis biasanya bukan "membatalkan versi", melainkan merilis versi perbaikan secepat mungkin.

Strategi rollback yang praktis

Rollback release otomatis harus dibedakan antara sebelum push dan sesudah push.

Sebelum commit/tag didorong

Ini kondisi terbaik. Jika release lokal atau di runner gagal sebelum git push, Anda cukup membatalkan perubahan Git dan mengulang setelah perbaikan.

git tag -d v0.4.3
git reset --hard HEAD~1

Perintah di atas hanya contoh konsep. Pastikan commit terakhir memang commit release sebelum menjalankannya.

Sesudah tag didorong

Jika tag sudah di remote, rollback menjadi lebih sensitif. Menghapus tag remote mungkin memungkinkan secara teknis, tetapi bisa membingungkan jika workflow lain atau pengguna sudah melihat tag tersebut.

git push --delete origin v0.4.3

Gunakan hanya bila Anda yakin tag belum dipakai oleh pihak lain dan publish ke crates.io belum dilakukan. Dalam banyak kasus tim, lebih aman membuat release korektif baru daripada memaksa sejarah berubah.

Praktik terbaik untuk DX tim

  • Standarkan siapa yang boleh merilis. Jangan biarkan semua orang bisa memicu workflow release tanpa kontrol.
  • Dokumentasikan level bump. Kapan perubahan dianggap patch, minor, atau major harus jelas agar semver konsisten.
  • Jangan campur perubahan fitur dan release commit. Commit release sebaiknya fokus pada versi, changelog, dan metadata.
  • Simpan changelog tetap ringkas. Tulis perubahan yang penting bagi pengguna, bukan semua commit internal.
  • Uji instalasi artefak. Build sukses tidak selalu berarti biner mudah dipakai di sistem target.
  • Pisahkan concern. Versioning/tagging, publish registry, dan upload artefak sebaiknya tidak saling mengunci dalam satu langkah besar.

Penutup

cargo-release dan GitHub Actions adalah kombinasi yang kuat untuk mengotomasi release CLI atau library Rust, asalkan alurnya dirancang dengan disiplin. Kunci keberhasilannya bukan pada banyaknya otomasi, melainkan pada struktur proses yang aman: validasi dulu, dry-run dulu, buat tag yang konsisten, lalu distribusikan artefak dan publish secara terpisah bila perlu.

Jika Anda baru mulai, target minimal yang sangat masuk akal adalah: lint/test wajib, cargo release --dry-run, commit dan tag otomatis, lalu build artefak lintas platform dari tag. Setelah itu, barulah tambahkan publish ke crates.io dan pembuatan changelog yang lebih rapi sesuai kebutuhan tim.