CI merah karena schema kosong: migrasi dulu, baru pytest
Pipeline CI gagal pada database service yang masih kosong; satu baris alembic upgrade head sebelum pytest menyelaraskan kontrak CI dengan container produksi.
Ringkasan
Pipeline CI jadi merah gara-gara database kosong, bukan karena kode error—ternyata migrasi nggak pernah jalan di CI. Solusinya gampang, tinggal tambahin alembic upgrade head sebelum pytest biar urutannya sama kayak produksi: schema dulu, baru tes. Sekarang error yang muncul beneran nunjukin bug kode, bukan tabel yang belum dibuat, jadi debugging nggak buang waktu lagi.
Pipeline merah, laptop hijau
Saya melihat pipeline CI DemandScope berwarna merah. Seluruh suite tes gagal, bukan karena logika bisnis yang keliru, melainkan karena layanan Postgres di lingkungan CI menolak permintaan. Jejak error mengarah pada tabel yang tidak ditemukan atau relasi yang gagal terbentuk.
Konteks dulu: suite di repo ini mengetes alur autentikasi dari ujung ke ujung. Register, login, sampai pengambilan profil pengguna, semuanya melewati database sungguhan yang dijalankan sebagai service di CI. Jadi begini, tebakan awal saya klise banget. Di mesin laptop, rangkaian tes tersebut berjalan lancar. Saya berasumsi kode sudah benar dan konfigurasi CI-lah yang berperilaku aneh.
Yang bikin makin pusing: jejak errornya menyesatkan. Yang tampil bukan pesan "migrasi belum jalan", melainkan deretan kegagalan tes yang kelihatan seperti bug di kode. Waktu debug pun berputar di tempat yang salah. Asumsi itu meleset. Setelah menelusuri log eksekusi, temuannya jauh lebih mendasar: CI tidak pernah menjalankan migrasi. Lingkungan produksi punya kontrak eksplisit, container menjalankan perintah migrasi sebelum aplikasi dimulai. Pipeline memanggil pytest langsung pada layanan database yang masih kosong. Dua lingkungan, dua kontrak berbeda.
Kontrak schema dulu, baru tes
Definisi kontraknya gampang diingat: schema sebelum tes, sama persis seperti schema sebelum app. Bukan kebetulan komentar di workflow menuliskan itu kata per kata. File migrasi adalah kode yang menentukan bentuk tabel, index, dan constraint yang dipakai aplikasi. Melewatkannya di satu lingkungan berarti lingkungan itu menguji melawan database versi khayalan.
Perintah alembic upgrade head adalah mekanisme resmi untuk menjalankan seluruh migrasi sampai revisi terbaru, dan Alembic melacak posisi database lewat tabel alembic_version[10]. Tanpa langkah ini, suite yang mengetes alur autentikasi end-to-end mencoba menembak struktur database yang belum pernah dibuat. Skema adalah bagian dari permukaan API. Menjalankan tes tanpa migrasi sama saja dengan mengetes kode melawan skema impian.
Kontrak dua lingkungan itu akhirnya disatukan. Container produksi sudah menjalankan migrasi sebelum app naik lewat CMD; CI tinggal mengikuti urutan yang sama, dengan komentar eksplisit supaya tidak ada yang menghapusnya saat merapikan workflow. Solusinya satu baris di workflow, disisipkan sebelum pytest dengan komentar yang menjelaskan kontraknya:
- run: pip install ".[dev]"
- run: ruff check .
# kontrak sama dengan CMD container: schema dulu, baru tes
- run: alembic upgrade head
- run: pytest -v
Dampaknya terasa di bentuk error. Sebelum ada migrasi, kegagalan muncul di lapisan paling dalam: koneksi ditolak, relasi tak dikenal, tabel menghilang. Setelah ada migrasi, kegagalan naik ke lapisan yang benar: asersi yang gagal karena perilaku kode. Semakin tinggi lapisan error muncul, semakin pendek jalur perbaikannya.
Kode exit pytest punya makna kontrak yang ketat: 0 berarti semua tes lulus, 1 berarti ada yang gagal[11]. Pipeline membaca angka itu untuk memutuskan merah atau hijau. Kalau skema belum ada, error yang muncul bukan kegagalan tes yang valid, melainkan error infrastruktur yang menyesatkan arah perbaikan. Urutan lokal yang direplikasi di CI jadi sederhana. Dua perintah saja, urutannya tidak boleh dibalik, dan aturan itu berlaku sama di mesin laptop maupun di runner:
# tanpa migrasi, pytest menembak schema kosong
alembic upgrade head
pytest -v
Ada satu catatan soal batasan: langkah ini menuntut database service di CI sudah siap menerima koneksi, dan migrasi memang bagian dari kontrak deploy, bukan tambal sulam khusus CI. Kalau kontraknya dijaga di satu tempat, CI dan produksi berhenti menjadi dua dunia. Setelah baris itu masuk, setiap error yang dilaporkan pytest kini merepresentasikan cacat kode atau ekspektasi yang salah, bukan tabel yang belum jadi. Keandalan pengujian naik bukan karena jumlah tesnya bertambah, melainkan karena kondisi awal suite berhenti membohongi. Baris migrasi di workflow itu murah; jam yang hilang untuk membaca jejak error yang menyesatkan itu yang mahal. Tes alur end-to-end memang tidak pernah benar-benar terisolasi dari keadaan database, dan menjamin skema siap adalah harga dari hasil tes yang bisa dipercaya.
Sources:
[10] Alembic Tutorial
[11] pytest: exit codes