يقوم طلب السحب بتعديل openapi.yaml. تتحول فحوصات التكامل المستمر (CI) إلى اللون الأخضر. المواصفات صالحة، وتتبع قواعد التنسيق بشكل سليم، ويوافق عليها اثنان من المراجعين. بعد ثلاثة أيام، يبدأ عميل الهاتف المحمول في إظهار أعطال بسبب مؤشر فارغ (null-pointer crashes) لأن حقلاً في الاستجابة كان موجودًا سابقًا قد اختفى. لم يقم أحد بإزالته عن قصد. قام أحدهم بإعادة تسمية خاصية أثناء عملية إعادة هيكلة (refactor)، ولم يكتشفها أي شيء في المراجعة.
هذه هي الفجوة التي لا يراها أي مدقق بسيط. يمكن أن تكون المواصفات جيدة التكوين تمامًا ومع ذلك تكسر كل مستهلك يعتمد عليها. الطريقة الوحيدة لمعرفة ذلك هي مقارنة المواصفات الجديدة بالنسخة التي تحل محلها، تغييرًا بتغيير، وطرح سؤال واحد: هل سيكسر هذا عميلًا كان يعمل بالأمس؟ هذه المقارنة هي فرق OpenAPI، وتشغيلها كبوابة دمج هو أحد الفحوصات ذات العائد الأعلى التي يمكنك إضافتها إلى مستودع API.
ما الذي يقارنه فرق OpenAPI بالفعل
يأخذ فرق OpenAPI مواصفاتين، أساسية ورئيسية (base and head)، ويبلغ عن التغييرات بينهما. المواصفات الأساسية عادة ما تكون المواصفات الموجودة على الفرع الهدف (النسخة الحية). والمواصفات الرئيسية هي المواصفات التي يقترحها طلب السحب الخاص بك. أداة الفرق الجيدة لا تقوم فقط بإلقاء دلتا نصية كما يفعل git diff. بل إنها تفهم بنية OpenAPI، لذا يمكنها التمييز بين التعديلات التجميلية والتغييرات التي تكسر العقد.
وهنا التمييز الذي يهم. بعض التغييرات إضافية وآمنة:
- إضافة معلمة طلب اختيارية جديدة
- إضافة حقل استجابة جديد
- إضافة نقطة نهاية جديدة بالكامل
- إضافة قيمة تعداد جديدة إلى نص الطلب
يستمر العملاء الحاليون في العمل مع كل هذه التغييرات. يرسلون ما كانوا يرسلونه دائمًا ويقرأون ما كانوا يقرأونه دائمًا. التغييرات الأخرى غير متوافقة مع الإصدارات السابقة، وهذه هي التي تسبب المشاكل:
- إزالة حقل استجابة يقرأه العميل
- إعادة تسمية خاصية (إزالة بالإضافة إلى إضافة، فيما يتعلق بالعميل)
- جعل معلمة كانت اختيارية سابقًا إلزامية
- تضييق نوع، مثل من
stringإلىinteger - إزالة قيمة تعداد قد يرسلها العميل
- حذف نقطة نهاية أو طريقة HTTP
مهمة أداة فرق OpenAPI هي فحص كل مسار، ومعلمة، ومخطط، واستجابة عبر كلا المستندين وتصنيف كل تغيير في إحدى هذه الفئات. هذا التصنيف هو جوهر الأمر. الفرق النصي الخام يدفن حقلاً required تمت إزالته تحت خمسين سطرًا من إعادة التنسيق. بينما يكشف الفرق الهيكلي عنه كتغيير كاسر ويخبرك بالمسار الذي يقع تحته.
إذا كنت تريد النموذج الذهني الأساسي لسبب كسر بعض التغييرات وعدم كسر البعض الآخر، فإن الدليل حول كيفية إصدار واهمال واجهات برمجة التطبيقات على نطاق واسع يغطي قواعد التوافق بعمق. أداة الفرق هي كيفية تطبيق هذه القواعد ميكانيكيًا بدلاً من الأمل في أن يتذكرها المراجع.
oasdiff: الأداة مفتوحة المصدر الفعالة
oasdiff هي الأداة مفتوحة المصدر التي تلجأ إليها معظم الفرق. إنها ثنائي Go واحد، سريعة، ومبنية خصيصًا حول مسألة التغيير الكاسر. تقرأ مستندات OpenAPI 3.0 و 3.1 وتوفر لك بعض الأوامر الفرعية اعتمادًا على ما تريده من المقارنة.
الأوامر الثلاثة التي ستستخدمها كثيرًا هي:
diffيبلغ عن المجموعة الكاملة من الاختلافات بين مواصفاتين.breakingيبلغ فقط عن التغييرات غير المتوافقة مع الإصدارات السابقة.changelogينتج قائمة قابلة للقراءة البشرية لكل تغيير مهم، سواء كان كاسرًا أم لا.
بالنسبة لبوابة الدمج، breaking هو الأمر المهم. وجّهه إلى مواصفاتك الأساسية ومواصفاتك الرئيسية:
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml هي المواصفات من الفرع الهدف و head-openapi.yaml هي المواصفات في طلب السحب. يطبع الأمر الفرعي breaking فقط التغييرات غير المتوافقة. العلم --fail-on ERR هو ما يحول هذا إلى بوابة: يجعله ينهي الأمر بحالة غير صفرية عندما يجد تغييرًا مصنفًا على مستوى ERR. الخروج غير الصفري هو الإشارة العالمية التي يقرأها CI على أنها فشل.
نموذج الشدة هذا يستحق الفهم. يصنف oasdiff التغييرات الكاسرة إلى مستويات، و ERR هو المستوى الخطير، وهو تغيير سيكسر العملاء. يغطي WARN التغييرات التي قد تكسر بعض العملاء اعتمادًا على كيفية كتابتها، و INFO هو للمعلومات. أنت من يقرر أين ترسم الخط. --fail-on ERR يمنع فقط الأعطال المؤكدة. --fail-on WARN أكثر صرامة ويصيد الاحتمالات أيضًا.
عندما تريد ملخصًا قابلاً للقراءة لسجل التغييرات أو تعليق طلب سحب بدلاً من النجاح/الفشل، فإن الأمر الفرعي changelog يوفر إخراجًا أكثر ودية:
oasdiff changelog base-openapi.yaml head-openapi.yaml
يحتوي oasdiff على بعض اللمسات المفيدة حقًا. إنه يقوم بمطابقة نقاط النهاية التي تتجاوز معلمات المسار المعاد تسميتها، لذلك لا يعلم على {userId} الذي يصبح {id} كحذف-إضافة عندما يكون المسار متطابقًا بخلاف ذلك. يمكنه دمج مخططات allOf قبل المقارنة حتى لا ينتج التوريث ضوضاء. ويصدر أكثر من مجرد نص عادي: يتوفر HTML و JSON و YAML و Markdown كلها عبر علامات الإخراج، مما يسهل إدخال النتيجة في تعليق CI أو سجل تغييرات تم إنشاؤه. بالنسبة لأداة يمكنك إسقاطها في خط أنابيب في خمس دقائق وتثق بها لتكون متحفظة فيما تسميه كاسرًا، فمن الصعب التغلب عليها.
openapi-diff: البديل المستند إلى JVM
إذا كانت مجموعتك التقنية تعمل بالفعل على JVM، فإن OpenAPITools/openapi-diff هو خيار ثانٍ قوي ويستحق المعرفة به. إنها أداة قائمة على Java (Java 8 وما فوق) تقارن بين مواصفتين من OpenAPI 3.x وتعرض الفرق بتنسيق HTML أو Markdown أو AsciiDoc أو JSON أو نص وحدة التحكم. يمكنك تشغيلها من ملف JAR مبني، أو عبر Maven، أو عبر Homebrew، أو كصورة Docker، لذا فهي تناسب مجموعة من إعدادات البناء دون عناء كبير.
تتعمق مقارنتها في المعلمات، والاستجابات، ونقاط النهاية، وطرق HTTP، وترسم نفس الخط الذي يهتم به الجميع: التغييرات التي حافظت على التوافق مع الإصدارات السابقة مقابل التغييرات التي كسرته. واجهة سطر الأوامر (CLI) مباشرة:
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
العلم --fail-on-incompatible ينهي العملية بحالة غير صفرية فقط عندما يكسر تغيير التوافق مع الإصدارات السابقة، وهو بالضبط سلوك البوابة الذي تريده. هناك خيار --fail-on-changed أكثر صرامة إذا كنت تفضل الفشل عند أي تغيير على الإطلاق، ووضع --state الذي يطبع فقط no_changes أو compatible أو incompatible عندما تريد إجابة بكلمة واحدة للكتابة حولها.
تكمن قوتها في الإخراج المعروض. تقارير HTML و Markdown نظيفة ومفصلة، مما يجعل openapi-diff خيارًا قويًا عندما تريد أداة فرق يقرأها الإنسان بالفعل، وليس مجرد رمز خروج CI. المقايضة هي تبعية JVM وبدء تشغيل أثقل من ثنائي Go. إذا كان فريقك يعتمد على Java بالفعل، فإن هذه التكلفة صفرية وتتناسب الأداة تمامًا. إذا لم تكن كذلك، فإن oasdiff هو الخيار الأخف. كلاهما يجيب على سؤال التغيير الكاسر بشكل جيد؛ اختر الأداة التي تتناسب مع بيئة التشغيل التي تديرها بالفعل.
ربط الفرق في التكامل المستمر (CI) كبوابة دمج
لا يكتشف الفرق الذي تقوم بتشغيله يدويًا أي شيء، لأن الوقت الذي تنسى فيه تشغيله هو الوقت الذي يحدث فيه الكسر. يجب أن تعيش البوابة في خط الأنابيب وتعمل على كل طلب سحب يلمس المواصفات.
العقبة الوحيدة في CI هي أنك تحتاج إلى كلا إصداري المواصفات موجودين في وقت واحد: الأساسي من الفرع الهدف والرئيسي من طلب السحب. يوفر لك سحب طلب السحب النسخة الرئيسية. يمكنك سحب النسخة الأساسية مباشرة من سجل Git دون الحاجت لتسجيل خروج ثانٍ:
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
بعض التفاصيل تحمل الوزن هنا. fetch-depth: 0 يسحب السجل الكامل حتى يتمكن git show من الوصول إلى الفرع الأساسي. يقرأ السطر git show origin/<base>:openapi.yaml المواصفات كما هي موجودة على الفرع الهدف ويكتبها إلى ملف، لا حاجة لاستنساخ إضافي. يعني مرشح paths أن المهمة لا تعمل إلا عندما تتغير المواصفات بالفعل، لذلك لا تدفع طلبات السحب غير ذات الصلة ثمن ذلك. والخطوة الأخيرة هي البوابة: إذا وجد oasdiff breaking تغييرًا بمستوى ERR، فإنه يخرج بحالة غير صفرية، وتصبح المهمة حمراء، ويعرض طلب السحب فحصًا فاشلاً قبل أن ينقر أي شخص على الدمج.
يرى المؤلف بالضبط أي تغيير كسر التوافق، وعلى أي مسار، بينما لا يزال الكود قيد المراجعة. هذه هي القيمة الكاملة. يتم اكتشاف الكسر في أرخص لحظة ممكنة بدلاً من تقرير تعطل العميل.
ليس كل تغيير كاسر خطأ، بالطبع. أحيانًا تقوم بشحن إصدار رئيسي متعمد ويكون الكسر مقصودًا. النمط النظيف هو الحجب بشكل افتراضي وطلب تجاوز صريح للاستثناءات: تسمية على طلب السحب، أو زيادة في الإصدار في info.version، أو سير عمل منفصل معتمد. بهذه الطريقة، يكون الكسر دائمًا قرارًا اتخذه شخص ما عن قصد، وليس أبدًا حادثًا تسلل. يرشد دليل استراتيجية إصدار API إلى متى يستحق الكسر إصدارًا رئيسيًا جديدًا مقابل متى يجب تجنبه.
الفجوة التي لا يمكن للفرق إغلاقها
هنا يكمن حد كل أداة أعلاه، وهو أمر مهم. يقارن الفرق بين ملفين. يخبرك أن المستند الجديد متوافق مع الإصدارات السابقة مع المستند القديم. ولا يقول شيئًا عما إذا كانت خدمتك قيد التشغيل تتطابق بالفعل مع أي منهما.
هذا فشل مختلف، وهو الذي يضرب بقوة أكبر في الإنتاج. تعد المواصفات بحقل created_at؛ لكن التنفيذ توقف بهدوء عن إعادته قبل ثلاث دفعات (sprints). تقول المواصفات إن نقطة النهاية ترجع 200؛ الخدمة الحية ترجع 500 تحت شرط لم يختبره أحد. الفرق نظيف لأن كلا نسختي المواصفات تتفقان. العقد والرمز لا يتفقان. لا توجد طريقة لفرق ثابت لمعرفة ذلك، لأنه لا يتحدث أبدًا إلى واجهة برمجة التطبيقات.
إغلاق هذه الفجوة يعني اختبار واجهة برمجة التطبيقات الحية مقابل العقد، وليس مجرد مقارنة العقد بنفسه. تقوم بإنشاء اختبارات من المواصفات، وتشغيلها مقابل الخدمة قيد التشغيل، والتأكد من أن الاستجابات الحقيقية تتطابق مع الأشكال الموثقة. هذا هو اختبار العقد، وهو الطبقة التي تكتشف الانجراف بين ما كتبته وما قمت بشحنه بالفعل.
إغلاق الفجوة باستخدام Apidog وواجهة سطر الأوامر الخاصة بـ Apidog (CLI)
تم بناء Apidog لهذه الحلقة، مما يجعله رفيقًا طبيعيًا لخطوة الفرق بدلاً من استبدالها. يمكنك استيراد أو مزامنة مواصفات OpenAPI الخاصة بك في مشروع Apidog، ويمكن لـ Apidog إنشاء سيناريوهات اختبار مباشرة من المواصفات، مع تأكيدات مستمدة من المخطط. تتحقق الاختبارات من أن الاستجابات الحقيقية تتطابق مع الأنواع الموثقة، والحقول المطلوبة، ورموز الحالة. تقوم ببناء هذه السيناريوهات وصيانتها بصريًا بدلاً من كتابة مجموعة موازية من نصوص الاختبار يدويًا والتي تنحرف عن التزامن في كل مرة يتغير فيها العقد.
نظرًا لأن Apidog يحتفظ بالتصميم، والمحاكاة، والاختبار في مساحة عمل واحدة، تظل المواصفات هي مصدر الحقيقة عبر كل هذه الجوانب. يمكنك تنزيل Apidog واستيراد مواصفات موجودة لتجربة الحلقة على واجهة برمجة التطبيقات الخاصة بك. إذا كنت لا تزال تقرر كيفية التحكم في هذه المواصفات عبر الإصدارات في المقام الأول، فإن الشرح التفصيلي حول التحكم في إصدار مواصفات OpenAPI باستخدام Git يتوافق جيدًا مع سير العمل هذا.
تُعد واجهة سطر الأوامر لـ Apidog (Apidog CLI) هي ما يشغل تلك السيناريوهات بدون واجهة رسومية في خط الأنابيب الخاص بك. إنها حزمة npm:
npm install -g apidog-cli
يمكنك تشغيل سيناريو باستخدام المعرف، وتوجيهه إلى البيئة التي تريد التحقق منها، وطلب تقرير مناسب للتكامل المستمر (CI):
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
يقوم الرمز المميز للوصول (access token) بمصادقة التشغيل ويعيش في سر CI، وليس أبدًا في ملف ملتزم. يحدد العلم -t السيناريو، ويحدد -e البيئة، ويصدر -r junit,cli XML من نوع JUnit يمكن قراءته آليًا للوحة معلومات CI الخاصة بك جنبًا إلى جنب مع إخراج طرفي قابل للقراءة لسجل البناء. أنت لا تخمن المعرفات: تقوم بنسخ الأمر الدقيق، مع ملء معرفات السيناريو والبيئة الحقيقية بالفعل، من علامة تبويب CI/CD للسيناريو في Apidog. إذا كنت تريد سطح الخيارات الكامل، فإن دليل CLI الكامل يوثق كل علم، ويطبع apidog run --help هذه الأعلام عند الطلب.
سلوك البوابة هو نفس مبدأ الفرق. عندما يفشل التأكيد، لأن الاستجابة المباشرة لم تعد تتطابق مع العقد، فإن apidog run يخرج بحالة غير صفرية. يقرأ CI رمز الخروج، ويضع علامة على الخطوة بأنها فاشلة، ويحظر الدمج. لا يوجد إعداد إضافي. طالما كانت خطوة التشغيل في خط الأنابيب، فإن تراجع العقد يوقف الخط بنفس الطريقة التي يفعلها فرق التغيير الكاسر.
التسلسل الكامل قبل الدمج
ضع النصفين معًا وستحصل على خط أنابيب يكتشف كلا نوعي الكسر. يكتشف الفرق التغييرات التي قد تكسر العميل عن طريق قراءة المواصفات. يكتشف اختبار العقد خدمة لم تعد تحترم المواصفات عن طريق ممارسة واجهة برمجة التطبيقات قيد التشغيل. قم بتشغيلهما كوظيفتين منفصلتين:
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
تُشغل الوظيفتان بالتوازي. تقرأ مهمة الفرق الملفات ولا تحتاج سوى Git، لذلك تنتهي في ثوانٍ. تحتاج مهمة مطابقة العقد إلى بيئة قابلة للوصول، لذلك عادة ما تُشغل مقابل بنية اختبار (staging build) منشورة. يحافظ if: always() على التحميل على تدفق التقرير حتى عند فشل الاختبارات، وهذا هو بالضبط الوقت الذي ترغب فيه بقراءته. إذا أصبحت أي من الوظيفتين حمراء، يتم حظر طلب السحب. لمزيد من المعلومات حول تشغيل واجهة سطر الأوامر (CLI) في خطوط الأنابيب الحقيقية، فإن دليل Apidog CLI لـ GitHub Actions و الشرح التفصيلي لخط أنابيب CI/CD يتعمقان أكثر في الربط.
