CI/CD hemat biaya bukan berarti mengurangi kualitas, tetapi mengurangi komputasi yang tidak memberi nilai. Jika setiap pull request selalu menjalankan lint, unit test, integration test, build, dan security scan penuh tanpa melihat perubahan file, biaya runner, waktu tunggu, dan antrean pipeline akan cepat membesar.
Prinsipnya sederhana: jalankan pemeriksaan yang relevan dengan perubahan. Perubahan pada dokumentasi tidak perlu memicu integration test database. Perubahan file frontend tidak selalu perlu membangun image backend. Dengan selective execution, tim kecil-menengah bisa memangkas menit komputasi CI/CD tanpa kehilangan kontrol mutu, selama aturan wajib dan cakupan pengujian dirancang dengan jelas.
Konteks “AI's Affordability Crisis” memberi pengingat yang relevan: komputasi mahal harus dipakai dengan disiplin. Pipeline CI/CD juga termasuk beban komputasi yang mudah boros jika semua dijalankan terus-menerus.
Mengapa pipeline CI/CD sering boros
Pola pemborosan yang paling umum adalah menjalankan semua job untuk semua event:
- Setiap push memicu lint, test, build, dan scan penuh.
- Semua path dianggap sama penting, padahal perubahan
READMEberbeda dampaknya dengan perubahansrc/ataumigrations/. - Matriks berlebihan, misalnya menguji semua versi runtime di setiap pull request.
- Tidak ada cache, sehingga dependency diunduh ulang di setiap run.
- Tidak ada concurrency cancel, sehingga commit lama tetap diproses walau sudah digantikan commit baru.
- Build diulang di beberapa job, padahal artifact yang sama bisa dipakai ulang.
Dalam banyak repo web dan backend, pemborosan ini lebih terasa karena pipeline biasanya menyentuh dependency manager, database service, bundler frontend, image build, atau scanner keamanan yang memakan waktu.
Prinsip selective execution untuk CI/CD hemat biaya
1. Klasifikasikan job berdasarkan risiko
Jangan mulai dari tool, mulai dari tingkat risiko perubahan:
- Risiko rendah: dokumentasi, file konfigurasi editor, aset non-eksekusi.
- Risiko sedang: perubahan frontend, utilitas kecil, unit logic.
- Risiko tinggi: perubahan dependency, autentikasi, migrasi database, workflow deployment, konfigurasi container, infrastruktur, dan security-sensitive code.
Dari sini, tentukan job mana yang wajib dan mana yang opsional.
2. Pakai path filter, branch rule, dan jenis event
Tiga sinyal utama untuk memutuskan job:
- Path/file yang berubah: misalnya
frontend/**,backend/**,db/migrations/**. - Branch atau target merge: pull request ke
mainbiasanya lebih ketat daripada branch fitur. - Jenis event:
pull_request,push,schedule, atauworkflow_dispatch.
Dengan kombinasi ini, Anda bisa membuat aturan seperti:
- Lint dijalankan untuk hampir semua perubahan kode.
- Unit test dijalankan hanya untuk area yang terpengaruh.
- Integration test dijalankan jika ada perubahan backend, migration, contract API, atau dependency tertentu.
- Security scan penuh dijalankan saat merge ke branch utama atau terjadwal harian.
- Build produksi dijalankan saat merge ke branch release atau main.
3. Pisahkan job wajib dan opsional
Job wajib sebaiknya menjadi required checks di aturan merge. Job opsional tetap berguna, tetapi tidak boleh menghambat perubahan yang risiko teknisnya rendah.
Contoh pembagian:
- Wajib pada pull request: lint area berubah, unit test relevan, validasi format, dan pemeriksaan dasar dependency lockfile.
- Opsional pada pull request: integration test penuh, build multi-platform, scan keamanan mendalam.
- Wajib saat merge ke main: integration test, build artifact produksi, scan keamanan penting.
Strategi kapan lint, test, build, dan scan dijalankan
Lint
Lint murah dibanding integration test, jadi biasanya layak dijalankan lebih sering. Namun tetap bisa difilter agar tidak berjalan untuk perubahan dokumentasi murni.
Jalankan lint saat:
- Ada perubahan di direktori aplikasi seperti
src/,app/,backend/,frontend/. - Ada perubahan pada file konfigurasi lint seperti
.eslintrc,pyproject.toml,composer.json, ataupackage.jsonbila memengaruhi toolchain.
Tidak perlu lint penuh saat:
- Hanya
docs/**,README.md, atau file non-kode berubah.
Unit test
Unit test sebaiknya mengikuti modul yang berubah. Pada repo yang sudah terstruktur baik, ini bisa dibagi minimal antara frontend dan backend.
Jalankan unit test frontend saat:
frontend/**berubah.- Konfigurasi bundler atau dependency frontend berubah.
Jalankan unit test backend saat:
backend/**,app/**,src/**backend berubah.- Dependency backend atau konfigurasi runtime berubah.
Jika tim belum punya test impact analysis yang lebih canggih, pemisahan per area seperti ini sudah cukup efektif dan aman.
Integration test
Integration test biasanya paling mahal karena membutuhkan service tambahan seperti database, cache, message broker, atau API mock. Karena itu, job ini cocok dijalankan berdasarkan sinyal risiko yang lebih ketat.
Jalankan integration test saat:
- Kode backend berubah.
- Ada perubahan migration atau schema database.
- Ada perubahan kontrak API, serializer, atau konfigurasi yang memengaruhi integrasi.
- Pull request menargetkan
maindan menyentuh area kritis. - Push ke
mainsetelah merge.
Pertimbangkan tidak menjalankan integration test pada setiap commit branch fitur jika perubahan hanya di dokumentasi atau CSS, terutama saat runner terbatas.
Build
Build sering mahal karena bundling frontend, compile asset, atau membangun image container. Build sebaiknya dijalankan hanya ketika hasil build memang perlu divalidasi.
Jalankan build saat:
- Perubahan menyentuh source code atau file build config.
- Pull request perlu memverifikasi bahwa aplikasi masih bisa dibangun.
- Push ke
mainatau branch release untuk menghasilkan artifact deployable.
Hindari build berulang di beberapa job. Bangun sekali, lalu simpan artifact untuk job berikutnya.
Security scan
Security scan memiliki spektrum biaya yang lebar. Pemeriksaan lockfile atau dependency manifest biasanya murah, sedangkan SAST mendalam atau container scan bisa jauh lebih mahal.
Pola yang masuk akal:
- PR ringan: scan dependency dasar saat file dependency berubah.
- Merge ke main: scan lebih lengkap untuk mencegah masalah masuk ke trunk.
- Terjadwal: scan malam atau harian untuk menangkap CVE baru tanpa menambah beban tiap commit.
Contoh workflow GitHub Actions yang konkret
Contoh berikut menunjukkan pola dasar selective execution untuk repo web/backend. Ia memakai job deteksi perubahan lebih dulu, lalu job lain dijalankan berdasarkan output job tersebut. Pendekatan ini lebih fleksibel dibanding hanya menaruh filter path di level trigger, karena keputusan bisa dipakai ulang oleh banyak job.
name: ci
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
schedule:
- cron: '0 2 * * *'
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.filter.outputs.docs_only }}
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
migrations: ${{ steps.filter.outputs.migrations }}
deps_frontend: ${{ steps.filter.outputs.deps_frontend }}
deps_backend: ${{ steps.filter.outputs.deps_backend }}
ci_config: ${{ steps.filter.outputs.ci_config }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
docs_only:
- 'docs/**'
- '**/*.md'
frontend:
- 'frontend/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'yarn.lock'
- 'vite.config.*'
backend:
- 'backend/**'
- 'app/**'
- 'src/**'
- 'composer.json'
- 'composer.lock'
- 'pyproject.toml'
- 'requirements*.txt'
migrations:
- 'database/migrations/**'
- 'db/migrations/**'
- 'schema/**'
deps_frontend:
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'yarn.lock'
deps_backend:
- 'composer.json'
- 'composer.lock'
- 'pyproject.toml'
- 'requirements*.txt'
ci_config:
- '.github/workflows/**'
lint:
needs: changes
if: needs.changes.outputs.docs_only != 'true' && (
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.backend == 'true' ||
needs.changes.outputs.ci_config == 'true'
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
if: needs.changes.outputs.frontend == 'true'
with:
node-version: 'lts/*'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install frontend deps
if: needs.changes.outputs.frontend == 'true'
run: |
cd frontend
npm ci
- name: Lint frontend
if: needs.changes.outputs.frontend == 'true'
run: |
cd frontend
npm run lint
- name: Setup backend runtime
if: needs.changes.outputs.backend == 'true'
run: echo "Setup backend runtime here"
- name: Lint backend
if: needs.changes.outputs.backend == 'true'
run: echo "Run backend linter here"
unit_test:
needs: changes
if: needs.changes.outputs.docs_only != 'true' && (
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.backend == 'true'
)
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
area: [frontend, backend]
steps:
- uses: actions/checkout@v4
- name: Skip unrelated matrix entry
if: |
(matrix.area == 'frontend' && needs.changes.outputs.frontend != 'true') ||
(matrix.area == 'backend' && needs.changes.outputs.backend != 'true')
run: echo "No relevant changes for ${{ matrix.area }}"
- uses: actions/setup-node@v4
if: matrix.area == 'frontend' && needs.changes.outputs.frontend == 'true'
with:
node-version: 'lts/*'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Frontend unit tests
if: matrix.area == 'frontend' && needs.changes.outputs.frontend == 'true'
run: |
cd frontend
npm ci
npm test -- --runInBand
- name: Backend unit tests
if: matrix.area == 'backend' && needs.changes.outputs.backend == 'true'
run: echo "Run backend unit tests here"
integration_test:
needs: [changes, lint, unit_test]
if: |
github.event_name == 'push' && github.ref == 'refs/heads/main' ||
needs.changes.outputs.migrations == 'true' ||
needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Run integration tests
run: echo "Run integration tests here"
build:
needs: [changes, lint, unit_test]
if: needs.changes.outputs.docs_only != 'true' && (
github.ref == 'refs/heads/main' ||
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.backend == 'true'
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: |
mkdir -p dist
echo "artifact" > dist/app.txt
- uses: actions/upload-artifact@v4
with:
name: app-build
path: dist/
security_scan:
needs: changes
if: |
github.event_name == 'schedule' ||
github.ref == 'refs/heads/main' ||
needs.changes.outputs.deps_frontend == 'true' ||
needs.changes.outputs.deps_backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run dependency/security scan
run: echo "Run security scan here"Mengapa pola ini efektif
- Job changes sebagai sumber keputusan tunggal: satu kali mendeteksi perubahan, banyak job memakainya.
- concurrency cancel: commit lama dibatalkan saat ada commit baru di branch yang sama.
- fail-fast pada matriks: jika satu variasi penting gagal, pipeline tidak terus menghabiskan runner untuk variasi lain tanpa nilai tambah.
- cache dependency: dependency manager tidak selalu mengunduh ulang paket.
- artifact reuse: hasil build bisa dipakai untuk test deploy atau packaging berikutnya.
Fail-fast, cache, artifact reuse, dan concurrency cancel
Fail-fast
Fail-fast berarti hentikan pekerjaan mahal secepat mungkin jika sinyal awal sudah menunjukkan kegagalan. Urutan umum yang efisien:
- Deteksi perubahan.
- Lint dan validasi cepat.
- Unit test.
- Integration test.
- Build produksi.
- Security scan berat atau deploy check.
Kalau lint gagal, tidak ada alasan melanjutkan build image yang mahal. Namun ada trade-off: pada beberapa tim, menjalankan semua test sekaligus memberi lebih banyak sinyal dalam satu run. Pilih fail-fast jika tujuan utamanya menghemat komputasi dan mempercepat feedback dasar.
Cache dependency
Cache cocok untuk dependency yang sering dipakai ulang, misalnya package manager frontend atau backend. Kuncinya adalah memakai file lock sebagai indikator invalidasi cache. Jangan cache direktori yang rawan menyebabkan hasil tidak konsisten jika Anda belum memahami dampaknya.
Kesalahan umum:
- Cache terlalu agresif hingga menyembunyikan masalah dependency.
- Tidak mengikat cache ke lockfile, sehingga paket lama terus dipakai.
- Menganggap cache selalu lebih cepat; pada dependency kecil, overhead cache bisa saja tidak terlalu terasa.
Artifact reuse
Jika satu job sudah menghasilkan build output, simpan sebagai artifact dan unduh di job lain. Ini relevan untuk:
- Bundle frontend yang dipakai di test smoke.
- Binary atau package yang dipakai di langkah packaging.
- Hasil kompilasi yang dipakai untuk validasi deploy.
Tanpa artifact reuse, job berikutnya sering membangun ulang hal yang sama.
Concurrency cancel
Pada branch fitur, sering ada beberapa commit beruntun dalam hitungan menit. Menjalankan semua pipeline untuk commit lama hampir selalu boros. Dengan cancel-in-progress, hanya commit terbaru yang terus berjalan. Ini salah satu penghematan termudah dan biasanya aman untuk workflow validasi pull request.
Job wajib vs opsional dan aturan merge
Selective execution akan lebih aman jika dipadukan dengan aturan merge yang eksplisit. Jangan hanya mengandalkan kebiasaan tim.
Contoh pembagian yang praktis
- Required checks untuk pull request ke main: lint, unit test area terkait, validasi lockfile jika dependency berubah.
- Required checks saat backend atau migration berubah: integration test.
- Optional checks: scan penuh, build multi-runtime, benchmark, atau test end-to-end berat.
- Scheduled checks: scan keamanan berkala, full regression, atau test matriks lebih luas.
Jika platform merge Anda mendukungnya, buat aturan bahwa pull request dengan label tertentu seperti high-risk atau perubahan pada path kritis harus menjalankan job tambahan. Ini berguna untuk area autentikasi, pembayaran, atau data migration.
Trade-off, keterbatasan, dan risiko coverage
Selective execution memang menghemat biaya, tetapi ada konsekuensi yang harus dipahami:
- Risiko false negative: perubahan tampak lokal, tetapi efeknya global. Misalnya perubahan utility bersama ternyata memengaruhi banyak modul.
- Path filter tidak memahami dependensi logis: ia hanya melihat file berubah, bukan dampak runtime.
- Konfigurasi CI bisa menjadi kompleks: terlalu banyak kondisi membuat workflow sulit dipelihara.
- Coverage bisa menurun jika job mahal terlalu jarang dijalankan.
Cara mengurangi risiko:
- Mulai dengan pemisahan area besar: docs, frontend, backend, migration, dependency.
- Tetap jalankan full pipeline pada merge ke
mainatau secara terjadwal. - Terapkan label/manual override untuk memaksa full run saat reviewer ragu.
- Audit insiden lolos tes untuk memperbaiki aturan filter.
Selective execution bukan pengganti strategi test yang baik. Ia adalah lapisan optimasi. Jika struktur test dan batas modul masih kabur, hasil optimasinya juga akan rapuh.
Metrik yang perlu dipantau
Jangan mengklaim penghematan tanpa mengukur. Beberapa metrik yang layak dipantau:
- Median dan p95 durasi pipeline per jenis event.
- Total menit runner per minggu atau per bulan.
- Persentase job yang di-skip karena selective execution.
- Rasio rerun akibat flaky test atau cache bermasalah.
- Waktu tunggu feedback PR sampai required checks selesai.
- Jumlah bug yang lolos karena job tidak dijalankan pada PR.
- Hit rate cache jika platform Anda menyediakan metriknya.
Jika penghematan runner meningkat tetapi bug lolos juga meningkat, berarti aturan terlalu agresif. Tujuannya bukan sekadar mengurangi menit komputasi, tetapi mengoptimalkan biaya terhadap tingkat risiko yang dapat diterima tim.
Kesalahan umum saat implementasi
- Mengandalkan path filter level workflow saja, lalu kesulitan mengatur logika antar-job. Gunakan satu job deteksi perubahan agar keputusan bisa dipakai konsisten.
- Tidak ada fallback full run untuk merge ke branch utama atau schedule berkala.
- Semua job dijadikan required meski setengahnya jarang relevan.
- Matrix terlalu luas di PR. Simpan kombinasi runtime tambahan untuk nightly atau main branch.
- Cache tanpa disiplin invalidasi, menyebabkan hasil tes sulit dipercaya.
- Tidak membatalkan run lama, sehingga antrean runner menumpuk.
Checklist adopsi bertahap untuk tim kecil-menengah
Berikut urutan adopsi yang realistis tanpa harus merombak seluruh pipeline sekaligus:
- Petakan job saat ini: mana yang paling mahal, paling lama, dan paling sering tidak relevan.
- Kelompokkan path utama: docs, frontend, backend, migration, dependency, CI config.
- Buat job deteksi perubahan dan hubungkan lint serta unit test ke output job itu.
- Aktifkan concurrency cancel untuk pull request dan branch fitur.
- Tambahkan cache dependency berbasis lockfile.
- Pisahkan integration test agar hanya berjalan untuk backend, migration, atau push ke main.
- Gunakan artifact reuse untuk mencegah build berulang.
- Tentukan required vs optional checks di aturan merge.
- Jadwalkan full pipeline harian atau setelah merge ke main sebagai jaring pengaman.
- Pantau metrik 2-4 minggu lalu sesuaikan aturan yang terlalu longgar atau terlalu ketat.
Kapan selective execution layak dipakai?
Pendekatan ini paling cocok jika:
- Repo Anda memiliki area yang cukup jelas, misalnya frontend dan backend terpisah.
- Pipeline mulai terasa mahal atau lambat.
- Tim sering melakukan commit kecil yang memicu job berat berulang kali.
- Runner terbatas dan antrean CI menghambat review.
Jika repo masih kecil dan seluruh pipeline hanya butuh beberapa menit, optimasi kompleks mungkin belum perlu. Namun begitu biaya runner, waktu tunggu, atau jumlah job meningkat, CI/CD hemat biaya dengan selective execution biasanya memberi dampak nyata tanpa harus menurunkan standar kualitas.
Intinya: jangan perlakukan semua perubahan sebagai perubahan berisiko tinggi. Jalankan lint dan test hanya saat perlu, tetapi pastikan ada pagar pengaman berupa required checks yang tepat, full run berkala, dan metrik yang terus dipantau.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!