CI integrationتصفَّح الأدلة

التكامل مع خطوط CI

Run Vera from GitHub Actions, GitLab CI, Bitbucket Pipelines or any CI system over the raw API, with JUnit XML reports and consistent gating behaviour.

شغِّل اختبارات Vera من خط التكامل المستمر برمز وصول، وأفشِل البناء حين يفشل اختبار حقيقي — بينما تظهر الاختبارات المعزولة، والتي نجحت بعد إعادة محاولة، والتي نجحت بعد إصلاح ذاتي، تنبيهاتٍ لا موانع للدمج. (النجاح بعد الإصلاح الذاتي مذكور هنا لاكتمال الصورة: التشغيل الذي يبدأ من CI لا يُصلح ذاتيًا اليوم، فهذه الحالة لا تقع بعد — التفاصيل.)

تشرح هذه الصفحة تشغيل مجموعة ثابتة: اختبار، أو مجموعة، أو المشروع كله. أما تشغيل الاختبارات التي يمسّها فرق طلب الدمج وحدها فانظر القسم 8 و أثر التغيير.

1. أنشئ رمز وصول

من لوحة التحكم افتح Settings ← CI Tokens (أو استدعِ POST /api/tokens بجسم { "name": "ci" }). ويُعرض الرمز vera_… مرة واحدة — فاحفظه سرًّا في خط CI (باسم VERA_TOKEN مثلًا). ودوِّن كذلك معرِّف المشروع وعنوان Vera الأساسي (VERA_URL) — وهو للنسخة المستضافة https://vera-agent.com.

2. GitHub Actions — الإجراء vera-run (المُوصى به)

name: E2E (Vera)
on: [pull_request]

jobs:
  vera:
    runs-on: ubuntu-latest
    steps:
      # ... deploy your PR preview and capture its URL ...
      - uses: mahmoodnasr/vera-run@v1 # once published; or vendor the dir and use `./.github/actions/vera-run`
        with:
          api-url: ${{ secrets.VERA_URL }}
          api-token: ${{ secrets.VERA_TOKEN }}
          project: ${{ vars.VERA_PROJECT_ID }}
          # optional:
          # suite: suite_xyz              # or `test: test_xyz`
          # environment: staging          # named Vera environment profile
          # preview-url: ${{ steps.deploy.outputs.url }}
          # fail-on-flaky: 'true'
          # fail-on-healed: 'true'

يُفشل الإجراء المهمة عند الفشل المانع وحده: أما الاختبار المعزول الذي فشل، أو الذي نجح بعد إعادة محاولة، فيُنتج تنبيه ::warning:: وجدولًا في ملخَّص المهمة (مع رابط التقرير) بدل الإفشال. وإذا أُهمل preview-url رجع الإجراء إلى VERCEL_URL أو DEPLOY_PRIME_URL أو DEPLOY_URL — فتعمل المجموعة نفسها على نشرة المعاينة لكل طلب دمج. وانظر actions/vera-run لكل المدخلات والمخرجات.

3. GitLab CI

انسخ ci/gitlab/.gitlab-ci.yml إلى ملف .gitlab-ci.yml في مشروعك (أو ضمِّنه بـ include:)، ثم:

  1. Settings ← CI/CD ← Variables: أضِف VERA_TOKEN بقيمة رمزك vera_…، واجعله MaskedProtected إن كانت فروع طلبات الدمج لديك محمية).
  2. اضبط VERA_URL وVERA_PROJECT في كتلة variables: الخاصة بالمهمة.
vera-e2e:
  stage: test
  image:
    name: alpine/curl:latest
    entrypoint: ['']
  variables:
    VERA_URL: 'https://vera.example.com'
    VERA_PROJECT: 'proj_abc123'
    VERA_PREVIEW_URL: '$CI_ENVIRONMENT_URL' # review-app URL, when your deploy job sets `environment:`
  script:
    - | # the embedded gate script — see the template for the full body
  artifacts:
    when: always
    reports:
      junit: vera-junit/*.xml # per-step results in the MR test widget
      dotenv: .vera/result.env # VERA_STATUS/VERA_PASSED/... for later jobs

ولا تحتاج المهمة إلا إلى curl وصَدَفة POSIX — بلا Node، وبلا jq، وبلا خطوة تثبيت. وهي لا تضبط allow_failure: true عن عمد: فالسكربت هو الذي يقرِّر ما يمنع الدمج، وجعل المهمة كلها غير مانعة كان سيُخفي الأعطال الحقيقية أيضًا.

4. Bitbucket Pipelines

انسخ ci/bitbucket/bitbucket-pipelines.yml إلى bitbucket-pipelines.yml (أو ادمج خطوته في خطوتك)، ثم:

  1. Repository settings ← Repository variables: أضِف VERA_TOKEN بقيمة رمزك vera_…، واجعله Secured.
  2. عدِّل سطرَي export VERA_URL= وexport VERA_PROJECT= في رأس الخطوة (فليس في Bitbucket كتلة variables: لكل خطوة).

وتكتب الخطوة ملفات JUnit XML في test-results/، وهو مسار يلتقطه Bitbucket تلقائيًّا ويعرضه في تبويب Tests الخاص بالبناء. والصورة الافتراضية في Bitbucket فيها curl أصلًا.

5. تكافؤ المنع بين مزوِّدي CI

الأسطح الثلاثة تمنع الدمج بالطريقة نفسها. وهذا وعدٌ من المنتج لا مصادفة: فاختبار معزول يُحمِّر خط GitLab لديك بينما ينبِّه فقط على GitHub يجعل العزل بلا قيمة.

النتيجةGitHub ActionGitLab CIBitbucket Pipelines
نجحت كل الاختباراتنجاحنجاحنجاح
فشل حقيقي (غير معزول)فشلفشلفشل
فشل اختبار معزولنجاح + تنبيهنجاح + تنبيهنجاح + تنبيه
اختبار نجح بعد إعادة محاولةنجاح + تنبيهنجاح + تنبيهنجاح + تنبيه
…مع تفعيل fail-on-flakyفشلفشلفشل
اختبار نجح بعد إصلاح مُحدِّد ذاتيًّانجاح + تنبيهنجاح + تنبيهنجاح + تنبيه
…مع تفعيل fail-on-healedفشلفشلفشل
رمز باطل أو مُبطَل، أو 5xx، أو تعذَّر الوصولفشلفشلفشل

وسطرا الإصلاح الذاتي يصفان ما سيفعله كل سطح، ومجموعة اختبارات التكافؤ تُثبت أنها متفقة — غير أن التشغيل الذي يبدأ من CI لا يبلغ تلك الحالة اليوم. انظر النجاح بعد الإصلاح الذاتي لا يقع من CI بعد.

ولك أن تختار إفشال البناء على تذبذب إعادة المحاولة بـ fail-on-flaky: 'true' في GitHub، أو VERA_FAIL_ON_FLAKY: 'true' في GitLab وBitbucket. أما فشل الاختبار المعزول فيبقى غير مانع في الحالتين — فالعزل صمام أمان، وصمامٌ يظل قادرًا على إفشال البناء ليس صمامًا.

والخياران مستقلان: fail-on-flaky لا يمنع أبدًا نجاحًا جاء بعد إصلاح ذاتي، وfail-on-healed لا يمنع تذبذبًا. وكلاهما مُختبَر في سطر خاص به ضمن مجموعة التكافؤ.

الأخضر ليس حالة واحدة

التشغيل الذي يُبلِّغ بـ passed بلغ ذلك بإحدى ثلاث طرق، وVera تخبرك بأيِّها:

ما الذي حدثيمنع الدمج افتراضيًّا
PASSفعل الاختبار ما يقوله، من أول مرة
PASS ON RETRYفشل ثم نجح في إعادة المحاولة — أي متذبذبلا (fail-on-flaky)
PASS ON HEALتعطَّل مُحدِّد، فأعاد الإصلاح الذاتي كتابته فنجح الاختبارلا (fail-on-healed) — ولا يقع من CI بعد

والإصلاح الذاتي لا يعيد كتابة إلا المُحدِّد، ولا يمسّ التوقُّع أبدًا — فالتحقُّق ظلّ لازمًا أن يصدق على تطبيقك، ولهذا لا يُعدّ النجاح بعد إصلاح فشلًا. لكن ما قد يُخفيه هو تغيُّر في المعنى: فإن اختفى زرّ Delete customer وربط المُصلِح الخطوة بعنصر مجاور، فقد ينجح تحقُّق فضفاض رغم ذلك. والتفاعل الذي كُتب الاختبار من أجله صار غائبًا، وعمود نجاح/فشل لا يستطيع أن يريك هذا.

ولذلك يُذكر healed بجانب الحالة لا مطويًّا داخلها، وتفصيل التشغيل يسمّي كل مُحدِّد أُعيدت كتابته (قبل ← بعد). والفِرَق التي تمنع الدمج في فرع الإصدار تضبط عادةً fail-on-healed: 'true' هناك وتتركه مطفأً في فروع الميزات: فيصير الإصلاح عندئذٍ إشارةً مانعة تستدعي إنسانًا يؤكِّد أن الاختبار ما زال يعني ما كان يعنيه.

النجاح بعد الإصلاح الذاتي لا يقع من CI بعد

PASS ON HEAL حالة حقيقية، لكنها ليست مما يُنتجه تشغيلٌ بدأ من CI اليوم، ويجدر بك أن تعرف ذلك قبل أن تعتمد على الخيار.

فالتشغيل الذي يبدأ عبر POST /api/v1/…/run — وهو ما تستدعيه الأسطح الثلاثة أعلاه — يُنفَّذ في السحابة بلا مُصلِح مُحدِّدات مرتبط به. فالإصلاح الذاتي في Vera إجراء إصلاح صريح: زر Repair على تشغيل فاشل، وخدمة الإصلاح التلقائي التي تُفعِّلها أنت. وهو عمدًا ليس خاصية ملازمة لأي تشغيل عادي، لأنه عدد غير محدود من نداءات الذكاء الاصطناعي الإضافية لكل تشغيل، ولأن تشغيلًا بلا رقيب يعيد كتابة مُحدِّداته بصمت يجعل كل «أخضر» ملتبسًا — وهو الالتباس نفسه الذي وُجد هذا القسم ليزيله.

فعلى مسار CI تكون قيمة healed هي false أو 0، ولا يُفعَّل fail-on-healed ولا VERA_FAIL_ON_HEALED أبدًا. وقد أُبقي الخيار موثَّقًا ومُختبَرًا في مجموعة تكافؤ المنع حتى يضبطه الفريق مرة واحدة فيسري يوم يصير التشغيل من CI قادرًا على الإصلاح، بدل أن يجد الضبط غائبًا في اللحظة التي يحتاجه فيها. أما التشغيلات المُصلَحة نفسها فحقيقية وتحمل healed في GET /api/v1/runs/:id — لكنها تأتي من أسطح الإصلاح، وهي ليست ما يبدأه CI.

ولا يتأثر fail-on-flaky بشيء من ذلك. فإعادة المحاولة تقع في كل تشغيل، بمُصلِح أو بغيره، ومن ثَمّ فحالة PASS ON RETRY تُبلَّغ وتُمنَع اليوم كما وُصف أعلاه تمامًا.

وكيف يُصان هذا الوعد:

  • تشغِّل GitLab وBitbucket السكربت نفسه، ci/vera-gate.sh، مضمَّنًا حرفيًّا في القالبين.
  • ويقوم apps/server/src/services/ci-gating-parity.test.ts بنصب /api/v1 وهميّ، ثم ينفِّذ إجراء GitHub وسكربت الصَّدَفة معًا على كل سطر من ذلك الجدول، ويفشل إن اختلفت رموز خروجهما أو إن انحرفت النسخة المضمَّنة في أحد القالبين. ويشغِّله pnpm test، فلا يستطيع الجدول أعلاه أن يتقادم بصمت.

6. الواجهة البرمجية المباشرة (لأي نظام CI)

  • POST /api/v1/projects/:id/run — بجسم { "testId": "…" } أو { "suiteId": "…" } أو { "testIds": ["…", "…"] } (بحدٍّ أقصى 50؛ وما زاد يردّ 400 too_many_test_ids ولا يُشغَّل شيء — لأن نتيجةً مبنية على أول 50 من 84 ستُقرأ كأنها نتيجة كاملة)، أو { "all": true }. واحدٌ منها بالضبط: فالجمع بين testIds وtestId أو suiteId أو all مرفوض. ويقبل اختياريًّا "environment": "staging" (ملف بيئة مُسمّى) و "baseUrlOverride": "https://pr-42.example.com" (وهو يغلب البيئة — ومقصود به روابط المعاينة لكل طلب دمج). ويعمل تزامنيًّا ويُعيد النتيجة النهائية:

    {
      "runIds": ["…"],
      "status": "passed",
      "passed": 12,
      "failed": 1,
      "blockingFailed": 0,
      "quarantinedFailed": 1,
      "passedOnRetry": 2,
      "healed": 1,
      "reportUrl": "https://vera.example.com/projects/proj_x/tests"
    }

    امنع الدمج بناءً على status (وهو يراعي العزل أصلًا: لا يُفشله إلا فشل غير معزول) أو على blockingFailed. أما healed فيعدّ التشغيلات التي لم تبلغ الأخضر إلا بعد أن أعاد الإصلاح الذاتي كتابة مُحدِّد — وهو ليس جزءًا من status أبدًا، ويُبلَّغ دائمًا، وقيمته دائمًا 0 من هذه النقطة اليوم، لأن التشغيل الذي يبدأ هنا بلا مُصلِح (لماذا). أما تشغيل اختبار واحد فيُعيد { "status", "blocking", "quarantined", "passedOnRetry", "healed", "reportUrl" } — وامنع الدمج عندها على blocking.

  • GET /api/v1/runs/:id — الحالة الراهنة (مع passedOnRetry وhealed). وخلافًا لنقطة التشغيل أعلاه، تقرأ هذه ما يحمله السجل فعلًا، فتُبلِّغ healed: true لتشغيل أنتجه أحد أسطح الإصلاح.

  • GET /api/v1/runs/:id/wait — يبقى منتظرًا حتى 60 ثانية لحالة نهائية (ويردّ 202 إن كان ما يزال يعمل).

  • GET /api/v1/runs/:id/junit — التشغيل بصيغة JUnit XML (انظر القسم 7).

  • GET /api/v1/suite-runs/:id/junit — تشغيل مجموعة بصيغة JUnit XML (بعنصر <testsuite> لكل تشغيل عضو).

وكلها تتطلب Authorization: Bearer vera_…. والرمز المُبطَل أو الباطل يردّ 401.

resp=$(curl -sS -X POST \
  -H "Authorization: Bearer $VERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"all": true}' \
  "$VERA_URL/api/v1/projects/$PROJECT_ID/run")
[ "$(echo "$resp" | jq -r '.status')" = "passed" ] || { echo "::error::Vera run failed"; exit 1; }

7. JUnit XML — اعرض النتائج في واجهة فحوص طلب الدمج

يمكن تصدير كل تشغيل (وكل تشغيل مجموعة) بصيغة JUnit XML، وهي الصيغة التي تفهمها إجراءات تقارير الاختبارات. مرِّرها إلى dorny/test-reporter، أو mikepenz/action-junit-report، أو إلى قارئ JUnit المدمج في نظامك، فتحصل على تفصيل نجاح/فشل لكل خطوة مرفقًا بالفحص — بلا تنقيب في JSON.

  • GET /api/v1/runs/:id/junit ← عنصر <testsuite> واحد (الاختبار)، وعنصر <testcase> لكل خطوة.
  • GET /api/v1/suite-runs/:id/junit ← عنصر <testsuites> يضمّ <testsuite> لكل تشغيل عضو.

وكلاهما يردّ بـ Content-Type: application/xml مع Content-Disposition كمرفق، ويتطلب Authorization: Bearer vera_…. أما النسختان اللتان تعتمدان جلسة المتصفح (GET /api/runs/:id/junit وGET /api/suite-runs/:id/junit) فتعيدان الجسم نفسه للوحة التحكم.

وما يقابل ماذا:

JUnitVera
<testsuite name>اسم الاختبار (وتشغيلات المصفوفة أو مجموعات البيانات تُلحق [cell] أو [row: …])
<testcase name>#<idx> <action> <selector/url snippet> لكل خطوة
<testcase classname>اسم الاختبار
<testcase time>مدة الخطوة بالثواني
<failure message=…> والجسمخطأ الخطوة الفاشلة (السطر الأول رسالةً، والنص كاملًا في جسم CDATA)
<skipped/>خطوة لم تُنفَّذ أصلًا (بعد فشل سابق مثلًا)
<properties>runId وtestId وengine وenvironment وstatus

ومثال على خطوة في GitHub Actions — تُطلق تشغيلًا، وتنتظره، وتنزِّل ملف JUnit الخاص به، ثم تنشره:

- name: Run Vera + publish JUnit
  run: |
    # trigger a single test (or suiteId / all:true) and capture the run id
    run_id=$(curl -sS -X POST \
      -H "Authorization: Bearer ${{ secrets.VERA_TOKEN }}" \
      -H "Content-Type: application/json" \
      -d '{"testId": "'"${{ vars.VERA_TEST_ID }}"'"}' \
      "${{ secrets.VERA_URL }}/api/v1/projects/${{ vars.VERA_PROJECT_ID }}/run" \
      | jq -r '.runIds[0]')

    # block until it reaches a terminal status, then fetch the JUnit report
    curl -sS -H "Authorization: Bearer ${{ secrets.VERA_TOKEN }}" \
      "${{ secrets.VERA_URL }}/api/v1/runs/$run_id/wait" > /dev/null
    curl -sS -H "Authorization: Bearer ${{ secrets.VERA_TOKEN }}" \
      "${{ secrets.VERA_URL }}/api/v1/runs/$run_id/junit" -o vera-junit.xml

- name: Publish test report
  if: always()
  uses: mikepenz/action-junit-report@v4
  with:
    report_paths: vera-junit.xml

أما الإطلاق الجماعي ({"suiteId": "…"} أو {"all": true}) فيُعيد مصفوفة runIds — فاجلب GET /api/v1/runs/<id>/junit لكل معرِّف وسلِّم المُبلِّغ كل الملفات، أو — إن كان تشغيل المجموعة مسجَّلًا في صفٍّ ضمن suite_runs (أطلقته لوحة التحكم أو المُجدوِل) — اجلب الملف المجمَّع الواحد GET /api/v1/suite-runs/<suiteRunId>/junit.

8. شغِّل الاختبارات المتأثرة وحدها (vera check)

كل ما سبق يشغِّل مجموعة ثابتة. أما في طلب الدمج فأنت تريد عادةً أن تتبع المجموعةُ الفرقَ البرمجي — وهذا هو vera check --run، الموثَّق بالكامل في أثر التغيير:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # so `origin/<base_ref>` resolves

- name: Impacted tests
  run: npx vera-agent check --base origin/${{ github.base_ref }} --run
  env:
    VERA_API_URL: ${{ vars.VERA_API_URL }}
    VERA_PROJECT_ID: ${{ vars.VERA_PROJECT_ID }}
    VERA_TOKEN: ${{ secrets.VERA_TOKEN }}

وثلاثة أمور اعرفها قبل أن تنسخ ذلك إلى أي مكان:

  • أسماء المتغيرات تختلف عن بقية هذه الصفحة. فواجهة الأوامر تقرأ VERA_API_URL وVERA_PROJECT_ID وVERA_TOKEN؛ والإجراء vera-run يأخذ مدخلات api-url وproject وapi-token؛ وسكربت الصَّدَفة يقرأ VERA_URL وVERA_PROJECT وVERA_TOKEN. ولا يشترك الثلاثة إلا في اسم الرمز.
  • fetch-depth: 0 لا غنى عنه. فـ actions/checkout يستنسخ نسخة ضحلة افتراضيًّا لا يوجد فيها origin/<base_ref>، و--base صريح لا يُحلّ يُفشل الخطوة.
  • --strict اختياري ويأتي لاحقًا. فهو يُفشل البناء على قاعدة عمل يمسّها الفرق البرمجي ولا يحرسها اختبار كافٍ، وفي مشروع ما تزال تغطية خريطته قريبة من الصفر سيكون أحمر في كل طلب دمج. فعِّله حين تسمح الأرقام — والتفصيل في القسم 9 من دليل أثر التغيير.

ورموز الخروج هي: 0 سليم · 1 خطأ استعمال أو إعداد · 2 تعذَّر الإتمام · 3 فشل اختبار · 4 وجد --strict ثغرة. أما التعليق الثابت على طلب الدمج الذي يعرض هذا التقرير فمخطَّط لا مُنجَز؛ واليوم هي خطوة تنجح أو تفشل وتطبع في السجل، مع --json إن أردت بناء عرضك بنفسك.

ملاحظات

  • الرموز محصورة بالمؤسسة؛ فالرمز لا يبلغ إلا مشاريع مؤسسته.
  • ولا يُخزَّن على الخادم إلا نصّ الرمز المعمَّى (SHA-256) — فالنص الصريح لا يمكن استرجاعه بعد الإنشاء. والتدوير يكون بإنشاء رمز جديد وإبطال القديم.
  • النجاح بعد الإصلاح الذاتي: تكون healed صحيحةً أو عددًا حين لا يبلغ التشغيل الأخضر إلا بعد إعادة كتابة مُحدِّد متعطِّل أثناء التشغيل. وهي لا تغيِّر status أبدًا؛ وامنع الدمج عليها بـ fail-on-healed أو VERA_FAIL_ON_HEALED إن أردت إنسانًا يؤكِّد أن الخطوة المُعاد كتابتها ما تزال تختبر ما كُتبت له. ولاحظ أن التشغيل من CI لا يُصلح ذاتيًّا اليوم، فهذا الخيار خامل حاليًّا — الشرح الكامل. وfail-on-flaky غير متأثر.
  • دلالات العزل والتذبذب: الاختبار المعزول يظل يعمل ويُبلِّغ، لكن فشله لا يُفشل status أبدًا؛ والتشغيل الذي فشل ثم نجح في إعادة محاولة يُبلِّغ passedOnRetry. وكلاهما يظهر في الرد ليتمكن CI من عرضهما تنبيهين.