راه‌اندازی یکپارچه‌سازی مستمر


راه‌اندازی یکپارچه‌سازی مداوم (CI) برای مسیر شما بسیار مهم است، چون به پیدا کردن اشتباه‌ها کمک می‌کند.

GitHub Actions

مخزن‌های Exercism (از جمله مخزن‌های مسیرها) برای اجرای CI خود از GitHub Actions استفاده می‌کنند. GitHub Actions بر پایه‌ی _workflow_ها هستند؛ workflowها اسکریپت‌هایی را تعریف می‌کنند که هر بار رویداد مشخصی رخ دهد (مثلاً push کردن یک commit) به‌طور خودکار اجرا می‌شوند. برای اطلاعات بیشتر درباره‌ی workflowهای GitHub Actions، مستندات workflowها را ببینید.

workflowهای از پیش نصب‌شده

مسیرها همراه با تعدادی workflow از پیش نصب‌شده عرضه می‌شوند که بیشترشان را نباید تغییر دهید (به آن‌ها workflowهای مشترک می‌گویند). با این حال، یک workflow هست که باید تغییرش دهید: workflow مربوط به test.yml.

workflow مربوط به test

هدف workflow مربوط به test.yml این است که بررسی کند تمرین‌های مسیر در وضعیت درستی قرار دارند. این workflow طوری تنظیم شده است که وقتی روی شاخه‌ی main یا روی شاخه‌ی یک pull request یک push انجام می‌شود، به‌طور خودکار اجرا شود (در اصطلاح GitHub Actions: trigger می‌شود).

خود workflow نباید کار زیادی انجام دهد، جز این موارد:

  • checkout کردن code (از قبل پیاده‌سازی شده)
  • نصب وابستگی‌ها (مثلاً نصب بسته‌ها، اختیاری)
  • نصب ابزارها (مثلاً نصب یک SDK، یعنی کیت توسعه‌ی نرم‌افزار؛ اختیاری)
  • اجرای اسکریپت بررسی تمرین‌ها (از قبل پیاده‌سازی شده)

پیاده‌سازی اسکریپت بررسی تمرین‌ها

همان‌طور که گفته شد، تمرین‌ها با یک اسکریپت بررسی می‌شوند، یعنی اسکریپت bin/verify-exercises (bash). این اسکریپت تقریباً آماده است و کارهای زیر را انجام می‌دهد:

  • روی همه‌ی پوشه‌های تمرین حلقه می‌زند
  • سپس برای هر پوشه‌ی تمرین:
    • راه‌حل example/exemplar را به فایل‌های راه‌حل (stub) کپی می‌کند (از قبل پیاده‌سازی شده)
    • تابع unskip_tests را فراخوانی می‌کند که در آن می‌توانید testها را در فایل‌های test خود unskip کنید (اختیاری)
    • تابع run_tests را فراخوانی می‌کند که در آن باید testها را اجرا کنید (الزامی)

توابع run_tests و unskip_tests تنها مواردی هستند که باید پیاده‌سازی کنید.

خارج کردن testها از حالت skip

اگر مسیر شما از skip کردن testها پشتیبانی می‌کند، باید مطمئن شویم هنگام بررسی راه‌حل example/exemplar یک تمرین، هیچ testی skip نمی‌شود. به‌طور کلی، مسیرها به دو روش از «unskip کردن» testها پشتیبانی می‌کنند:

  1. حذف حاشیه‌نویسی‌ها، code و متن از فایل‌های test. برای مثال، تغییر test.skip به test.
  2. فراهم کردن یک متغیر محیطی. برای مثال، تنظیم SKIP_TESTS=false.

حذف حاشیه‌نویسی‌ها، code و متن از فایل‌های test

اگر skip کردن testها مبتنی بر فایل باشد (همان گزینه‌ی اولی که بالا گفته شد)، تابع unskip_tests را ویرایش کنید تا فایل‌های test را تغییر دهید (code موجود از قبل حلقه زدن روی فایل‌های test را مدیریت می‌کند).

Note

تابع unskip_test روی یک کپی از پوشه‌ی تمرین اجرا می‌شود، پس با خیال راحت فایل‌ها را هرطور که صلاح می‌دانید تغییر دهید.

مثال

فایل bin/verify-exercises file در مسیر Arturo از sed استفاده می‌کند تا testها را درون فایل‌های test از حالت skip خارج کند:

unskip_tests() {
    jq -r '.files.test[]' .meta/config.json | while read -r test_file; do
        sed -i 's/test.skip/test/g' "${test_file}"
    done
}

فراهم کردن یک متغیر محیطی

Caution

اگر خارج کردن testها از حالت skip نیازمند تنظیم یک متغیر محیطی است، مطمئن شوید که آن را در تابع run_tests تنظیم کرده‌اید.

اجرای testها

تابع run_tests مسئول اجرای testهای یک تمرین است. وقتی این تابع فراخوانی می‌شود، فایل‌های example/exemplar از قبل در فایل‌های راه‌حل (stub) کپی شده‌اند، بنابراین فقط باید فرمان درست را برای اجرای testها فراخوانی کنید.

اگر همه‌ی testها موفق شوند، تابع باید کد خروجی صفر برگرداند؛ در غیر این صورت باید یک کد خروجی غیرصفر برگرداند.

Note

تابع run_tests روی یک کپی از پوشه‌ی تمرین اجرا می‌شود، پس با خیال راحت فایل‌ها را هرطور که صلاح می‌دانید تغییر دهید.

گزینه‌ی ۱: استفاده از ابزارهای زبان

گزینه‌ی پیش‌فرض برای اسکریپت بررسی تمرین‌ها، استفاده از ابزارهای خود زبان (SDK یا فایل اجرایی و از این قبیل) است که بیشتر مسیرها از آن استفاده می‌کنند. هر مسیر روش خودش را برای اجرای testها دارد، اما معمولاً فقط یک فرمان است.

مثال

فایل bin/verify-exercises file در مسیر Arturo تابع run_tests را طوری تغییر می‌دهد که فقط فرمان arturo را روی فایل test فراخوانی کند:

run_tests() {
    arturo tester.art
}

گزینه‌ی ۲: استفاده از ایمیج Docker مربوط به test runner

گزینه‌ی دوم این است که تمرین‌ها را با اجرای test runner مسیر بررسی کنید. البته این بستگی دارد به اینکه مسیر یک test runner کارآمد داشته باشد.

اگر مسیر شما هنوز test runner ندارد، می‌توانید یکی از این دو کار را بکنید:

  • یک test runner کارآمد بسازید، یا
  • از گزینه‌ی ۱ استفاده کنید و مستقیماً از ابزارهای زبان بهره ببرید

تغییرات زیر باید در اسکریپت پیش‌فرض bin/verify-exercises اعمال شود:

  1. بررسی کنید که فرمان docker در دسترس است
  2. ایمیج Docker مربوط به test runner را pull (دانلود) کنید
  3. با docker run ایمیج Docker مربوط به test runner را روی هر تمرین اجرا کنید
  4. با jq بررسی کنید که فایل results.json برگردانده‌شده از کانتینر Docker نشان دهد همه‌ی testها موفق شده‌اند
  5. تابع unskip_test و فراخوانی آن را حذف کنید
Note

مزیت اصلی این روش این است که بیشترین شباهت را به نحوه‌ی اجرای testها در محیط تولید (روی وب‌سایت) دارد. با این روش، احتمال اینکه مواردی که در CI موفق شده‌اند در محیط تولید شکست بخورند کمتر است. عیب این روش این است که معمولاً کندتر است، چون باید ایمیج Docker را pull کرد و سربار Docker هم روی آن می‌آید.

مثال

فایل bin/verify-exercises file در مسیر Unison بررسی‌ای اضافه می‌کند تا مطمئن شود فرمان docker هم نصب است:

required_tool docker

سپس ایمیج test runner مسیر را pull می‌کند:

docker pull exercism/unison-test-runner

بعد تابع run_tests را تغییر می‌دهد تا با docker run، test runner را روی تمرین فعلی (که در پوشه‌ی کاری قرار دارد) اجرا کند و پس از آن با یک فرمان jq وضعیت درست را بررسی کند:

run_tests() {
    local slug

    slug="${1}"

    docker run \
        --rm \
        --network none \
        --mount type=bind,src="${PWD}",dst=/solution \
        --mount type=bind,src="${PWD}",dst=/output \
        --tmpfs /tmp:rw \
        exercism/unison-test-runner "${slug}" "/solution" "/output"
    jq -e '.status == "pass"' "${PWD}/results.json" >/dev/null 2>&1
}

در پایان، باید نحوه‌ی فراخوانی فرمان run_tests را تغییر دهیم، چون اکنون به slug نیاز دارد:

run_tests "${slug}"

پیاده‌سازی workflow مربوط به test

حالا که اسکریپت verify-exercises تمام شده، وقت آن است که workflow مربوط به test.yml را نهایی کنیم. اینکه این کار را چگونه انجام دهیم، به گزینه‌ای بستگی دارد که برای پیاده‌سازی اسکریپت verify-exercises انتخاب شده است.

گزینه‌ی ۱: استفاده از ابزارهای زبان

اگر اسکریپت verify-exercises مستقیماً از ابزارهای زبان استفاده می‌کند، workflow مربوط به test باید این موارد را نصب کند:

  • وابستگی‌های ابزارهای زبان، مانند openssh یا کامپایلر C/C++.
  • ابزارهای زبان، مانند یک SDK یا فایل اجرایی. اگر نصب ابزارهای زبان، فایل یا فایل‌های اجرایی نصب‌شده را به path اضافه نمی‌کند، حتماً آن را به مسیر سیستمی GitHub Actions اضافه کنید.

وقتی این کار انجام شد، verify-exercises باید همان‌طور که انتظار دارید کار کند و CI را با موفقیت راه‌اندازی کرده‌اید!

برای نمونه، workflow مربوط به test.yml در مسیر Arturo را ببینید:

name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  ci:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout repository
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev libmpfr-dev

      - name: Install Arturo
        run: bin/install-arturo
        env:
          GH_TOKEN: ${{ github.token }}

      - name: Verify all exercises
        run: bin/verify-exercises

گزینه‌ی ۲: استفاده از ایمیج Docker مربوط به test runner

گزینه‌ی دوم این است که تمرین‌ها را با اجرای test runner مسیر بررسی کنید. این گزینه دو شرط دارد:

  1. مسیر یک test runner کارآمد داشته باشد
  2. اسکریپت verify-exercises از ایمیج Docker مربوط به test runner برای اجرای testهای یک تمرین استفاده کند

اگر مسیر شما هنوز test runner ندارد، می‌توانید یکی از این دو کار را بکنید:

  • یک test runner کارآمد بسازید، یا
  • از گزینه‌ی ۱ استفاده کنید و مستقیماً از ابزارهای زبان بهره ببرید

این روش چند مزیت دارد:

  1. لازم نیست هیچ وابستگی یا ابزاری را درون workflow مربوط به test نصب کنید (چون آن‌ها از قبل درون ایمیج Docker نصب شده‌اند)
  2. این روش بیشترین شباهت را به نحوه‌ی اجرای testها در محیط تولید (روی وب‌سایت) دارد و احتمال بروز مشکل در محیط تولید را کاهش می‌دهد.

عیب اصلی آن این است که احتمالاً کندتر است، چون باید ایمیج Docker را pull کرد و سربار Docker هم به آن اضافه می‌شود.

چند راه برای pull کردن ایمیج Docker مربوط به test runner وجود دارد:

  1. دانلود ایمیج درون فایل verify-exercises. این همان روشی است که مسیر Unison به کار می‌برد.
  2. دانلود ایمیج درون workflow. این همان روشی است که مسیر Standard ML به کار می‌برد.
  3. ساخت ایمیج درون workflow. این همان روشی است که مسیر 8th به کار می‌برد.

خب، کدام روش را به کار ببریم؟ توصیه می‌کنیم حداقل گزینه‌ی شماره ۱ را پیاده کنید تا اسکریپت verify-exercises مستقل باشد. اگر ایمیج شما به‌طور خاص بزرگ است، ممکن است پیاده‌سازی گزینه‌ی ۳ هم مفید باشد، چون ایمیج ساخته‌شده‌ی Docker را در cache مربوط به GitHub Actions ذخیره می‌کند. اجراهای بعدی می‌توانند ایمیج Docker را به‌جای دانلود از cache بخوانند که برای کارایی بهتر است (لطفاً برای اطمینان اندازه‌گیری کنید).

گزینه‌ی ۳: اجرای اسکریپت بررسی تمرین‌ها درون ایمیج Docker مربوط به test runner

گزینه‌ی سوم و جایگزین، ترکیبی از دو گزینه‌ی قبلی است. اینجا هم از ایمیج Docker مربوط به test runner استفاده می‌کنیم، با این تفاوت که این بار اسکریپت verify-exercises را درون همان ایمیج Docker اجرا می‌کنیم. برای فعال کردن این گزینه، باید container مربوط به workflow را روی test runner تنظیم کنیم:

container:
  image: exercism/vimscript-test-runner

سپس می‌توانیم مرحله‌های نصب وابستگی‌ها و ابزارها را رد کنیم (چون آن‌ها از قبل درون ایمیج Docker مربوط به test runner نصب شده‌اند) و به اجرای اسکریپت bin/verify-exercises بپردازیم.

مثال

workflow مربوط به test.yml در مسیر vimscript از این گزینه استفاده می‌کند:

name: Verify Exercises

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  ci:
    runs-on: ubuntu-24.04
    container:
      image: exercism/vimscript-test-runner

    steps:
      - name: Checkout repository
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Verify all exercises
        run: bin/verify-exercises