Akar Masalah: Bottleneck Sekuensial pada Test Suite

Pertumbuhan basis kode berbanding lurus dengan jumlah pengujian otomatis. Pada test suite berskala besar—khususnya integrasi dan End-to-End (E2E)—eksekusi sekuensial dalam satu runner CI menjadi titik hambat utama. Runner tunggal memiliki batas kapasitas CPU dan memori, sehingga penambahan spesifikasi mesin vertikal (vertical scaling) cepat menemui batas efisiensi biaya.

Dampaknya langsung terasa pada siklus rilis: durasi pipeline meningkat dari hitungan menit menjadi puluhan menit, antrean pull request menumpuk, dan developer feedback loop melambat signifikan. Solusi horizontal untuk masalah ini adalah test sharding: memecah kumpulan pengujian ke dalam beberapa subset independen (shard) dan mengeksekusinya secara paralel di beberapa runner GitHub Actions.

Arsitektur Test Sharding dan Matrix Strategy

Test sharding membagi daftar file atau fungsi tes berdasarkan indeks pecahan dari total partisi yang ditentukan (format index/total). Agar eksekusi stabil, pembagian beban kerja harus bersifat deterministik. Test runner seperti Playwright atau Vitest menggunakan algoritma hashing pada path file pengujian untuk memetakan file secara konsisten ke shard tertentu.

Di GitHub Actions, orkestrasi ini ditangani melalui strategy.matrix. Matrix membangkitkan sekumpulan job identik yang berjalan paralel, masing-masing membawa variabel lingkungan penanda indeks shard unik.

+-----------------------------------------------------------------+
|                       GitHub Actions Runner                     |
|                                                                 |
|   +------------------+  +------------------+  +-------------+   |
|   |  Shard Job 1/4   |  |  Shard Job 2/4   |  | Shard 3 & 4 |   |
|   |  (Tests A - G)   |  |  (Tests H - N)   |  |    (...)    |   |
|   +--------+---------+  +--------+---------+  +------+------+   |
|            |                     |                   |          |
|            v                     v                   v          |
|     [Blob Report 1]       [Blob Report 2]     [Blob Rep. 3-4]   |
+------------+---------------------+-------------------+----------+
             |                     |                   |
             +-------------------->|<------------------+
                                   v
             +-----------------------------------------+
             |         Downstream Consolidation        |
             |  1. Download all shard artifacts        |
             |  2. Merge reports (HTML/JUnit)          |
             |  3. Combine LCOV/Coverage data          |
             +-----------------------------------------+

Implementasi Workflow Paralel (Contoh: Playwright)

Konfigurasi berikut mendistribusikan eksekusi Playwright ke dalam 4 shard paralel menggunakan matrix, menyimpan output laporan modular, lalu menggabungkannya dalam downstream job terpisah.

name: E2E Tests
on:
  pull_request:
    branches: [main]

jobs:
  test-sharding:
    name: Run Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps chromium

      - name: Run tests on shard
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
        env:
          PLAYWRIGHT_BLOB_OUTPUT_NAME: blob-report-${{ matrix.shardIndex }}

      - name: Upload blob report artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: blob-report/
          retention-days: 1

Konsolidasi Hasil: Merging Report dan Coverage

Menjalankan tes secara terpisah menghasilkan output terfragmentasi. Hasil audit dan pelaporan harus disatukan agar tim pengembang tetap mendapatkan satu dashboard ringkasan eksekusi.

1. Merge Test Report

Gunakan downstream job dengan dependensi needs: [test-sharding] untuk mengunduh seluruh artifak dan memicu perintah penggabungan bawaan runner.

  merge-reports:
    name: Merge Reports & Publish
    if: always()
    needs: [test-sharding]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Download all shard reports
        uses: actions/download-artifact@v4
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true

      - name: Merge into single HTML Report
        run: npx playwright merge-reports --reporter html ./all-blob-reports

      - name: Upload Consolidated Report
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-final
          path: playwright-report/
          retention-days: 7

2. Konsolidasi Coverage Data

Untuk unit test (misal Vitest atau Jest), tiap shard menghasilkan file coverage-final.json atau file .lcov. Pada downstream job yang sama, gabungkan seluruh file tersebut menggunakan tool CLI seperti nyc:

npx nyc merge ./all-coverage-artifacts ./coverage/merged-coverage.json
npx nyc report --reporter=lcov --reporter=text --temp-dir=./coverage

Analisis Trade-Off: Menit CI vs Developer Wait Time

Sharding memangkas wall-clock time (waktu tunggu nyata), tetapi berpotensi meningkatkan total konsumsi menit penagihan (billable minutes) akibat overhead komputasi berulang.

Rumus Total Durasi Mesin:
Total Machine Minutes = N × (Setup Overhead) + Total Raw Test Time
Di mana N adalah jumlah shard.

Setiap runner baru memerlukan fase checkout, instalasi Node.js, dependensi npm ci, dan setup runtime browser. Jika fase setup memakan waktu 2 menit dan test suite dibagi ke dalam 4 runner, terjadi penambahan beban statis sebesar 8 menit komputasi (4 runner × 2 menit).

  • Kapan Menguntungkan: Test suite berdurasi 30 menit dibagi ke 4 shard (masing-masing 7.5 menit tes + 2 menit setup = ~9.5 menit total waktu tunggu). Developer menghemat ~20 menit waktu tunggu per PR, sebanding dengan sedikit kenaikan billable minutes.
  • Kapan Merugikan: Test suite hanya berdurasi 3 menit lalu dibagi ke 6 shard. Durasi setup mendominasi siklus eksekusi runner, memboroskan kuota penagihan GitHub Actions tanpa perolehan kecepatan yang signifikan.

Checklist Kelayakan Penerapan Sharding

Evaluasi kondisi test suite sebelum menerapkan sharding pada pipeline produksi:

  1. Isolasi Test Suite: Setiap test case harus stateless. Uji coba tidak boleh bergantung pada mutable state bersama, database global yang tidak diisolasi, atau urutan eksekusi antar-file.
  2. Rasio Waktu Setup terhadap Eksekusi: Total waktu tes murni minimal harus 3 kali lebih besar daripada waktu setup lingkungan (checkout + install runtime + dependensi).
  3. Deterministik: Tidak ada flaky test akibat kompetisi network resource ketika beberapa shard mengakses API staging/mock secara bersamaan.
  4. Kuota Concurrency Runner: Pastikan kuota concurrent jobs pada akun atau organisasi GitHub mencukupi agar runner shard tidak masuk antrean tunggu (queued state) yang justru memperlambat pipeline.