Sebuah *pull request* mengedit openapi.yaml. Pemeriksaan CI berhasil. Spesifikasi valid, lolos lint, dan dua peninjau menyetujuinya. Tiga hari kemudian klien seluler mulai mengalami *null-pointer crashes* karena bidang respons yang dulunya ada kini hilang. Tidak ada yang menghapusnya dengan sengaja. Seseorang mengganti nama properti selama refaktor, dan tidak ada dalam ulasan yang menangkapnya.
Inilah celah yang tidak pernah terlihat oleh validator biasa. Sebuah spesifikasi bisa saja sangat rapi tetapi tetap merusak setiap konsumen yang bergantung padanya. Satu-satunya cara untuk mengetahuinya adalah dengan membandingkan spesifikasi baru dengan versi yang digantikannya, perubahan demi perubahan, dan mengajukan satu pertanyaan: apakah ini akan merusak klien yang berfungsi kemarin? Perbandingan itu adalah *OpenAPI diff*, dan menjalankannya sebagai gerbang *merge* adalah salah satu pemeriksaan dengan pengembalian tertinggi yang dapat Anda tambahkan ke repositori API.
Apa yang sebenarnya dibandingkan oleh *OpenAPI diff*
Sebuah *OpenAPI diff* mengambil dua spesifikasi, dasar (*base*) dan kepala (*head*), dan melaporkan apa yang berubah di antara keduanya. *Base* biasanya adalah spesifikasi pada cabang target Anda (yang sedang *live*). *Head* adalah spesifikasi yang diusulkan oleh *pull request* Anda. Alat *diff* yang baik tidak hanya membuang delta tekstual seperti yang akan dilakukan git diff. Ia memahami struktur OpenAPI, sehingga dapat membedakan antara editan kosmetik dan editan yang merusak kontrak.
Berikut adalah perbedaan yang penting. Beberapa perubahan bersifat aditif dan aman:
- Menambahkan parameter permintaan opsional baru
- Menambahkan bidang respons baru
- Menambahkan *endpoint* baru secara keseluruhan
- Menambahkan nilai enum baru ke badan permintaan
Klien yang ada tetap berfungsi melalui semua perubahan tersebut. Mereka mengirimkan apa yang selalu mereka kirim dan membaca apa yang selalu mereka baca. Perubahan lain tidak kompatibel mundur (*backward-incompatible*), dan inilah yang merugikan:
- Menghapus bidang respons yang dibaca klien
- Mengganti nama properti (menghapus ditambah menambah, sejauh yang menjadi perhatian klien)
- Membuat parameter yang sebelumnya opsional menjadi wajib
- Mempersempit tipe, seperti
stringmenjadiinteger - Menghapus nilai enum yang mungkin dikirim klien
- Menghapus *endpoint* atau metode HTTP
Tugas alat *OpenAPI diff* adalah memindai setiap jalur, parameter, skema, dan respons di kedua dokumen dan mengurutkan setiap perubahan ke salah satu kategori tersebut. Klasifikasi itulah intinya. Perbedaan baris mentah mengubur bidang required yang dihapus di bawah lima puluh baris pemformatan ulang. Perbedaan struktural menampilkannya sebagai perubahan yang merusak dan memberi tahu Anda di jalur mana ia berada.
Jika Anda ingin model mental dasar mengapa beberapa perubahan merusak dan yang lain tidak, panduan tentang cara membuat versi dan menghentikan API dalam skala besar membahas aturan kompatibilitas secara mendalam. Alat *diff* adalah cara Anda menerapkan aturan-aturan tersebut secara mekanis daripada berharap peninjau mengingatnya.
oasdiff: kuda pekerja sumber terbuka
oasdiff adalah alat sumber terbuka yang banyak digunakan tim. Ini adalah satu *binary* Go, cepat, dan dibuat khusus untuk pertanyaan perubahan yang merusak. Ia membaca dokumen OpenAPI 3.0 dan 3.1 dan memberi Anda beberapa subperintah tergantung pada apa yang Anda inginkan dari perbandingan.
Tiga yang paling sering Anda gunakan:
diffmelaporkan seluruh perbedaan antara dua spesifikasi.breakinghanya melaporkan perubahan yang tidak kompatibel mundur (*backward-incompatible*).changelogmenghasilkan daftar yang mudah dibaca dari setiap perubahan signifikan, baik yang merusak maupun tidak.
Untuk gerbang *merge*, breaking adalah yang terpenting. Arahkan ke spesifikasi dasar Anda dan spesifikasi kepala Anda:
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml adalah spesifikasi dari cabang target dan head-openapi.yaml adalah spesifikasi dalam *pull request*. Subperintah breaking hanya mencetak perubahan yang tidak kompatibel. *Flag* --fail-on ERR adalah yang mengubah ini menjadi gerbang: ini membuat perintah keluar dengan status bukan nol saat menemukan perubahan yang diklasifikasikan pada tingkat ERR. Keluar bukan nol adalah sinyal universal yang dibaca CI sebagai kegagalan.
Model tingkat keparahan itu patut dipahami. oasdiff mengurutkan perubahan yang merusak ke dalam beberapa tingkat, dan ERR adalah yang serius, perubahan yang akan merusak klien. WARN mencakup perubahan yang mungkin merusak beberapa klien tergantung pada cara penulisannya, dan INFO bersifat informasional. Anda memutuskan di mana harus menarik garis. --fail-on ERR hanya memblokir kerusakan yang pasti. --fail-on WARN lebih ketat dan juga menangkap kemungkinan kerusakan.
Ketika Anda ingin ringkasan yang mudah dibaca untuk *changelog* atau komentar PR daripada lulus/gagal, subperintah changelog memberikan keluaran yang lebih ramah:
oasdiff changelog base-openapi.yaml head-openapi.yaml
oasdiff memiliki beberapa sentuhan yang benar-benar berguna. Ia melakukan pencocokan *endpoint* yang bertahan dari parameter jalur yang diganti namanya, sehingga tidak menandai {userId} menjadi {id} sebagai hapus-plus-tambah ketika jalur tersebut identik. Ia dapat menggabungkan skema allOf sebelum membandingkan sehingga pewarisan tidak menghasilkan *noise*. Dan ia mengeluarkan lebih dari sekadar teks biasa: HTML, JSON, YAML, dan Markdown semuanya tersedia melalui *flag* keluaran, yang membuatnya mudah untuk memasukkan hasilnya ke dalam anotasi CI atau *changelog* yang dihasilkan. Untuk alat yang dapat Anda masukkan ke dalam *pipeline* dalam lima menit dan percayai untuk bersikap konservatif tentang apa yang disebutnya merusak, sulit untuk mengalahkannya.
openapi-diff: alternatif JVM
Jika *stack* Anda sudah menggunakan JVM, OpenAPITools/openapi-diff adalah pilihan kedua yang solid dan patut diketahui. Ini adalah alat berbasis Java (Java 8 ke atas) yang membandingkan dua spesifikasi OpenAPI 3.x dan menampilkan perbedaan sebagai HTML, Markdown, AsciiDoc, JSON, atau teks konsol. Anda dapat menjalankannya dari *jar* yang sudah dibuat, melalui Maven, melalui Homebrew, atau sebagai *image* Docker, sehingga cocok untuk berbagai pengaturan *build* tanpa banyak kesulitan.
Perbandingannya masuk jauh ke dalam parameter, respons, *endpoint*, dan metode HTTP, dan ia menarik garis yang sama yang menjadi perhatian semua orang: perubahan yang mempertahankan kompatibilitas mundur versus perubahan yang merusaknya. CLI-nya lugas:
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
*Flag* --fail-on-incompatible keluar bukan nol hanya ketika perubahan merusak kompatibilitas mundur, yang merupakan perilaku gerbang yang Anda inginkan. Ada --fail-on-changed yang lebih ketat jika Anda lebih suka gagal pada perubahan apa pun, dan mode --state yang hanya mencetak no_changes, compatible, atau incompatible ketika Anda ingin jawaban satu kata untuk *scripting*.
Di mana ia bersinar adalah pada keluaran yang dirender. Laporan HTML dan Markdown rapi dan terperinci, yang menjadikan openapi-diff pilihan yang kuat ketika Anda menginginkan artefak *diff* yang benar-benar akan dibaca manusia, bukan hanya kode keluar CI. *Tradeoff*-nya adalah ketergantungan JVM dan *startup* yang lebih berat daripada *binary* Go. Jika tim Anda sudah menggunakan Java, biaya itu nol dan alatnya langsung pas. Jika tidak, oasdiff adalah pilihan yang lebih ringan. Keduanya menjawab pertanyaan perubahan yang merusak dengan baik; pilih yang sesuai dengan *runtime* yang sudah Anda pertahankan.
Menghubungkan *diff* ke CI sebagai gerbang *merge*
Sebuah *diff* yang Anda jalankan secara manual tidak menangkap apa pun, karena saat Anda lupa menjalankannya adalah saat kerusakan dikirim. Gerbang harus berada dalam *pipeline* dan aktif pada setiap *pull request* yang menyentuh spesifikasi.
Satu kerutan di CI adalah Anda membutuhkan kedua versi spesifikasi secara bersamaan: *base* dari cabang target dan *head* dari PR. *Checkout* PR memberi Anda *head*. Anda menarik *base* langsung dari riwayat git tanpa *checkout* kedua:
name: openapi-diff
on:
pull_request:
paths:
- "openapi.yaml"
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get base spec
run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Diff for breaking changes
run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
Beberapa detail memiliki bobot di sini. fetch-depth: 0 menarik seluruh riwayat sehingga git show dapat mencapai cabang dasar. Baris git show origin/<base>:openapi.yaml membaca spesifikasi sebagaimana adanya pada cabang target dan menulisnya ke file, tanpa perlu klon tambahan. Filter paths berarti pekerjaan hanya berjalan ketika spesifikasi benar-benar berubah, sehingga PR yang tidak terkait tidak perlu membayarnya. Dan langkah terakhir adalah gerbang: jika oasdiff breaking menemukan perubahan tingkat ERR, ia keluar bukan nol, pekerjaan menjadi merah, dan PR menunjukkan pemeriksaan yang gagal sebelum siapa pun mengklik *merge*.
Penulis melihat dengan tepat perubahan mana yang merusak kompatibilitas, di jalur mana, saat kode masih dalam peninjauan. Itulah seluruh nilainya. Kerusakan tertangkap pada saat yang paling murah alih-alih dalam laporan *crash* pelanggan.
Tentu saja, tidak setiap perubahan yang merusak adalah kesalahan. Terkadang Anda mengirimkan versi mayor yang disengaja dan kerusakan itu memang disengaja. Pola yang bersih adalah dengan menjaga secara *default* dan memerlukan *override* eksplisit untuk pengecualian: label pada PR, peningkatan versi di info.version, atau alur kerja terpisah yang disetujui. Dengan begitu, kerusakan selalu merupakan keputusan yang sengaja dibuat seseorang, bukan kecelakaan yang lolos. Panduan strategi pembuatan versi API menjelaskan kapan kerusakan menghasilkan versi mayor baru versus kapan seharusnya dihindari.
Celah yang tidak bisa ditutup oleh *diff*
Inilah batasan setiap alat di atas, dan ini penting. Sebuah *diff* membandingkan dua file. Ia memberi tahu Anda bahwa dokumen baru kompatibel mundur dengan dokumen lama. Ia tidak mengatakan apa-apa tentang apakah layanan Anda yang sedang berjalan benar-benar cocok dengan salah satunya.
Itu adalah kegagalan yang berbeda, dan itulah yang paling menyakitkan dalam produksi. Spesifikasi menjanjikan bidang created_at; implementasi secara diam-diam berhenti mengembalikannya tiga *sprint* yang lalu. Spesifikasi mengatakan *endpoint* mengembalikan 200; layanan *live* mengembalikan 500 dalam kondisi yang tidak pernah diuji siapa pun. *Diff* bersih karena kedua versi spesifikasi setuju. Kontrak dan kode tidak. Sebuah *diff* statis tidak tahu, karena ia tidak pernah berbicara dengan API.
Menutup celah itu berarti menguji API *live* terhadap kontrak, bukan hanya membandingkan kontrak dengan dirinya sendiri. Anda membuat tes dari spesifikasi, menjalankannya terhadap layanan yang berjalan, dan menegaskan bahwa respons nyata sesuai dengan bentuk yang didokumentasikan. Itulah pengujian kontrak, dan itu adalah lapisan yang menangkap *drift* antara apa yang Anda tulis dan apa yang sebenarnya Anda kirimkan.
Menutupnya dengan Apidog dan Apidog CLI
Apidog dibangun untuk lingkaran ini, yang menjadikannya pendamping alami langkah *diff* daripada penggantinya. Anda mengimpor atau menyinkronkan spesifikasi OpenAPI Anda ke proyek Apidog, dan Apidog dapat menghasilkan skenario pengujian langsung dari spesifikasi, dengan penegasan yang berasal dari skema. Pengujian memeriksa bahwa respons nyata sesuai dengan tipe yang didokumentasikan, bidang yang diperlukan, dan kode status. Anda membangun dan memelihara skenario tersebut secara visual alih-alih menulis sendiri kumpulan *script* pengujian paralel yang berubah setiap kali kontrak bergerak.
Karena Apidog menjaga desain, *mocking*, dan pengujian dalam satu *workspace*, spesifikasi tetap menjadi sumber kebenaran di antara semuanya. Anda dapat mengunduh Apidog dan mengimpor spesifikasi yang ada untuk mencoba lingkaran ini pada API Anda sendiri. Jika Anda masih memutuskan bagaimana menjaga spesifikasi itu tetap terkontrol di berbagai versi sejak awal, panduan tentang pengontrolan versi spesifikasi OpenAPI dengan Git sangat cocok dengan alur kerja ini.
Apidog CLI adalah yang menjalankan skenario tersebut tanpa kepala (*headlessly*) di *pipeline* Anda. Ini adalah paket npm:
npm install -g apidog-cli
Anda menjalankan skenario berdasarkan ID, mengarahkannya ke lingkungan yang ingin Anda validasi, dan meminta laporan yang ramah CI:
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
*Access token* mengotentikasi jalannya dan berada di *secret* CI, tidak pernah dalam file yang dikomit. *Flag* -t memilih skenario, -e memilih lingkungan, dan -r junit,cli mengeluarkan XML JUnit yang dapat dibaca mesin untuk *dashboard* CI Anda bersama dengan keluaran terminal yang mudah dibaca untuk log *build*. Anda tidak perlu menebak-nebak ID: Anda menyalin perintah yang tepat, dengan ID skenario dan lingkungan yang sebenarnya sudah terisi, dari tab CI/CD skenario di Apidog. Jika Anda menginginkan opsi lengkap, panduan CLI lengkap mendokumentasikan setiap *flag*, dan apidog run --help mencetaknya sesuai permintaan.
Perilaku gerbang memiliki prinsip yang sama dengan *diff*. Ketika sebuah penegasan gagal, karena respons *live* tidak lagi cocok dengan kontrak, apidog run keluar bukan nol. CI membaca kode keluar, menandai langkah sebagai gagal, dan memblokir *merge*. Tidak ada konfigurasi tambahan. Selama langkah lari ada di *pipeline*, regresi kontrak menghentikan baris dengan cara yang sama seperti *diff* perubahan yang merusak.
Urutan pra-merge lengkap
Satukan kedua bagian tersebut dan Anda akan mendapatkan *pipeline* yang menangkap kedua jenis kerusakan. *Diff* menangkap perubahan yang akan merusak klien dengan membaca spesifikasi. Pengujian kontrak menangkap layanan yang tidak lagi memenuhi spesifikasi dengan menjalankan API yang berjalan. Jalankan keduanya sebagai pekerjaan terpisah:
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
contract-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g apidog-cli
- name: Run contract tests
run: |
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t 605067 \
-e 1629989 \
-r junit,cli \
--out-dir ./apidog-reports
env:
APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: apidog-report
path: ./apidog-reports
Kedua pekerjaan berjalan secara paralel. Pekerjaan *diff* membaca file dan hanya membutuhkan git, sehingga selesai dalam hitungan detik. Pekerjaan kepatuhan membutuhkan lingkungan yang dapat dijangkau, sehingga biasanya berjalan melawan *build staging* yang telah di-deploy. if: always() pada unggahan menjaga laporan tetap mengalir bahkan ketika tes gagal, yang justru saat Anda ingin membacanya. Jika salah satu pekerjaan menjadi merah, PR diblokir. Untuk lebih lanjut tentang menjalankan CLI di *pipeline* nyata, panduan Apidog CLI GitHub Actions dan panduan *pipeline* CI/CD yang lebih luas membahas pengkabelan secara lebih mendalam.
