التكامل مع خطوط 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:)، ثم:
- Settings ← CI/CD ← Variables: أضِف
VERA_TOKENبقيمة رمزكvera_…، واجعله Masked (وProtected إن كانت فروع طلبات الدمج لديك محمية). - اضبط
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 (أو ادمج خطوته في خطوتك)، ثم:
- Repository settings ← Repository variables: أضِف
VERA_TOKENبقيمة رمزكvera_…، واجعله Secured. - عدِّل سطرَي
export VERA_URL=وexport VERA_PROJECT=في رأس الخطوة (فليس في Bitbucket كتلةvariables:لكل خطوة).
وتكتب الخطوة ملفات JUnit XML في test-results/، وهو مسار يلتقطه Bitbucket
تلقائيًّا ويعرضه في تبويب Tests الخاص بالبناء. والصورة الافتراضية في
Bitbucket فيها curl أصلًا.
5. تكافؤ المنع بين مزوِّدي CI
الأسطح الثلاثة تمنع الدمج بالطريقة نفسها. وهذا وعدٌ من المنتج لا مصادفة: فاختبار معزول يُحمِّر خط GitLab لديك بينما ينبِّه فقط على GitHub يجعل العزل بلا قيمة.
| النتيجة | GitHub Action | GitLab CI | Bitbucket 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) فتعيدان
الجسم نفسه للوحة التحكم.
وما يقابل ماذا:
| JUnit | Vera |
|---|---|
<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 من عرضهما تنبيهين.