karawaci.kode

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