2026-08-20 · 8 min
GitHub Actions Matrix Build: Test Paralel di Node 20/22 dan Bun 1.3 Sekaligus
Klien fintech di Jakarta minta saya pastikan library internal mereka — sebuah wrapper validasi pembayaran — tetap jalan baik di Node 20 LTS, Node 22, dan Bun 1.3 yang mulai dipakai tim frontend mereka. Sebelumnya cara mereka “memastikan” adalah dengan menjalankan test di laptop masing-masing engineer dengan versi Node yang berbeda-beda. Saya setup GitHub Actions matrix build dalam satu hari kerja, dan sejak itu CI-lah yang menjamin kompatibilitas, bukan ingatan manusia.
Kapan masalah ini muncul
Matrix build relevan ketika kamu:
- Maintain library yang dipakai di beberapa runtime (paket npm yang mendukung Node LTS terbaru dan Bun)
- Tim yang bermigrasi dari Node ke Bun secara bertahap — butuh confidence kedua runtime berjalan sebelum fully migrating
- Aplikasi yang dideploy ke beberapa environment berbeda versi Node-nya (staging masih Node 20, production sudah Node 22)
Alternatif yang sering dipilih sebelum tahu matrix: menduplikasi job secara manual, atau hanya test di satu runtime dan berharap yang lain tidak ada masalah. Keduanya buruk. Duplikasi job berarti pemeliharaan dua kali lipat; mengabaikan runtime lain berarti bug kompatibilitas ketahuan di production, bukan di CI.
Setup dasar matrix build
Berikut struktur minimal yang saya pakai:
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Test (${{ matrix.runtime }} ${{ matrix.version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- runtime: node
version: "20"
- runtime: node
version: "22"
- runtime: bun
version: "1.3"
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
cache: "npm"
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.version }}
- name: Install dependencies (Node)
if: matrix.runtime == 'node'
run: npm ci
- name: Install dependencies (Bun)
if: matrix.runtime == 'bun'
run: bun install --frozen-lockfile
- name: Run tests (Node)
if: matrix.runtime == 'node'
run: npm test
- name: Run tests (Bun)
if: matrix.runtime == 'bun'
run: bun test
Saya sengaja pakai include alih-alih matrix dua dimensi runtime × version karena kombinasinya tidak berbentuk grid rapi — Bun punya versinya sendiri yang tidak bersinggungan dengan versi Node. Dengan include, tiap kombinasi eksplisit dan tidak ada kombinasi tidak masuk akal seperti “node runtime dengan version 1.3”.
fail-fast: false — kenapa ini penting
Default fail-fast di GitHub Actions adalah true, artinya begitu satu kombinasi matrix gagal, semua kombinasi lain yang masih jalan akan di-cancel. Ini masuk akal untuk hemat menit CI jika kamu yakin satu kegagalan berarti semua kombinasi gagal.
Tapi untuk kompatibilitas lintas runtime, saya selalu set fail-fast: false. Alasannya: kamu ingin tahu semua kombinasi yang gagal, bukan hanya yang pertama. Kalau Node 22 lolos tapi Bun 1.3 gagal karena satu API yang berbeda, kamu butuh informasi itu sekaligus — bukan baru tahu setelah fix Node 22 lalu push lagi.
Cache yang benar untuk masing-masing runtime
Ini bagian yang sering menyebabkan CI jadi lambat atau cache tidak pernah hit. Node dan Bun punya lokasi cache yang berbeda:
# Untuk Node — setup-node sudah handle cache otomatis
- name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
cache: "npm" # atau 'pnpm' kalau pakai pnpm
# Untuk Bun — setup-bun handle cache otomatis juga
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.version }}
# cache otomatis aktif, tidak perlu konfigurasi tambahan
Kalau kamu perlu fine-grained control — misalnya ingin share cache antara branch berbeda — bisa pakai actions/cache manual dengan key yang menyertakan hash lockfile:
- name: Cache Bun modules
if: matrix.runtime == 'bun'
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
restore-keys: |
bun-${{ runner.os }}-
Perhatikan bun.lockb sebagai file lockfile Bun — berbeda dengan package-lock.json milik npm. Kalau keduanya ada di repo (karena kamu maintain kompatibilitas dua package manager), pastikan hash-nya benar sesuai runtime.
Menandai test khusus runtime dengan environment variable
Kadang ada fitur yang memang hanya ditest di satu runtime tertentu. Daripada membuat job terpisah, saya pakai environment variable dari matrix:
steps:
# ... setup steps ...
- name: Run tests
run: |
if [ "${{ matrix.runtime }}" = "bun" ]; then
bun test
else
npm test
fi
env:
RUNTIME: ${{ matrix.runtime }}
RUNTIME_VERSION: ${{ matrix.version }}
Di kode test, variabel RUNTIME bisa dipakai untuk skip test tertentu:
// test/compatibility.test.ts
import { describe, test, expect } from 'vitest'; // atau bun:test untuk Bun
const runtime = process.env.RUNTIME ?? 'node';
describe('Validasi pembayaran', () => {
test('proses transfer nominal besar', () => {
// test ini jalan di semua runtime
expect(validateTransfer(1_000_000_000)).toBe(true);
});
test.skipIf(runtime === 'bun')('fitur X yang belum support Bun', () => {
// skip di Bun karena API belum tersedia
});
});
Ini lebih jujur daripada pura-pura semua test cross-runtime sementara kenyataannya tidak.
Menambahkan job summary per kombinasi
GitHub Actions punya fitur $GITHUB_STEP_SUMMARY yang memungkinkan kamu menulis Markdown langsung ke summary PR. Sangat berguna untuk matrix build agar reviewer bisa langsung lihat kombinasi mana yang lulus:
- name: Report test result
if: always()
run: |
echo "## Test Result: ${{ matrix.runtime }} ${{ matrix.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ${{ job.status }} = 'success' ]; then
echo "PASS" >> $GITHUB_STEP_SUMMARY
else
echo "FAIL" >> $GITHUB_STEP_SUMMARY
fi
Di production kami, summary ini yang pertama dilihat sebelum buka log — langsung tahu kombinasi mana yang bermasalah tanpa harus klik satu per satu.
Trade-off yang perlu diakui
Matrix build bukan gratis:
Biaya menit CI. Tiga kombinasi = tiga kali menit. Kalau test suite kamu berjalan 5 menit, matrix 3 kombinasi menghabiskan 15 menit total (meski wall-clock hanya 5 menit karena paralel). Untuk repo private dengan batasan menit, ini perlu diperhitungkan.
Bun lockfile terpisah. Bun menggunakan bun.lockb sebagai binary lockfile, sementara npm pakai package-lock.json. Kalau kamu commit keduanya — yang saya lakukan untuk repo ini — ada duplikasi yang harus dijaga sinkron. Alternatifnya, Bun bisa membaca package-lock.json biasa, tapi tanpa cache binary Bun yang optimal.
Divergensi API runtime. Bun mengimplementasikan sebagian besar Node.js API tapi tidak semuanya. Saya pernah menemukan kasus di klien Jakarta di mana node:worker_threads behave sedikit berbeda di Bun 1.2 — test yang passing di Node tiba-tiba flaky di Bun. Matrix build justru mengekspos masalah ini lebih cepat, tapi itu berarti ada bug yang perlu di-handle dan kadang workaround-nya tidak elegan.
Bun test runner belum paritas penuh dengan Jest/Vitest. bun test punya API yang mirip tapi tidak identik. Beberapa matcher edge-case dan fitur spy/mock masih ada perbedaan. Untuk library yang test suite-nya berat di fitur Jest, migrasi ke bun test butuh effort tersendiri.
Verdict
Pakai matrix build lintas Node 20/22 + Bun 1.3 kalau:
- Kamu maintain library npm yang dikonsumsi tim lain, terutama yang mulai eksperimen Bun
- Tim kamu sedang migrasi runtime secara bertahap dan butuh confidence window di mana kedua runtime valid
- Test suite kamu sudah stabil dan tidak terlalu bergantung pada fitur Jest yang belum ada di
bun test
Jangan repot setup ini kalau kamu punya monolith yang hanya deploy ke satu environment dengan Node versi tetap — cukup test di versi production kamu dan geser ke versi baru saat memang mau upgrade. Matrix build ada untuk mengelola variabilitas runtime, bukan untuk komplikasi CI yang tidak perlu.
Satu langkah awal yang bagus: mulai dari matrix dua kombinasi saja (Node 20 dan Node 22), jalankan seminggu sampai stabil, baru tambah Bun. Jangan langsung tiga kombinasi sekaligus — kalau ada kegagalan, kamu tidak tahu apakah itu karena matrix setup kamu salah atau memang ada incompatibilitas nyata.
Ditulis oleh Reza Pradipta