Bir çekme isteği (pull request) openapi.yaml dosyasını düzenler. CI kontrolleri yeşile döner. Belirtim geçerlidir, hatasız bir şekilde lint işleminden geçer ve iki incelemeci onaylar. Üç gün sonra, mobil bir istemci, eskiden var olan bir yanıt alanının kaybolması nedeniyle null-pointer çökmeleri yaşamaya başlar. Kimse bunu kasıtlı olarak kaldırmadı. Biri bir yeniden düzenleme (refactor) sırasında bir özelliği yeniden adlandırdı ve incelemede hiçbir şey bunu fark etmedi.
Bu, düz bir doğrulayıcının (validator) asla görmediği bir boşluktur. Bir belirtim mükemmel bir şekilde iyi biçimlendirilmiş olabilir ve yine de ona bağımlı olan her tüketiciyi bozabilir. Bunu bilmenin tek yolu, yeni belirtimi değiştirdiği sürümle, değişiklik değişik karşılaştırmak ve bir soru sormaktır: bu, dün çalışan bir istemciyi bozar mıydı? Bu karşılaştırma bir OpenAPI diff'idir ve bunu bir birleştirme kapısı (merge gate) olarak çalıştırmak, bir API deposuna ekleyebileceğiniz en yüksek geri dönüşlü kontrollerden biridir.
Bir OpenAPI Diff'inin Gerçekten Karşılaştırdığı Şey
Bir OpenAPI diff'i, bir temel (base) ve bir baş (head) olmak üzere iki belirtim alır ve aralarındaki değişiklikleri raporlar. Temel, genellikle hedef dalınızdaki (canlı olan) belirtimdir. Baş ise çekme isteğinizin önerdiği belirtimdir. İyi bir diff aracı, sadece git diff'in yaptığı gibi metinsel bir delta dökümü yapmaz. OpenAPI yapısını anlar, bu sayede kozmetik düzenlemeler ile sözleşmeyi bozan değişiklikler arasındaki farkı söyleyebilir.
İşte önemli olan ayrım. Bazı değişiklikler ekleyicidir ve güvenlidir:
- Yeni bir isteğe bağlı istek parametresi eklemek
- Yeni bir yanıt alanı eklemek
- Tamamen yeni bir uç nokta eklemek
- Bir istek gövdesine yeni bir enum değeri eklemek
Mevcut istemciler tüm bunlara rağmen çalışmaya devam eder. Her zaman gönderdiklerini gönderir ve her zaman okuduklarını okurlar. Diğer değişiklikler geriye dönük uyumsuzluk gösterir ve bunlar zarar verenlerdir:
- Bir istemcinin okuduğu bir yanıt alanını kaldırmak
- Bir özelliği yeniden adlandırmak (bir istemci açısından bir kaldırma ve bir ekleme)
- Daha önce isteğe bağlı olan bir parametreyi zorunlu hale getirmek
- Bir türü daraltmak, örneğin
string'deninteger'a - İstemcinin gönderebileceği bir enum değerini kaldırmak
- Bir uç noktayı veya bir HTTP metodunu silmek
Bir OpenAPI diff aracının görevi, her iki belge üzerindeki her yolu, parametreyi, şemayı ve yanıtı taramak ve her değişikliği bu kategorilerden birine ayırmaktır. Bu sınıflandırma tüm meselenin özüdür. Ham bir satır diff'i, kaldırılan bir required alanını elli satırlık yeniden biçimlendirmenin altına gömer. Yapısal bir diff ise bunu bozan bir değişiklik olarak ortaya çıkarır ve hangi yolda bulunduğunu söyler.
Bazı değişikliklerin neden bozulduğunu ve diğerlerinin neden bozulmadığını anlamak için temel zihinsel modeli isterseniz, API'leri büyük ölçekte nasıl sürümlendireceğiniz ve kullanımdan kaldıracağınız hakkındaki kılavuz uyumluluk kurallarını derinlemesine ele alır. Diff aracı, bir inceleyicinin bunları hatırlamasını ummak yerine bu kuralları mekanik olarak nasıl uygulayacağınızdır.
oasdiff: Açık Kaynak İş Gücü
oasdiff, çoğu ekibin başvurduğu açık kaynaklı bir araçtır. Tek bir Go ikili dosyasıdır, hızlıdır ve özellikle bozucu değişiklik sorunu etrafında inşa edilmiştir. OpenAPI 3.0 ve 3.1 belgelerini okur ve karşılaştırmadan ne istediğinize bağlı olarak size birkaç alt komut sunar.
En çok kullanacaklarınızdan üçü:
diff, iki belirtim arasındaki tüm farkları raporlar.breaking, yalnızca geriye dönük uyumsuz değişiklikleri raporlar.changelog, bozucu olsun ya da olmasın, her önemli değişikliğin insan tarafından okunabilir bir listesini üretir.
Bir birleştirme kapısı için, breaking önemlidir. Temel belirtiminiz ile baş belirtiminiz arasında işaret edin:
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml hedef daldaki belirtim, head-openapi.yaml ise çekme isteğindeki belirtimdir. breaking alt komutu yalnızca uyumsuz değişiklikleri yazdırır. --fail-on ERR bayrağı bunu bir kapıya dönüştürür: `ERR` seviyesinde sınıflandırılan bir değişiklik bulduğunda komutun sıfır olmayan bir durumla çıkmasını sağlar. Sıfır olmayan çıkış, CI'ın başarısızlık olarak okuduğu evrensel sinyaldir.
Bu ciddiyet modelini anlamakta fayda var. oasdiff, bozucu değişiklikleri seviyelere ayırır ve ERR, istemcileri bozacak ciddi bir değişikliktir. WARN, nasıl yazıldıklarına bağlı olarak bazı istemcileri bozabilecek değişiklikleri kapsar ve INFO bilgilendiricidir. Çizgiyi nereye çekeceğinize siz karar verirsiniz. --fail-on ERR yalnızca kesin bozulmaları engeller. --fail-on WARN daha katıdır ve olabilecekleri de yakalar.
Bir geçiş/başarısızlık yerine bir değişiklik günlüğü veya bir PR yorumu için okunabilir özet istediğinizde, changelog alt komutu daha dostça bir çıktıdır:
oasdiff changelog base-openapi.yaml head-openapi.yaml
oasdiff'in gerçekten kullanışlı birkaç dokunuşu var. Yeniden adlandırılan yol parametrelerini atlatan uç nokta eşleştirmesi yapar, böylece {userId}'nin {id}'ye dönüşmesini, yol aksi takdirde aynıysa, bir silme-artı-ekleme olarak işaretlemez. Karşılaştırmadan önce allOf şemalarını birleştirebilir, böylece kalıtım gürültü yaratmaz. Ve düz metinden daha fazlasını yayar: HTML, JSON, YAML ve Markdown, çıktı bayrakları aracılığıyla kullanılabilir, bu da sonucu bir CI açıklamasına veya oluşturulan bir değişiklik günlüğüne beslemeyi kolaylaştırır. Beş dakikada bir işlem hattına (pipeline) ekleyebileceğiniz ve neyi bozucu olarak adlandırdığı konusunda muhafazakar olacağına güvenebileceğiniz bir araç için geçilmesi zordur.
openapi-diff: JVM Alternatifi
Yığın teknolojiniz zaten JVM üzerinde çalışıyorsa, OpenAPITools/openapi-diff sağlam bir ikinci seçenektir ve hakkında bilgi sahibi olmaya değerdir. İki OpenAPI 3.x belirtimini karşılaştıran ve farkı HTML, Markdown, AsciiDoc, JSON veya konsol metni olarak işleyen Java tabanlı bir araçtır (Java 8 ve üzeri). Yerleşik bir jar dosyasından, Maven aracılığıyla, Homebrew üzerinden veya bir Docker görüntüsü olarak çalıştırabilirsiniz, böylece çok fazla uğraşmadan çeşitli derleme kurulumlarına uyar.
Karşılaştırması parametreler, yanıtlar, uç noktalar ve HTTP metotlarına derinlemesine iner ve herkesin önem verdiği aynı çizgiyi çizer: geriye dönük uyumluluğu koruyan değişiklikler ile bozan değişiklikler. CLI oldukça basittir:
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
--fail-on-incompatible bayrağı yalnızca bir değişiklik geriye dönük uyumluluğu bozduğunda sıfır olmayan bir değerle çıkar, ki bu tam da istediğiniz kapı davranışıdır. Eğer herhangi bir değişiklikte başarısız olmayı tercih ediyorsanız daha katı bir --fail-on-changed seçeneği ve betik oluşturmak için tek kelimelik bir yanıt istediğinizde yalnızca no_changes, compatible veya incompatible yazdıran bir --state modu bulunur.
Öne çıktığı yer işlenmiş çıktısıdır. HTML ve Markdown raporları temiz ve ayrıntılıdır, bu da openapi-diff'i yalnızca bir CI çıkış kodu değil, bir insanın gerçekten okuyacağı bir fark yapıtı istediğinizde güçlü bir seçenek haline getirir. Dezavantajı ise JVM bağımlılığı ve bir Go ikili dosyasına göre daha ağır bir başlangıç süresidir. Ekibiniz zaten bir Java ortamındaysa, bu maliyet sıfırdır ve araç doğrudan yerine oturur. Eğer değilseniz, oasdiff daha hafif bir dokunuştur. Her ikisi de bozucu değişiklik sorusunu iyi yanıtlar; halihazırda sürdürdüğünüz çalışma zamanına uygun olanı seçin.
Diff'i CI'ya Bir Birleştirme Kapısı Olarak Bağlama
Elle çalıştırdığınız bir diff hiçbir şeyi yakalamaz, çünkü çalıştırmayı unuttuğunuz zaman, bozulmanın yayınlandığı zamandır. Kapı, işlem hattında yaşamalı ve belirtimi etkileyen her çekme isteğinde tetiklenmelidir.
CI'daki tek bir pürüz, belirtimin her iki sürümünün de aynı anda mevcut olması gerektiğidir: hedef daldan gelen temel ve PR'dan gelen baş. PR kontrolü size başı verir. Temeli, ikinci bir kontrol etme işlemi yapmadan doğrudan git geçmişinden çekersiniz:
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
Burada birkaç ayrıntı önemlidir. fetch-depth: 0, git show'un temel dal'a ulaşabilmesi için tam geçmişi çeker. git show origin/<base>:openapi.yaml satırı, belirtimi hedef daldaki haliyle okur ve bir dosyaya yazar, ekstra klonlamaya gerek kalmaz. paths filtresi, görevin yalnızca belirtim gerçekten değiştiğinde çalışması anlamına gelir, böylece ilgisiz PR'lar bunun bedelini ödemez. Ve son adım kapıdır: eğer oasdiff breaking bir `ERR` seviyesi değişiklik bulursa, sıfır olmayan bir çıkış yapar, görev kırmızıya döner ve PR, biri birleştire tıklamadan önce başarısız bir kontrol gösterir.
Yazar, kod hala incelemedeyken, uyumluluğu hangi değişikliğin, hangi yolda bozduğunu tam olarak görür. İşte tüm değer bu. Bozulma, bir müşterinin çökme raporunda değil, mümkün olan en ucuz anda yakalanır.
Elbette, her bozucu değişiklik bir hata değildir. Bazen kasıtlı olarak büyük bir sürüm yayınlarsınız ve bozulma kasıtlıdır. Temiz desen, varsayılan olarak engellemek ve istisnalar için açık bir geçersiz kılma (override) gerektirmektir: PR üzerinde bir etiket, info.version'da bir sürüm artışı veya ayrı olarak onaylanmış bir iş akışı. Bu şekilde bir bozulma her zaman birinin kasıtlı olarak aldığı bir karar olur, asla gözden kaçan bir kaza olmaz. API sürümleme stratejisi kılavuzu, bir bozulmanın ne zaman yeni bir ana sürüm hak ettiğini ve ne zaman kaçınılması gerektiğini ayrıntılarıyla anlatır.
Bir Diff'in Kapatamayacağı Boşluk
Yukarıdaki her aracın sınırı budur ve bu önemli bir sınırdır. Bir diff iki dosyayı karşılaştırır. Yeni belgenin eski belgeyle geriye dönük uyumlu olduğunu söyler. Ancak çalışan hizmetinizin aslında bunlardan herhangi birine uygun olup olmadığı hakkında hiçbir şey söylemez.
Bu farklı bir hatadır ve üretimde en çok zarar veren de budur. Belirtim bir created_at alanı vaat eder; ancak uygulama üç sprint önce bunu sessizce geri döndürmeyi bıraktı. Belirtim, bir uç noktanın 200 döndürdüğünü söyler; ancak canlı hizmet, kimsenin test etmediği bir koşul altında 500 döndürür. Fark temizdir çünkü her iki belirtim sürümü de aynı fikirdedir. Sözleşme ve kod uyuşmaz. Statik bir diff'in bunu bilmesinin bir yolu yoktur, çünkü API ile asla konuşmaz.
Bu boşluğu kapatmak, sadece sözleşmeyi kendi kendine karşılaştırmakla kalmayıp, canlı API'yi sözleşmeye karşı test etmek anlamına gelir. Belirtimden testler oluşturur, bunları çalışan hizmete karşı çalıştırır ve gerçek yanıtların belgelenmiş şekillere uygun olduğunu iddia edersiniz. Bu, sözleşme testidir ve yazdığınız ile gerçekte gönderdiğiniz arasındaki farkı yakalayan katmandır.
Apidog ve Apidog CLI ile Kapatmak
Apidog bu döngü için oluşturulmuştur, bu da onu diff adımı için bir yerine doğal bir arkadaş yapar. OpenAPI belirtiminizi bir Apidog projesine aktarır veya senkronize edersiniz ve Apidog, şemadan türetilen iddialarla doğrudan belirtimden test senaryoları oluşturabilir. Testler, gerçek yanıtların belgelenmiş türlere, zorunlu alanlara ve durum kodlarına uygun olduğunu kontrol eder. Sözleşme her değiştiğinde senkronizasyon dışına çıkan paralel bir test betiği seti elle yazmak yerine bu senaryoları görsel olarak oluşturur ve sürdürürsünüz.
Apidog, tasarımı, mock'u ve testi tek bir çalışma alanında tuttuğu için, belirtim hepsinde doğruluk kaynağı olmaya devam eder. Kendi API'nizde bu döngüyü denemek için Apidog'u indirebilir ve mevcut bir belirtimi içe aktarabilirsiniz. Eğer hala bu belirtimi sürümler arasında nasıl kontrol altında tutacağınıza karar vermekte zorlanıyorsanız, Git ile bir OpenAPI belirtimini sürüm kontrolü hakkındaki kılavuz bu iş akışıyla iyi eşleşir.
Apidog CLI, bu senaryoları işlem hattınızda (pipeline) başsız olarak çalıştıran araçtır. Bu bir npm paketidir:
npm install -g apidog-cli
Bir senaryoyu kimliğine göre çalıştırırsınız, doğrulamak istediğiniz ortama yönlendirirsiniz ve CI dostu bir rapor istersiniz:
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
Erişim jetonu çalıştırmayı doğrular ve bir CI sırrında yaşar, asla bir işlenmiş dosyada bulunmaz. `-t` bayrağı senaryoyu seçer, `-e` ortamı seçer ve `-r junit,cli` derleme günlüğü için okunabilir terminal çıktısının yanı sıra CI panonuz için makine tarafından okunabilir JUnit XML'i yayar. Kimlikleri tahmin etmenize gerek yok: gerçek senaryo ve ortam kimlikleri zaten doldurulmuş olan tam komutu Apidog'daki senaryonun CI/CD sekmesinden kopyalarsınız. Tüm seçenek yüzeyini istiyorsanız, tam CLI kılavuzu her bayrağı belgeler ve `apidog run --help` bunları talep üzerine yazdırır.
Kapı davranışı, diff ile aynı prensiptir. Canlı bir yanıt sözleşmeyle artık eşleşmediği için bir iddia başarısız olduğunda, apidog run sıfır olmayan bir çıkış yapar. CI çıkış kodunu okur, adımı başarısız olarak işaretler ve birleştirmeyi engeller. Ek yapılandırma yok. Çalıştırma adımı işlem hattında olduğu sürece, bir sözleşme gerilemesi, bozucu değişiklik diff'inin yaptığı gibi hattı durdurur.
Tam Birleştirme Öncesi Sırası
İki yarımı bir araya getirdiğinizde, her iki tür bozulmayı da yakalayan bir işlem hattı elde edersiniz. Diff, belirtimi okuyarak bir istemciyi bozabilecek değişiklikleri yakalar. Sözleşme testi, çalışan API'yi kullanarak belirtimi artık yerine getirmeyen bir hizmeti yakalar. Bunları ayrı işler olarak çalıştırın:
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
İki iş paralel olarak çalışır. Diff işi dosyaları okur ve git'ten başka hiçbir şeye ihtiyaç duymaz, bu yüzden saniyeler içinde biter. Uyumluluk işi erişilebilir bir ortama ihtiyaç duyar, bu yüzden genellikle dağıtılmış bir hazırlık sürümüne karşı çalışır. Yüklemedeki if: always(), testler başarısız olsa bile raporun akmasını sağlar, ki bu tam da okumak istediğiniz zamandır. Eğer herhangi bir iş kırmızıya dönerse, PR engellenir. CLI'yı gerçek işlem hatlarında çalıştırma hakkında daha fazla bilgi için, Apidog CLI GitHub Actions kılavuzu ve daha geniş CI/CD işlem hattı incelemesi kablolama konusunda daha derine iner.
