GitHub Actions untuk Laravel sering melambat bukan karena test-nya berat, tetapi karena instalasi dependency berulang, job yang terlalu gemuk, dan strategi cache yang kurang tepat. Solusi yang paling aman biasanya bukan sekadar "menyalakan cache", melainkan memilih apa yang layak di-cache, memisahkan jenis pekerjaan CI, dan menjalankan test pada beberapa versi PHP dengan matrix yang terkontrol.

Dalam artikel ini, kita akan membangun workflow Laravel yang praktis: lint, static analysis, dan test dipisah menjadi job terpisah; Composer dioptimalkan dengan cache yang relevan; dan pengujian lintas versi PHP dilakukan dengan matrix. Kita juga akan membahas kapan cache benar-benar membantu, kapan justru menimbulkan hasil tidak konsisten, serta cara menjaga durasi CI tetap rendah saat repository membesar.

Mengapa pipeline Laravel sering lambat dan tidak stabil

Pada banyak project Laravel, bottleneck CI biasanya muncul dari beberapa hal berikut:

  • Composer install berulang di setiap job dan setiap run.
  • Satu job melakukan semuanya: install dependency, lint, PHPStan, PHPUnit, build asset, dan sebagainya.
  • Cache yang terlalu agresif, misalnya menyimpan folder vendor tanpa key yang benar.
  • Perbedaan versi PHP antara lingkungan lokal dan CI.
  • Matrix yang tidak dikendalikan, sehingga kombinasi job membengkak dan waktu total meningkat drastis.

Tujuan pipeline yang baik bukan hanya cepat, tetapi juga deterministik. Jika commit yang sama kadang lolos dan kadang gagal karena cache lama, maka CI kehilangan nilai utamanya sebagai sinyal kualitas.

Prinsip desain workflow GitHub Actions untuk Laravel

1. Pisahkan job berdasarkan tujuan

Pisahkan minimal menjadi tiga jenis job:

  • Lint untuk format dan style, misalnya Laravel Pint.
  • Static analysis untuk PHPStan.
  • Test untuk PHPUnit, biasanya dengan matrix beberapa versi PHP.

Keuntungan pemisahan ini:

  • Lebih mudah melihat sumber kegagalan.
  • Job ringan seperti Pint bisa selesai cepat tanpa menunggu test berat.
  • Cache dan dependency bisa diatur sesuai kebutuhan tiap job.
  • Lebih mudah mengoptimalkan job yang paling mahal.

2. Cache yang aman lebih penting daripada cache yang agresif

Untuk Laravel berbasis Composer, cache yang paling aman biasanya adalah cache download Composer, bukan langsung cache vendor. Composer cache menyimpan paket yang sudah diunduh, tetapi proses instalasi tetap menghormati composer.lock, platform PHP, dan dependency resolution yang relevan.

Sebaliknya, cache vendor bisa lebih cepat dalam kondisi tertentu, tetapi juga lebih rawan menghasilkan state yang tidak cocok jika:

  • versi PHP berbeda,
  • ekstensi PHP berbeda,
  • composer.lock berubah,
  • script post-install memodifikasi isi directory,
  • ada dependency native atau perilaku spesifik platform.

Prinsip praktis: Mulai dari cache Composer download terlebih dahulu. Tambahkan cache vendor hanya jika benar-benar perlu dan key-nya dibuat cukup ketat.

3. Gunakan matrix PHP secara selektif

Matrix berguna untuk memastikan aplikasi Laravel Anda tetap berjalan pada beberapa versi PHP yang didukung. Namun tidak semua job perlu dijalankan pada semua versi.

Contoh strategi yang masuk akal:

  • Pint: cukup satu versi PHP.
  • PHPStan: cukup satu versi PHP yang menjadi baseline tim, kecuali ada alasan kompatibilitas khusus.
  • PHPUnit: jalankan di beberapa versi PHP melalui matrix.

Pendekatan ini menekan waktu total CI tanpa kehilangan cakupan kompatibilitas yang penting.

Contoh workflow GitHub Actions untuk Laravel

Berikut contoh workflow yang memisahkan job lint, static analysis, dan test. Contoh ini menggunakan:

  • actions/checkout untuk mengambil source code,
  • shivammathur/setup-php untuk menyiapkan runtime PHP,
  • actions/cache untuk Composer cache,
  • matrix PHP untuk job test.
name: ci

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  lint:
    name: Lint (Pint)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: none

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer downloads
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-php-8.2-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-8.2-composer-
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Run Pint
        run: ./vendor/bin/pint --test

  static-analysis:
    name: Static Analysis (PHPStan)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: none

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer downloads
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-php-8.2-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-8.2-composer-
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --no-progress

  test:
    name: PHPUnit (PHP ${{ matrix.php }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.1', '8.2', '8.3']
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer downloads
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-${{ matrix.php }}-composer-
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Prepare environment
        run: |
          cp .env.example .env
          php artisan key:generate

      - name: Run PHPUnit
        run: php artisan test

Workflow di atas sengaja dibuat konservatif. Ia sudah cukup cepat untuk banyak project Laravel, tetapi tetap aman karena tidak bergantung pada cache vendor yang rentan stale.

Memahami key cache dan restore-keys

Struktur key yang baik

Bagian paling penting dari cache adalah key. Pada contoh di atas:

key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}

Artinya cache dibedakan berdasarkan:

  • OS runner, misalnya Ubuntu.
  • Versi PHP, karena dependency dan platform requirement bisa berbeda.
  • Isi composer.lock, sehingga saat dependency berubah, key juga berubah.

Ini penting karena cache yang terlalu umum mudah menyebabkan mismatch.

Kegunaan restore-keys

restore-keys memungkinkan GitHub Actions mencari cache yang paling mendekati jika key utama tidak ditemukan. Contohnya:

restore-keys: |
  ${{ runner.os }}-php-${{ matrix.php }}-composer-
  ${{ runner.os }}-composer-

Dengan pola ini, jika composer.lock berubah, Actions masih bisa mencoba cache Composer lain untuk OS dan versi PHP yang sama. Untuk cache download Composer, pendekatan ini umumnya aman karena Composer tetap memverifikasi dependency yang dibutuhkan saat install.

Namun untuk cache vendor, penggunaan restore-keys perlu lebih hati-hati. Cache lama yang "mirip" belum tentu valid untuk state dependency terbaru.

Kapan cache Composer membantu, dan kapan cache vendor berbahaya

Cache Composer: pilihan default yang aman

Cache Composer membantu terutama saat:

  • job CI sering berjalan pada pull request,
  • dependency cukup banyak,
  • job terpisah memerlukan composer install masing-masing,
  • runner bersifat ephemeral sehingga tidak ada state lokal antar-run.

Cache ini mempercepat pengunduhan paket, tetapi Composer tetap membangun vendor dari lock file yang ada. Karena itu, hasilnya cenderung konsisten.

Cache vendor: cepat, tetapi harus disiplin

Menyimpan vendor atau artifact hasil install bisa mengurangi waktu lebih jauh, terutama jika beberapa job membutuhkan dependency yang sama. Tetapi pendekatan ini aman hanya jika:

  • key memasukkan composer.lock, OS, dan versi PHP,
  • job penerima menggunakan lingkungan yang kompatibel,
  • Anda memahami efek script Composer dan file generated lain,
  • dependency tidak bergantung pada state yang berubah antar-job.

Masalah umum cache vendor:

  • Autoload stale setelah perubahan dependency.
  • Binary tool tidak cocok dengan versi PHP berbeda dalam matrix.
  • State tersisa dari job sebelumnya jika folder dimodifikasi setelah install.
  • False green: CI lolos karena memakai vendor lama yang kebetulan masih jalan.

Jika prioritas Anda adalah stabilitas, gunakan cache download Composer. Jika prioritas Anda adalah kecepatan maksimal, Anda boleh mengevaluasi cache vendor atau artifact, tetapi buat key sangat spesifik dan hindari memakainya lintas versi PHP.

Kapan artifact lebih cocok daripada cache

Untuk berbagi hasil install antar-job dalam satu workflow run, artifact kadang lebih tepat daripada cache. Cache dirancang untuk dipakai ulang antar-run, sedangkan artifact lebih cocok untuk memindahkan output dari satu job ke job lain pada run yang sama.

Contoh kapan artifact masuk akal:

  • Anda punya job awal yang melakukan composer install.
  • Job berikutnya pada versi PHP yang sama hanya perlu memakai hasil itu.
  • Anda ingin menghindari instalasi ulang dalam run yang sama.

Tetapi tetap ada trade-off: upload/download artifact juga memerlukan waktu. Untuk project kecil, langkah tambahan ini sering tidak memberi keuntungan nyata.

Strategi matrix PHP yang efisien

Gunakan fail-fast dengan sadar

Pada contoh workflow, kita memakai:

strategy:
  fail-fast: false

Jika fail-fast bernilai false, kegagalan pada satu versi PHP tidak langsung membatalkan kombinasi lain. Ini berguna saat Anda ingin melihat cakupan kegagalan penuh, misalnya apakah bug hanya terjadi di PHP tertentu.

Trade-off-nya: runner tetap menghabiskan waktu sampai seluruh matrix selesai. Untuk repository besar dengan antrean CI panjang, sebagian tim memilih fail-fast: true agar feedback awal lebih cepat. Pilihan terbaik bergantung pada tujuan pipeline:

  • Feedback lengkap lintas versi → pakai false.
  • Hentikan biaya CI secepat mungkin saat sudah jelas gagal → pertimbangkan true.

Jangan mematrix semua job

Kesalahan umum adalah menjalankan Pint, PHPStan, dan PHPUnit di semua versi PHP. Hasilnya, jumlah job meledak tanpa manfaat sebanding. Untuk sebagian besar project Laravel:

  • Pint cukup satu versi.
  • PHPStan cukup satu versi yang sesuai baseline tim atau target produksi utama.
  • PHPUnit yang paling layak dimatrix.

Pilih versi PHP berdasarkan dukungan nyata

Jangan menambah versi PHP ke matrix hanya karena tersedia. Masukkan hanya versi yang memang didukung oleh aplikasi dan dependency Anda. Matrix yang terlalu luas memperlambat CI dan menambah noise debugging.

Integrasi dasar dengan PHPUnit, Pint, dan PHPStan

PHPUnit melalui Artisan atau binary langsung

Di project Laravel, php artisan test biasanya nyaman dipakai karena terintegrasi dengan pengalaman Laravel. Jika Anda butuh opsi spesifik PHPUnit, Anda juga bisa memanggil binary secara langsung. Yang penting, command CI harus sama atau mendekati command yang dipakai tim sehari-hari agar perilaku lebih konsisten.

Pint sebagai quality gate cepat

./vendor/bin/pint --test adalah pilihan umum untuk memastikan style sesuai aturan tanpa mengubah file di CI. Menempatkan Pint pada job terpisah membuat feedback style muncul cepat dan tidak tertutup oleh log test yang panjang.

PHPStan untuk bug yang tidak tertangkap test

PHPStan berguna untuk menemukan masalah tipe, penggunaan API yang keliru, dan asumsi yang rapuh. Dalam CI, pastikan baseline dan konfigurasi sudah stabil di repository. Hindari menjalankan analisis dengan konfigurasi yang berbeda antara lokal dan CI, karena itu sering memunculkan hasil yang membingungkan.

Menjaga waktu CI tetap rendah saat project membesar

1. Kurangi duplikasi setup

Jika banyak job memiliki langkah yang sama, pertimbangkan ekstraksi ke reusable workflow atau composite action internal. Tujuannya bukan sekadar rapi, tetapi agar perubahan setup dilakukan satu kali dan tidak menyebabkan inkonsistensi antar-job.

2. Evaluasi dependency dev yang berat

Semakin banyak tool analisis, semakin lama composer install. Pastikan semua dependency dev memang bernilai. Tool yang tidak dipakai aktif akan tetap membebani CI.

3. Jalankan jenis pekerjaan pada frekuensi yang tepat

Tidak semua pemeriksaan harus berjalan pada setiap event dengan tingkat yang sama. Contohnya, job dasar bisa berjalan pada setiap pull request, sementara pemeriksaan lebih mahal dijalankan pada push ke branch utama atau jadwal tertentu. Ini keputusan operasional yang bergantung pada kebutuhan tim.

4. Hindari kerja yang tidak perlu dalam job test

Untuk job PHPUnit yang tidak memerlukan browser, jangan tambahkan layanan dan setup tambahan yang tidak dipakai. Semakin sedikit langkah, semakin rendah peluang gagal karena faktor non-test.

5. Pantau job paling mahal

Lihat run history dan identifikasi job yang paling sering memakan waktu. Optimasi terbaik hampir selalu datang dari bottleneck nyata, bukan dari semua bagian sekaligus.

Debugging saat cache membuat hasil CI tidak konsisten

Jika Anda melihat gejala seperti "lokal lolos, CI kadang gagal" atau hasil berbeda antar-run, periksa area berikut:

  • Apakah key cache cukup spesifik? Sertakan OS, versi PHP, dan composer.lock.
  • Apakah Anda memakai restore-keys terlalu longgar? Ini aman untuk Composer download cache, tetapi berisiko untuk vendor.
  • Apakah folder cache berisi hasil generated file? File generated bisa stale.
  • Apakah command install selalu dijalankan? Cache seharusnya membantu, bukan menggantikan validasi dependency.
  • Apakah matrix membagi versi PHP yang berbeda dengan cache sama? Ini sumber mismatch yang sangat umum.

Langkah debugging praktis:

  1. Matikan sementara cache pada job yang bermasalah.
  2. Bandingkan hasilnya dengan run yang memakai cache.
  3. Jika stabil tanpa cache, perketat key atau ubah strategi menjadi hanya cache Composer download.
  4. Pastikan tidak ada file dalam workspace yang dimodifikasi lalu ikut tersimpan sebagai cache/artifact tanpa sengaja.

Contoh kapan memakai artifact vendor dengan aman

Jika Anda benar-benar ingin menghindari composer install berulang pada beberapa job dengan versi PHP yang sama, pola berikut bisa dipertimbangkan:

  • Satu job prepare-dependencies melakukan install.
  • Folder vendor di-upload sebagai artifact.
  • Job lint dan static-analysis yang memakai versi PHP sama mengunduh artifact tersebut.

Tetapi jangan campur artifact vendor itu ke job test dengan versi PHP lain dalam matrix. Ini justru membuka peluang error yang sulit dilacak.

Jika Anda memilih pola ini, dokumentasikan dengan jelas alasan dan batasannya di repository agar tim paham bahwa artifact tersebut hanya valid untuk kombinasi environment tertentu.

Checklist implementasi

  • Gunakan workflow terpisah atau job terpisah untuk Pint, PHPStan, dan PHPUnit.
  • Gunakan Composer download cache sebagai optimasi awal yang aman.
  • Buat key cache dengan kombinasi OS + versi PHP + hash composer.lock.
  • Gunakan restore-keys untuk Composer cache secara konservatif.
  • Jalankan matrix PHP terutama pada job test, bukan semua job.
  • Tentukan fail-fast sesuai kebutuhan: hemat waktu atau ingin laporan lengkap lintas versi.
  • Hindari berbagi vendor antar-job yang memakai versi PHP berbeda.
  • Pastikan command CI serupa dengan command yang dipakai developer secara lokal.
  • Tinjau ulang durasi job secara berkala saat dependency dan jumlah test bertambah.

Kesalahan umum yang perlu dihindari

  • Meng-cache vendor dengan key terlalu umum.
  • Menjalankan semua job pada semua versi PHP.
  • Mengandalkan cache sebagai pengganti install yang benar.
  • Tidak memasukkan composer.lock ke key cache.
  • Mengaktifkan terlalu banyak langkah setup di job yang sederhana.
  • Tidak memisahkan lint, static analysis, dan test.
  • Menganggap cache selalu mempercepat. Pada project kecil, kompleksitas cache tambahan kadang tidak sepadan.

Penutup

GitHub Actions untuk Laravel yang cepat dan konsisten biasanya dibangun dari keputusan kecil yang disiplin: cache Composer dengan key yang benar, matrix PHP hanya untuk job yang memang perlu, dan pemisahan lint, static analysis, serta test agar sinyal CI jelas. Mulailah dari setup yang sederhana dan stabil, lalu optimalkan berdasarkan bottleneck nyata.

Jika Anda ragu antara kecepatan dan konsistensi, pilih konsistensi terlebih dahulu. CI yang sedikit lebih lambat tetapi deterministik hampir selalu lebih berharga daripada pipeline cepat yang sesekali memberi hasil menyesatkan.