Jika tim Anda memakai Laravel dan ingin mengurangi waktu tunggu saat review pull request, kombinasi Laravel + GitHub Actions adalah titik mulai yang sangat efektif. Tujuannya bukan membuat pipeline yang kompleks, tetapi memastikan setiap perubahan otomatis diperiksa dengan cepat: format code lewat Pint, eksekusi test lewat Pest atau PHPUnit, lalu simpan hasil coverage sebagai artefak atau ringkasan job.

Artikel ini fokus pada setup CI yang realistis untuk repo Laravel: Composer install dengan cache, job lint dan test yang dipisah, konfigurasi .env.testing, dan pilihan kapan memakai paralel atau fail-fast. Hasil akhirnya adalah pipeline yang cukup cepat untuk dipakai harian, tetapi tetap mudah dipahami dan dirawat oleh tim kecil.

Mengapa pipeline CI Laravel perlu dipisah antara lint dan test

Kesalahan style dan kegagalan test adalah dua jenis masalah yang berbeda. Jika keduanya dijalankan dalam satu job panjang, developer harus menunggu lebih lama hanya untuk mengetahui bahwa masalahnya sebenarnya sederhana, misalnya format code belum sesuai standar.

Karena itu, pendekatan yang umum dan praktis adalah:

  • Job lint untuk menjalankan Laravel Pint.
  • Job test untuk menjalankan Pest atau PHPUnit.
  • Coverage dikumpulkan di job test, lalu diunggah sebagai artefak atau ditulis ke summary.

Pemisahan ini memberi beberapa keuntungan:

  • Feedback lebih cepat untuk masalah style.
  • Log lebih mudah dibaca karena tiap job punya tujuan tunggal.
  • Retry lebih hemat, karena Anda tidak perlu mengulang seluruh pipeline jika hanya satu tahap yang gagal.

Untuk tim kecil, workflow sederhana yang mudah dipelihara hampir selalu lebih bernilai daripada pipeline yang sangat canggih tetapi sulit dipahami saat gagal.

Struktur minimum repo Laravel untuk CI

Sebelum menulis workflow, pastikan repo Laravel sudah memiliki komponen minimum berikut:

  • composer.json dan composer.lock yang konsisten.
  • Laravel Pint terpasang, biasanya sebagai dependency development.
  • Pest atau PHPUnit sudah bisa dijalankan secara lokal.
  • File .env.testing untuk konfigurasi environment test.
  • Jika test memakai database, siapkan konfigurasi yang cocok untuk CI, misalnya SQLite.

Contoh file .env.testing yang ringan untuk CI:

APP_ENV=testing
APP_KEY=base64:SomeGeneratedKeyHere=
APP_DEBUG=true
CACHE_DRIVER=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
MAIL_MAILER=array
DB_CONNECTION=sqlite
DB_DATABASE=:memory:

Poin penting dari file ini:

  • Driver array/sync mengurangi dependensi eksternal saat test.
  • SQLite in-memory sering menjadi pilihan paling cepat jika test Anda kompatibel.
  • APP_KEY tetap perlu ada jika sebagian test menyentuh enkripsi, session, atau fitur Laravel tertentu.

Namun ada trade-off. Jika aplikasi Anda sangat bergantung pada perilaku database tertentu, SQLite bisa berbeda dengan MySQL atau PostgreSQL. Dalam kasus seperti itu, lebih aman memakai service database di CI meskipun durasinya lebih lama.

Workflow GitHub Actions yang praktis untuk Laravel + GitHub Actions

Berikut contoh workflow yang cukup realistis untuk banyak proyek Laravel. Workflow ini:

  • Jalan saat push dan pull request.
  • Memisahkan job lint dan test.
  • Memakai cache untuk dependency Composer.
  • Memakai matrix PHP sederhana pada job test.
  • Menghasilkan file coverage dan mengunggahnya sebagai artefak.
  • Menulis ringkasan coverage ke job summary bila file tersedia.
name: ci

on:
  push:
    branches:
      - main
      - develop
  pull_request:

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

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

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

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

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

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

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

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

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

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

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

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

      - name: Run migrations
        run: php artisan migrate --env=testing --force

      - name: Run tests with coverage
        run: php artisan test --coverage-clover=coverage.xml

      - name: Upload coverage artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-php-${{ matrix.php }}
          path: coverage.xml
          if-no-files-found: ignore

      - name: Add coverage summary
        if: always()
        run: |
          if [ -f coverage.xml ]; then
            echo "## Coverage PHP ${{ matrix.php }}" >> $GITHUB_STEP_SUMMARY
            echo "File coverage tersimpan sebagai artifact: coverage-php-${{ matrix.php }}" >> $GITHUB_STEP_SUMMARY
          else
            echo "## Coverage PHP ${{ matrix.php }}" >> $GITHUB_STEP_SUMMARY
            echo "File coverage tidak ditemukan." >> $GITHUB_STEP_SUMMARY
          fi

Kenapa workflow di atas cukup efektif

Ada beberapa keputusan penting di workflow tersebut:

  • Lint tidak memakai coverage, karena coverage hanya memperlambat job yang sebenarnya tidak membutuhkannya.
  • Matrix PHP hanya di job test, sehingga biaya runtime tetap terkendali.
  • composer install --prefer-dist biasanya lebih cocok untuk CI karena mengambil distribusi paket yang lebih cepat dibanding source.
  • Cache Composer membantu mengurangi waktu unduh dependency antar run.
  • fail-fast: false pada matrix test berguna jika Anda ingin melihat hasil semua versi PHP sekaligus.

Jika Anda hanya mendukung satu versi PHP aktif di proyek, matrix bisa dihilangkan agar CI lebih cepat dan biaya eksekusi lebih rendah.

Memahami bagian penting YAML dan alasannya

Trigger: push dan pull_request

Menjalankan CI saat pull_request penting untuk validasi sebelum merge. Menjalankan juga saat push ke branch utama atau branch kerja memberi feedback lebih cepat untuk perubahan langsung.

Setup PHP

Action shivammathur/setup-php umum dipakai untuk workflow PHP karena mendukung setup versi PHP, extension, Composer, dan coverage driver. Dalam contoh di atas, lint tidak membutuhkan Xdebug, sedangkan test coverage membutuhkannya.

Trade-off-nya jelas: coverage hampir selalu menambah durasi test. Karena itu, beberapa tim memilih hanya menjalankan coverage di satu versi PHP saja, bukan di semua matrix.

Cache dependency Composer

Ada dua pendekatan cache yang sering dipakai:

  • Cache direktori unduhan Composer, yaitu hasil paket yang diunduh oleh Composer.
  • Cache folder vendor, yaitu hasil instalasi dependency.

Untuk banyak proyek, cache direktori Composer lebih aman dan lebih stabil. Alasannya:

  • Lebih kecil risiko mismatch karena struktur vendor yang tidak cocok dengan environment runner.
  • Tetap membiarkan composer install membangun folder vendor secara bersih.
  • Lebih mudah di-invalidasi lewat perubahan composer.lock.

Cache folder vendor bisa lebih cepat, tetapi lebih rentan menghasilkan perilaku aneh jika extension, platform PHP, atau file lock berubah. Untuk tim kecil, saya biasanya menyarankan mulai dari cache unduhan Composer dulu.

.env.testing dan environment test

Laravel mendukung environment testing, tetapi CI tetap perlu eksplisit. Menyalin .env.testing ke .env saat job berjalan membantu memastikan command artisan dan test memakai konfigurasi yang sesuai.

Kesalahan umum di sini adalah:

  • Tidak menyiapkan APP_KEY, sehingga beberapa test gagal dengan pesan yang tampak tidak terkait.
  • Masih memakai service eksternal seperti Redis, mail server, atau queue async padahal sebenarnya tidak diperlukan untuk unit/integration test.
  • DB testing tidak sinkron dengan asumsi migration atau factory.

Fail-fast vs paralel: kapan dipakai

Di GitHub Actions, dua keputusan ini cukup memengaruhi pengalaman tim:

Paralel job

Jika lint dan test dipisah sebagai job terpisah, keduanya bisa berjalan paralel. Ini hampir selalu pilihan yang baik karena mempercepat feedback total tanpa menambah kompleksitas besar.

fail-fast pada matrix

Untuk matrix PHP, ada dua pendekatan:

  • fail-fast: true — hentikan kombinasi lain saat salah satu gagal.
  • fail-fast: false — biarkan semua kombinasi selesai.

Pilih fail-fast: true jika Anda ingin menghemat menit CI dan biasanya cukup peduli pada sinyal gagal pertama. Pilih fail-fast: false jika Anda ingin visibilitas penuh, misalnya untuk mengetahui bahwa bug hanya muncul di PHP tertentu.

Untuk tim kecil, kompromi yang masuk akal adalah:

  • Lint dan test berjalan paralel.
  • Matrix test dipakai hanya jika memang perlu.
  • Coverage dijalankan di satu versi PHP saja bila durasi mulai terasa berat.

Integrasi minimum di repo Laravel

Supaya workflow di atas bisa langsung dipakai, langkah integrasi minimumnya biasanya seperti berikut:

  1. Pastikan Pint tersedia di project.
  2. Pastikan test dapat dijalankan lokal dengan php artisan test atau ./vendor/bin/pest.
  3. Buat file .env.testing yang aman untuk CI.
  4. Simpan workflow ke .github/workflows/ci.yml.
  5. Buka pull request kecil untuk memvalidasi log CI pertama.

Contoh command lokal yang sebaiknya lolos sebelum mendorong ke CI:

composer install
./vendor/bin/pint --test
php artisan test

Jika Anda memakai Pest secara langsung, Anda juga bisa mengganti perintah test di workflow menjadi:

./vendor/bin/pest --coverage-clover=coverage.xml

Pilih salah satu gaya yang paling konsisten dengan kebiasaan tim. Jika mayoritas developer terbiasa memakai php artisan test, gunakan itu agar command lokal dan command CI tidak berbeda terlalu jauh.

Bottleneck umum di CI Laravel dan cara menguranginya

1. composer install lambat

Penyebab paling umum adalah cache tidak efektif, dependency terlalu besar, atau lock file sering berubah. Pastikan:

  • Cache key memakai hash dari composer.lock.
  • Tidak ada langkah yang membuang cache secara tidak perlu.
  • Anda memakai --prefer-dist untuk CI.

2. Coverage membuat test jauh lebih lambat

Ini normal. Instrumentasi coverage menambah overhead. Solusi pragmatis:

  • Jalankan coverage hanya pada satu versi PHP.
  • Pisahkan job test biasa dan job coverage jika pipeline mulai berat.
  • Jangan aktifkan coverage pada job lint.

3. Test database tidak stabil

Jika migration atau seeding tidak idempotent, CI akan gagal secara acak. Periksa apakah:

  • Migration bisa dijalankan dari nol tanpa asumsi state lama.
  • Factory tidak bergantung pada data global yang berubah-ubah.
  • Setiap test membersihkan state dengan benar.

4. Perbedaan lokal vs CI

Masalah klasiknya adalah test lulus lokal tetapi gagal di GitHub Actions. Langkah debug yang biasanya membantu:

  • Cek versi PHP dan extension yang dipakai lokal vs CI.
  • Cek apakah .env.testing benar-benar dipakai.
  • Cetak informasi dasar saat debugging, misalnya driver database aktif.
  • Jalankan command yang sama persis seperti di workflow.

5. Log sulit dibaca

Jika semua dijalankan dalam satu script panjang, sumber gagal sulit ditemukan. Itu sebabnya memisahkan langkah seperti install, prepare environment, migrate, dan test sangat membantu saat investigasi.

Kapan perlu matrix PHP, kapan tidak

Matrix PHP berguna jika aplikasi Anda memang mendukung lebih dari satu versi PHP secara aktif, atau Anda sedang transisi versi runtime. Namun matrix juga menggandakan waktu eksekusi.

Gunakan satu versi PHP jika:

  • Tim kecil dan ingin feedback tercepat.
  • Environment production hanya satu versi.
  • Anda belum punya kebutuhan kompatibilitas lintas versi.

Gunakan matrix sederhana jika:

  • Paket atau aplikasi perlu mendukung beberapa versi PHP.
  • Anda ingin mendeteksi incompatibility lebih awal.
  • Biaya runtime CI masih bisa diterima.

Pendekatan yang sering efektif adalah menjalankan lint sekali, test di beberapa versi PHP, dan coverage hanya di salah satu versi utama.

Checklist adopsi untuk tim kecil

Jika Anda ingin mulai tanpa membuat pipeline berlebihan, pakai checklist berikut:

  • Sudah ada workflow CI dasar di .github/workflows/ci.yml.
  • Lint dipisah dari test agar feedback style lebih cepat.
  • Composer memakai cache berbasis composer.lock.
  • .env.testing tersedia dan cocok untuk CI.
  • Test bisa jalan tanpa dependensi eksternal yang tidak perlu.
  • Coverage diunggah sebagai artifact atau minimal ditulis di job summary.
  • Matrix PHP hanya dipakai jika ada kebutuhan nyata.
  • Tim sepakat command lokal = command CI sejauh mungkin.

Penutup

Setup Laravel + GitHub Actions yang baik tidak harus rumit. Untuk sebagian besar proyek, kombinasi cache Composer, job lint dan test terpisah, .env.testing yang ringan, serta coverage sebagai artifact sudah cukup untuk mempercepat feedback tim secara signifikan.

Mulailah dari workflow kecil yang stabil, lalu optimalkan hanya saat ada bottleneck nyata. Dengan pendekatan itu, CI tetap cepat, mudah dipahami, dan tidak menjadi beban baru saat tim sedang fokus mengirim perubahan ke aplikasi Laravel Anda.