Episode ini membahas prinsip DRY di CI/CD lewat reusable workflows dan composite actions. Kalian akan belajar memicu workflow modular dengan workflow_call, mengirim inputs dan secrets, serta mengemas banyak langkah menjadi satu action reusable.

Di episode 10 kita menutup fase keamanan seri ini dengan OIDC. Sekarang kita masuk fase baru yang tidak kalah penting di organisasi besar: reuse. Bayangkan perusahaan dengan lima puluh repository, masing-masing berisi salinan workflow lint, test, build, dan deploy yang sama persis. Suatu hari ditemukan bug di pipeline — perbaikannya harus disalin manual ke lima puluh file, dan dijamin ada yang terlewat. Inilah masalah yang ingin diselesaikan episode 11: menerapkan prinsip DRY (Don't Repeat Yourself) pada pipeline. Dua senjatanya adalah Reusable Workflows dan Composite Actions.
Dalam pemrograman, kalian tidak akan menulis fungsi yang sama di dua puluh tempat hanya karena dipakai di dua puluh modul — kalian ekstrak satu fungsi dan memanggilnya. Sayangnya banyak tim memperlakukan workflow CI/CD sebagai "copy-paste project". Akibatnya nyata: perbaikan tidak merata, versi action tidak sinkron, dan audit sulit dilakukan karena "standar" sebenarnya adalah aneka salinan yang menyimpang. DRY di CI/CD berarti logika pipeline ditulis sekali, dipelihara di satu tempat, dan dipakai ulang oleh banyak workflow.
Semua workflow yang kita tulis sejauh ini dipicu oleh event seperti push atau pull_request. Reusable workflow memperkenalkan trigger baru: workflow_call — workflow yang tidak berjalan sendiri, melainkan hanya dijalankan ketika workflow lain memanggilnya.
Kuncinya, workflow yang bisa dipanggil ulang ini mendeklarasikan antarmuka eksplisit: inputs untuk parameter, secrets untuk nilai rahasia yang dibutuhkan, dan outputs untuk nilai yang ingin dibagikan kembali ke pemanggil. Perhatikan rantai outputnya: di level workflow_call, output menunjuk ke output job, dan output job menunjuk ke output step lewat file $GITHUB_OUTPUT:
name: Reusable Build and Test
on:
workflow_call:
inputs:
node-version:
type: string
required: false
default: "20"
secrets:
npm-token:
required: true
outputs:
coverage:
description: Persentase coverage dari laporan test
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.coverage.outputs.rate }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- run: npm run lint
- run: npm test -- --coverage
- id: coverage
run: echo "rate=$(npm run coverage:rate)" >> "$GITHUB_OUTPUT"Inilah pola "pipeline dalam pipeline" yang bisa dikendalikan dari luar — seperti fungsi dengan parameter, nilai balik, dan dokumen ekspektasinya.
Dari workflow lain, reusable workflow dipanggil lewat key uses dengan format path penuh: owner/repo/.github/workflows/file.yml@ref. Ref bisa branch, tag, atau SHA commit — untuk produksi, sebaiknya tag atau SHA, bukan branch yang bisa berubah diam-diam.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
quality:
uses: devvnull/actions-library/.github/workflows/build-test.yml@v1
with:
node-version: "20"
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}Input dikirim lewat with, secret dipetakan eksplisit satu per satu di bawah secrets. Pemetaan eksplisit ini sengaja — kalian hanya menyerahkan apa yang benar-benar dibutuhkan, bukan seluruh kotak kunci.
Warning
Ada jalan pintas secrets: inherit yang meneruskan semua secret repository ke workflow yang dipanggil. Nyaman memang, tetapi melanggar prinsip least privilege dari episode 9: workflow pemanggil bisa membocorkan secret yang tidak diperlukan. Untuk tim kecil mungkin boleh, untuk organisasi besar selalu petakan secara eksplisit.
Output dari reusable workflow dibaca di job selanjutnya lewat context needs — persis seperti output job biasa:
jobs:
quality:
uses: devvnull/actions-library/.github/workflows/build-test.yml@v1
with:
node-version: "20"
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
report:
runs-on: ubuntu-latest
needs: quality
steps:
- run: echo "Coverage saat ini: ${{ needs.quality.outputs.coverage }}"Reusable workflow bekerja pada satuan job. Kadang yang ingin dihemat lebih kecil: sekelompok langkah yang sama dipakai berulang di dalam berbagai job. Di situlah composite action berperan — sebuah action kustom internal yang mengemas beberapa steps menjadi satu langkah yang bisa dipanggil lewat uses.
Composite action ditulis sebagai file action.yml di dalam repository. Ini contoh action internal yang menginstal Node.js sekaligus mengurus cache npm:
name: Setup Node dan Cache
description: Menginstal Node.js lalu memulihkan cache npm
inputs:
node-version:
description: Versi Node.js yang dipakai
required: true
outputs:
cache-hit:
description: Apakah cache berhasil dipulihkan
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- id: cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-Perhatikan runs.using: composite — di sinilah dideklarasikan bahwa action ini menjalankan langkah-langkah internalnya di job yang sama (bukan runner sendiri). Input dibaca lewat context inputs, bukan secrets: composite action tidak bisa mengakses secrets secara langsung, jadi nilai rahasia harus dikirim sebagai input dari pemanggil.
Pemakaiannya sederhana, cukup referensikan path folder action-nya:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-tools
with:
node-version: "20"
- run: npm run buildComposite action yang baik dipakai oleh banyak job di repository ini — perubahannya ikut terversi dan tervalidasi lewat pull request, seperti kode biasa.
| Aspek | Reusable Workflow | Composite Action |
|---|---|---|
| Satuan yang dibagi | Satu atau lebih job lengkap | Kumpulan steps dalam satu action |
| Runner | Punya runner sendiri | Menumpang runner job pemanggil |
| Trigger | on: workflow_call | Dipanggil lewat key uses |
| Parameter | inputs + secrets | inputs saja |
| Output | Bisa dibagikan ke job lain | Terbatas dalam satu job |
| Contoh | Pipeline lint-test-build standar | Setup environment plus cache |
| Kesalahan | Gejala | Solusi |
|---|---|---|
| Melupakan output di level job | Nilai output pemanggil kosong | Deklarasikan output di dua level sekaligus |
| Mereferensikan @main untuk produksi | Perubahan mendadak merusak semua pemanggil | Pin ref ke tag atau SHA commit |
| Memakai secrets di composite action | Nilai rahasia tidak pernah tampak | Kirim lewat input dari pemanggil |
| Menyalin workflow dari repo lain | Duplikasi tetap terpelihara | Gunakan uses ke versi yang sama |
Reusable components mengubah cara organisasi memelihara pipeline:
on: workflow_call, dengan inputs, secrets, dan outputs yang eksplisit.action.yml dan runs.using: composite.Di episode 12 berikutnya, kita masuk fase containerization: Integrasi Docker & Container Registries — menjalankan job di dalam container untuk isolasi lingkungan build, lalu mengotomasi build dan push image multi-platform ke GHCR dengan layer caching. Karena pipeline yang reusable juga harus reproducible!