Standar yang berjalan di satu repo bisa jadi mimpi buruk ketika tim bertumbuh menjadi puluhan service. Di episode ini kita mengemas semantic-release menjadi shared config dan reusable workflow, lalu menerapkannya di monorepo dengan per-package config serta matrix release. Kebijakan didefinisikan sekali, diterapkan di mana-mana.

Di episode 20 kita membangun standar dan onboarding. Standar itu mudah dijaga di satu repository. Tapi begitu tim bertumbuh menjadi lima, sepuluh, atau tiga puluh repository — tiap service punya pipeline sendiri — konsistensi mulai retak: satu repo masih menjalankan npx semantic-release --no-ci, yang lain lupa mengatur NPM_TOKEN. Di episode ini kita mengemas otomasi menjadi bagian yang bisa dipakai ulang, supaya kebijakan rilis didefinisikan sekali dan diterapkan di mana-mana.
Di episode ini kita membahas:
workflow_call.Ada dua lapisan yang perlu di-share: konfigurasi (aturan rilis: branch, plugin, tag format) dan pipeline (cara rilis dijalankan). Keduanya harus didefinisikan sekali dan dikonsumsi di mana-mana, agar perbaikan di satu tempat otomatis menyebar ke semua.
Buat satu package npm khusus konfigurasi, misalnya @acme/semantic-release-config, yang berisi aturan rilis standar organisasi:
{
"name": "@acme/semantic-release-config",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"semantic-release": "semantic-release"
},
"peerDependencies": {
"semantic-release": ">=24"
}
}Isi index.js dengan config lengkap:
module.exports = {
branches: [
{ name: 'main' },
{ name: 'staging', prerelease: 'rc' }
],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/npm',
'@semantic-release/github'
]
}Lalu setiap repository cukup menaruh config tipis yang meng-extend package tersebut:
module.exports = {
extends: '@acme/semantic-release-config'
}Kalau organisasi memutuskan menambah plugin atau mengubah prerelease, cukup ubah satu package — semua repo ikut berubah. Inilah cara menjaga kebijakan seragam tanpa menyalin file.
Tip
Rilis package config bersama itu sendiri menggunakan semantic-release! Ia adalah contoh makan dari masakan sendiri — versioning dan changelog-nya dikelola dengan alur yang sama persis dengan yang kalian ajarkan.
Lapisan kedua adalah pipeline. Simpan workflow release yang lengkap di satu repository infrastruktur, lalu ekspos lewat workflow_call supaya bisa dipanggil dari repo mana pun:
name: Reusable Release
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
secrets:
NPM_TOKEN:
required: true
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- name: Rilis otomatis
run: npx semantic-release
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Tiap service hanya memanggilnya:
name: Release Service A
on:
push:
branches: [main, staging]
jobs:
release:
uses: acme/release-pipelines/.github/workflows/release.yml@main
with:
node-version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Perubahan ke pipeline, misalnya menambah step audit dependensi, cukup sekali di repo infrastruktur. Semua service langsung mendapat upgrade tanpa memegang tiap repo satu per satu.
Warning
Workflow yang dipanggil dengan referensi tag bisa dipin-kan untuk stabilitas, tetapi perbaikan tidak akan menyebar otomatis. Seimbangkan keduanya: gunakan tag untuk workflow yang sensitif dan main branch untuk iterasi cepat. Dan jangan pernah memanggil reusable workflow dari repository yang tidak dipercaya.
Saat service baru dibuat, jangan mulai dari nol. Buat repository template (aktifkan opsi template pada pengaturan repository) yang berisi:
release.config.cjs yang meng-extend config bersama.CONTRIBUTING.md, CODEOWNERS, dan konfigurasi Dependabot.Setiap service baru adalah anak kandung standar yang sama. Deviasi hanya terjadi bila ada alasan — dan alasan itu harus lewat PR, bukan lewat cara lama yang sudah terlanjur.
Monorepo menyimpan banyak package dalam satu repository. Masalahnya: git tag bersifat global di satu repo, sementara versi ingin dihitung per package. Dua pendekatan yang umum:
Pendekatan A: Per-package config dengan matrix. Setiap package punya .releaserc.cjs sendiri, dan satu workflow menjalankan release per package:
jobs:
release:
strategy:
fail-fast: false
matrix:
package: [api, web, worker]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm ci
- name: Rilis package
run: npx semantic-release --config ./packages/${{ matrix.package }}/.releaserc.cjs
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Pendekatan B: Plugin monorepo. Gunakan @semantic-release/monorepo agar analisis commit dilakukan per direktori package. Commit yang menyentuh packages/api hanya memicu rilis api, tanpa mengganggu package lain.
Warning
Di monorepo, semua package berbagi satu set git tag. Rilis package A dan B secara bersamaan bisa saling menimpa tag. Solusinya: pisahkan rilis per package, gunakan plugin monorepo, atau pertimbangkan memecah menjadi repository terpisah bila tag tiap service wajib independen.
Untuk package npm, gunakan scope organisasi — @acme/api — agar kepemilikan jelas. Package scoped bersifat private secara default, jadi beri publishConfig untuk akses public bila memang publik:
{
"name": "@acme/api",
"version": "1.4.0",
"publishConfig": {
"access": "public",
"registry": "https://npm.pkg.github.com"
}
}Terakhir, pusatkan kebijakan di satu tempat agar mudah diaudit:
| Kebijakan | Tempat | Diterapkan ke |
|---|---|---|
| Aturan rilis | @acme/semantic-release-config | Semua repo |
| Pipeline rilis | Reusable workflow | Semua repo |
| Standar struktur | Repository template | Service baru |
| Proteksi branch | Branch protection rules | Repo milik tim |
Di episode ini kita melipatgandakan otomasi tanpa melipatgandakan beban pemeliharaan:
extends menjaga kebijakan rilis seragam.workflow_call menghilangkan pipeline yang disalin-salin.Sistem yang konsisten ini tinggal diuji di lingkungan paling keras: produksi. Di episode 22, episode pamungkas, kita melakukan hardening menyeluruh dan merangkum seluruh perjalanan. Sampai jumpa!