Seluruh perangkat yang kita bangun di Section ini — PHPUnit, PHPStan, PHP-CS-Fixer — masih bergantung pada satu hal yang rapuh: seseorang harus ingat menjalankannya. Continuous integration (CI) menghapus ketergantungan itu: sebuah server menonton repositori, dan setiap push atau pull request otomatis memicu eksekusi rangkaian perintah yang sama di lingkungan bersih. Hasil akhirnya satu kata: hijau atau merah. Chapter penutup Section ini menyatukan lint, static analysis, dan test menjadi pipeline utuh di GitHub Actions, lengkap dengan matriks versi PHP, artifact, proteksi branch, dan cermin lokalnya di laptop setiap anggota tim.
Konsep Pipeline #
Pipeline adalah daftar tahapan yang dijalankan berurutan pada setiap perubahan, berhenti pada kegagalan pertama. Karena berjalan di lingkungan segar tanpa sisa-sisa komputer siapa pun, pipeline mematikan alasan klasik “di komputer saya jalan”: bila perubahan lolos di sana, ia lolos di kondisi awal yang sama dengan siapa pun yang meng-clone repositori. Untuk proyek PHP yang telah mengikuti chapter-chapter sebelumnya, tahapan pipeline beserta perannya terpetakan begini:
| Tahap | Perintah contoh | Yang dijaga |
|---|---|---|
| install | composer install | dependensi terpasang persis seperti composer.lock |
| lint | vendor/bin/php-cs-fixer fix --dry-run --diff | konsistensi gaya kode |
| static analysis | vendor/bin/phpstan analyse --no-progress | kontrak tipe dan simbol |
| test | vendor/bin/phpunit | perilaku aplikasi |
Setiap tahap berakhir dengan exit code: nol berarti lanjut ke tahap berikutnya, bukan nol berarti seluruh pipeline merah. Prinsipnya sesederhana itu — dan kita bisa memegangnya dengan tangan sendiri.
Runner Stage Mini #
Mari kita buat mesin pipeline dalam ukuran mainan: sebuah fungsi yang menerima daftar tahap dan sebuah executor yang mengembalikan exit code tiap tahap. Fungsi ini menjalankan tahap satu per satu dan berhenti pada kegagalan pertama — persis kontrak yang dipegang server CI:
<?php
function jalankanPipeline(array $tahapan, callable $eksekusi): int
{
foreach ($tahapan as $tahap) {
echo "== $tahap: mulai\n";
if ($eksekusi($tahap) !== 0) {
echo "== $tahap: GAGAL - pipeline berhenti di tahap ini\n";
return 1;
}
echo "== $tahap: lulus\n";
}
echo "Pipeline hijau: seluruh tahap lulus\n";
return 0;
}
$simulasiMerah = [
'lint' => 0,
'static-analysis' => 0,
'test' => 1,
];
echo "-- Simulasi pull request MERAH --\n";
$kodeKeluar = jalankanPipeline(
array_keys($simulasiMerah),
fn (string $tahap): int => $simulasiMerah[$tahap],
);
echo "Exit code: $kodeKeluar\n";
echo "\n";
$simulasiHijau = [
'lint' => 0,
'static-analysis' => 0,
'test' => 0,
];
echo "-- Simulasi pull request HIJAU --\n";
$kodeKeluar = jalankanPipeline(
array_keys($simulasiHijau),
fn (string $tahap): int => $simulasiHijau[$tahap],
);
echo "Exit code: $kodeKeluar\n";Output yang Dihasilkan:
-- Simulasi pull request MERAH --
== lint: mulai
== lint: lulus
== static-analysis: mulai
== static-analysis: lulus
== test: mulai
== test: GAGAL - pipeline berhenti di tahap ini
Exit code: 1
-- Simulasi pull request HIJAU --
== lint: mulai
== lint: lulus
== static-analysis: mulai
== static-analysis: lulus
== test: mulai
== test: lulus
Pipeline hijau: seluruh tahap lulus
Exit code: 0Mari kita bedah bagiannya:
jalankanPipeline()mengiterasi tahap secara berurutan; exit code dari executor diperiksa untuk tiap tahap, dan nilai selain nol langsung menghentikan seluruh pipeline dengan nilai balik1.$simulasiMerahmemetakan tiap tahap ke exit code mainan: lint dan static analysis lulus, test gagal.- Closure
fn (string $tahap): int => ...berperan sebagai executor — dalam CI sungguhan, peran ini diisi proses nyata sepertivendor/bin/phpunit. - Simulasi kedua memakai peta yang sama dengan seluruh nilai nol, membuktikan jalur hijau sampai ke baris terakhir.
Perhatikan bahwa urutan bekerja seperti dirancang pada chapter-chapter sebelumnya: kegagalan di tahap test baru terasa setelah lint dan static analysis lulus. Pentingnya urutan terlihat jelas kalau kita membayangkan kebalikannya — test yang berjalan menit-menit lebih dulu akan terbuang sia-sia hanya untuk menemukan pelanggaran spasi yang bisa terdeteksi dalam dua detik.
Workflow GitHub Actions #
GitHub Actions membaca berkas YAML di .github/workflows/. Workflow berikut menjalankan ketiga tahap kita pada tiga versi PHP sekaligus:
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ['8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: none
- uses: ramsey/composer-install@v3
- name: Lint
run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Static analysis
run: vendor/bin/phpstan analyse --no-progress
- name: Test
run: vendor/bin/phpunitBagian-bagian kuncinya:
on:menentukan pemicu: setiap push kemaindan setiap pull request — termasuk yang belum dibuka reviewer mana pun.strategy.matrix.php-versionmenggandakan job menjadi tiga salinan (PHP 8.2, 8.3, dan 8.4) dengan${{ matrix.php-version }}menyuntikkan nilai tiap salinan. Pemutakhiran versi menjadi aman di sini: deprecation baru di 8.4 tertangkap jauh sebelum tim produksi benar-benar pindah versi.fail-fast: falsemembuat ketiga salinan tetap berjalan sampai selesai walau satu sudah merah, sehingga kita melihat gambaran penuh, bukan hanya kegagalan pertama.setup-php@v2adalah action standar ekosistem PHP untuk memasang PHP versi mana pun;coverage: nonemelewatkan driver coverage yang tidak dibutuhkan tahap-tahap ini.ramsey/composer-install@v3menjalankancomposer installdengan cache unduhan yang dikunci pada hashcomposer.lock— eksekusi kedua dan seterusnya nyaris tanpa unduhan.- Tiga langkah terakhir adalah pipeline kita sendiri: lint, static analysis, test — masing-masing membuat job merah begitu exit code-nya bukan nol.
Artifact untuk Otopsi #
Pipeline yang merah meninggalkan satu pertanyaan: “persisnya apa yang gagal?” Log mentah menjawab sebagian, tetapi laporan terstruktur jauh lebih enak diotopsi. Artifact adalah berkas yang diunggah oleh sebuah eksekusi dan dapat diunduh dari halaman run — misalnya laporan JUnit dari PHPUnit. Tambahkan dua langkah berikut menggantikan langkah test sebelumnya:
- name: Test
run: vendor/bin/phpunit --log-junit phpunit-junit.xml
- name: Unggah laporan JUnit
if: always()
uses: actions/upload-artifact@v4
with:
name: phpunit-${{ matrix.php-version }}
path: phpunit-junit.xmlFlag --log-junit membuat PHPUnit menulis laporan terstruktur — tiap test beserta statusnya — ke berkas phpunit-junit.xml. Kondisi if: always() memastikan laporan tetap diunggah justru ketika test gagal, saat laporan paling dibutuhkan. Nama artifact menyertakan versi PHP dari matriks agar ketiga salinan tidak saling menimpa.
Proteksi Branch dan Disiplin Hijau #
Pipeline hanya menjaga kalau hasilnya tidak bisa dilewati. GitHub menyediakan branch protection untuk itu: pada branch main, jadikan ketiga pemeriksaan (lint, static analysis, test) sebagai required status checks, larang force push, dan wajibkan pull request selalu mutakhir sebelum merge. Setelah pengaturan itu aktif, tombol merge berubah menjadi gerbang — merah berarti terkunci.
Aturan di atas baru benar-benar bekerja kalau disertai disiplin never-green-merge: tidak pernah merge saat pipeline merah atau masih berjalan, dan tidak pernah “re-run sampai hijau” tanpa membaca penyebab kegagalannya. Kegagalan flaky boleh dijalankan ulang, tetapi kegagalan yang tidak dipahami harus dibaca dulu; kalau tidak, yang dijalankan ulang bukan pipeline, melainkan keberuntungan.
Paritas Lokal #
Pipeline yang paling rapi sekalipun menimbulkan gesekan kalau perintah di CI berbeda dengan perintah di laptop. Solusinya adalah satu sumber kebenaran: definisikan seluruh tahap sebagai Composer script, lalu biarkan CI dan manusia memanggil hal yang sama:
{
"scripts": {
"lint": "php-cs-fixer fix --dry-run --diff",
"sa": "phpstan analyse --no-progress",
"test": "phpunit",
"ci": [
"@lint",
"@sa",
"@test"
]
}
}Composer otomatis menempatkan vendor/bin ke PATH saat menjalankan script, sehingga nama pendek tanpa path sudah cukup. Kini composer ci di laptop menjalankan urutan yang persis sama dengan yang dijalankan server, dan workflow di atas bisa diringkas menjadi satu langkah run: composer ci. Bila tim lebih nyaman dengan make, bentuk Makefile yang setara terlihat begini:
.PHONY: ci lint sa test
ci: lint sa test
lint:
vendor/bin/php-cs-fixer fix --dry-run --diff
sa:
vendor/bin/phpstan analyse --no-progress
test:
vendor/bin/phpunit
Target ci bergantung pada ketiga tahap sehingga make ci menjalankan semuanya dalam urutan yang benar. Command runner seperti just menawarkan kemampuan serupa dengan sintaks berbeda — pilihan alat mana pun sah, selama satu perintah lokal bermakna sama dengan satu eksekusi CI.
Ringkasan #
Continuous integration menyatukan seluruh perangkat QA Section ini menjadi satu kontrak: setiap push menjalankan install, lint, static analysis, dan test di lingkungan bersih; matriks versi PHP mengawal pemutakhiran; artifact menyimpan laporan untuk otopsi; proteksi branch memastikan hasil tidak bisa dilewati; dan Composer script menjaga paritas lokal. Runner mini kita menunjukkan inti mekanismenya: tahap berurutan, berhenti pada kegagalan pertama, satu exit code sebagai vonis. Dengan ini rangkaian Testing dan Quality Assurance tertutup — kualitas bukan lagi kegiatan manual, melainkan properti yang terpasang di alur kerja. Tentu saja pipeline yang merah hanya memberi tahu bahwa ada yang salah, bukan di mana akar masalahnya bersembunyi; mencarinya adalah pekerjaan Section berikutnya: Debugging dan Observability.
Referensi #
- Dokumentasi GitHub Actions: https://docs.github.com/en/actions
- Composer scripts: https://getcomposer.org/doc/articles/scripts.md
- Dokumentasi PHPUnit: https://docs.phpunit.de/en/12.x/