⏱️ وقت القراءة المقدر: 18 دقائق

هذا الجزء الثالث من سلسلة GitHub CLI. سنستكشف كيفية بناء نظام إدارة مشاريع قائم على السياق وأتمتة مهام سير العمل المتكررة.

إدارة المشاريع القائمة على السياق

فهم بنية GitHub Projects v2

يوفر GitHub Projects v2 نظام إدارة مشاريع مرن للغاية. دعنا نتعلم كيفية أتمتته بالكامل.

#!/bin/bash
# project-automation.sh - أتمتة إدارة GitHub Projects

# الحصول على معرف المشروع
get_project_id() {
    local org="${1}"
    local project_name="${2}"
    
    gh project list \
        --owner "${org}" \
        --format json \
        --jq ".projects[] | select(.title == \"${project_name}\") | .number"
}

# إنشاء مشروع جديد
create_project_with_fields() {
    local org="${1}"
    local project_name="${2}"
    
    echo "إنشاء مشروع: ${project_name}"
    
    # إنشاء المشروع
    local project_number
    project_number=$(gh project create \
        --owner "${org}" \
        --title "${project_name}" \
        --format json \
        --jq '.number')
    
    echo "رقم المشروع: ${project_number}"
    
    # إضافة حقل الأولوية
    gh project field-create "${project_number}" \
        --owner "${org}" \
        --name "الأولوية" \
        --data-type SINGLE_SELECT \
        --single-select-options "حرج,عالٍ,متوسط,منخفض"
    
    # إضافة حقل تقدير نقاط القصة
    gh project field-create "${project_number}" \
        --owner "${org}" \
        --name "نقاط القصة" \
        --data-type NUMBER
    
    # إضافة حقل الإصدار
    gh project field-create "${project_number}" \
        --owner "${org}" \
        --name "الإصدار" \
        --data-type TEXT
    
    echo "تم إنشاء المشروع ${project_name} مع الحقول المخصصة"
    echo "${project_number}"
}

إدارة العناصر في المشروع

# إضافة issue إلى مشروع
add_issue_to_project() {
    local issue_number="${1}"
    local project_number="${2}"
    local owner="${3}"
    local repo="${4}"
    
    # الحصول على URL الـ issue
    local issue_url
    issue_url=$(gh issue view "${issue_number}" \
        --repo "${repo}" \
        --json url \
        --jq '.url')
    
    # إضافة إلى المشروع
    gh project item-add "${project_number}" \
        --owner "${owner}" \
        --url "${issue_url}"
    
    echo "تمت إضافة الـ issue #${issue_number} إلى المشروع ${project_number}"
}

# تحديث حقول العنصر في المشروع
update_project_item() {
    local project_number="${1}"
    local item_id="${2}"
    local field_name="${3}"
    local field_value="${4}"
    local owner="${5}"
    
    # الحصول على معرف الحقل
    local field_id
    field_id=$(gh project field-list "${project_number}" \
        --owner "${owner}" \
        --format json \
        --jq ".fields[] | select(.name == \"${field_name}\") | .id")
    
    if [[ -z "${field_id}" ]]; then
        echo "خطأ: الحقل '${field_name}' غير موجود"
        return 1
    fi
    
    # تحديث قيمة الحقل
    gh project item-edit \
        --project-id "${project_number}" \
        --id "${item_id}" \
        --field-id "${field_id}" \
        --text "${field_value}" \
        --owner "${owner}"
    
    echo "تم تحديث حقل '${field_name}' إلى '${field_value}'"
}

ربط المشكلات بالمشاريع

أتمتة ربط المشكلات

#!/bin/bash
# issue-project-linker.sh - أتمتة ربط المشكلات بالمشاريع

# تصنيف المشكلة وإضافتها تلقائيًا
auto_categorize_and_add() {
    local issue_number="${1}"
    local repo="${2}"
    local org="${3}"
    
    # الحصول على بيانات المشكلة
    local issue_data
    issue_data=$(gh issue view "${issue_number}" \
        --repo "${repo}" \
        --json "title,labels,assignees,body,milestone")
    
    local title
    title=$(echo "${issue_data}" | jq -r '.title')
    
    local labels
    labels=$(echo "${issue_data}" | jq -r '.labels[].name' | tr '\n' ',')
    
    # تحديد المشروع المناسب بناءً على التسميات
    local target_project
    case "${labels}" in
        *bug*)
            target_project="تتبع الأخطاء"
            priority="عالٍ"
            ;;
        *feature*)
            target_project="خارطة الطريق"
            priority="متوسط"
            ;;
        *documentation*)
            target_project="الوثائق"
            priority="منخفض"
            ;;
        *urgent*|*critical*)
            target_project="تتبع الأخطاء"
            priority="حرج"
            ;;
        *)
            target_project="المهام العامة"
            priority="متوسط"
            ;;
    esac
    
    # الحصول على رقم المشروع
    local project_number
    project_number=$(get_project_id "${org}" "${target_project}")
    
    if [[ -z "${project_number}" ]]; then
        echo "تحذير: المشروع '${target_project}' غير موجود"
        return 1
    fi
    
    # إضافة إلى المشروع
    local item_id
    item_id=$(add_issue_to_project "${issue_number}" "${project_number}" "${org}" "${repo}")
    
    # تعيين الأولوية
    update_project_item "${project_number}" "${item_id}" "الأولوية" "${priority}" "${org}"
    
    echo "تمت إضافة المشكلة #${issue_number} '${title}' إلى مشروع '${target_project}' بأولوية '${priority}'"
}

تلخيص وقوف الفريق اليومي

إنشاء تقرير الفريق التلقائي

#!/bin/bash
# team-standup.sh - أتمتة تلخيص وقوف الفريق

generate_standup_report() {
    local repo="${1}"
    local org="${2}"
    local since="${3:-yesterday}"
    
    echo "=== تقرير وقوف الفريق - $(date '+%Y-%m-%d') ==="
    echo ""
    
    # PR مدموجة أمس
    echo "## طلبات السحب المدموجة"
    gh pr list \
        --repo "${repo}" \
        --state merged \
        --json "number,title,author,mergedAt,url" \
        --jq '.[] | select(.mergedAt >= "'$(date -d yesterday '+%Y-%m-%d')'") | 
              "- PR #\(.number): \(.title) (@\(.author.login))"'
    
    echo ""
    echo "## المشكلات المفتوحة هذا الأسبوع"
    gh issue list \
        --repo "${repo}" \
        --state open \
        --json "number,title,assignees,labels,createdAt" \
        --jq '.[] | select(.createdAt >= "'$(date -d '7 days ago' '+%Y-%m-%d')'") |
              "- #\(.number): \(.title) - المعيّن: \(.assignees[0].login // "غير معيّن")"'
    
    echo ""
    echo "## طلبات السحب المعلقة للمراجعة"
    gh pr list \
        --repo "${repo}" \
        --state open \
        --json "number,title,author,reviewRequests,createdAt" \
        --jq '.[] | select(.reviewRequests | length > 0) |
              "- PR #\(.number): \(.title) - المؤلف: @\(.author.login)"'
}

# الإرسال إلى Slack
send_standup_to_slack() {
    local report="${1}"
    local webhook_url="${2}"
    
    local payload
    payload=$(cat << EOF
{
    "text": "تقرير وقوف الفريق اليومي",
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "تقرير وقوف الفريق - $(date '+%Y/%m/%d')"
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "${report}"
            }
        }
    ]
}
EOF
)
    
    curl -s -X POST \
        -H 'Content-type: application/json' \
        --data "${payload}" \
        "${webhook_url}"
}

أتمتة تعيين مراجعات الكود

نظام التعيين الذكي

#!/bin/bash
# code-review-assigner.sh - نظام تعيين مراجعات الكود الذكي

# تعيين المراجعين بناءً على المعرفة بالكود
assign_code_reviewers() {
    local pr_number="${1}"
    local repo="${2}"
    
    # الحصول على الملفات المعدّلة
    local changed_files
    changed_files=$(gh pr view "${pr_number}" \
        --repo "${repo}" \
        --json "files" \
        --jq '.files[].path')
    
    # تحديد خبراء الكود
    declare -A expertise=(
        ["backend/"]="ahmed,sara"
        ["frontend/"]="ali,fatima"
        ["database/"]="omar,leila"
        ["infrastructure/"]="khaled,nour"
        ["security/"]="hana,ibrahim"
    )
    
    local reviewers=()
    
    while IFS= read -r file; do
        for area in "${!expertise[@]}"; do
            if [[ "${file}" == ${area}* ]]; then
                # إضافة خبراء هذه المنطقة
                IFS=',' read -ra area_experts <<< "${expertise[${area}]}"
                reviewers+=("${area_experts[@]}")
            fi
        done
    done <<< "${changed_files}"
    
    # إزالة المكررات
    local unique_reviewers
    unique_reviewers=$(echo "${reviewers[@]}" | tr ' ' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
    
    # الحصول على مؤلف PR لاستبعاده
    local pr_author
    pr_author=$(gh pr view "${pr_number}" \
        --repo "${repo}" \
        --json "author" \
        --jq '.author.login')
    
    # استبعاد المؤلف من المراجعين
    local filtered_reviewers
    filtered_reviewers=$(echo "${unique_reviewers}" | tr ',' '\n' | 
        grep -v "^${pr_author}$" | 
        head -3 |
        tr '\n' ',' | sed 's/,$//')
    
    if [[ -n "${filtered_reviewers}" ]]; then
        # طلب المراجعة
        gh pr edit "${pr_number}" \
            --repo "${repo}" \
            --add-reviewer "${filtered_reviewers}"
        
        echo "تم طلب المراجعة من: ${filtered_reviewers}"
    else
        echo "لم يُعثر على مراجعين مناسبين"
    fi
}

إعداد تقارير المقاييس

لوحة تحكم المشروع

#!/bin/bash
# project-metrics.sh - إعداد تقارير مقاييس المشروع

generate_project_metrics() {
    local repo="${1}"
    local period_days="${2:-30}"
    
    local since_date
    since_date=$(date -d "${period_days} days ago" '+%Y-%m-%dT00:00:00Z')
    
    echo "=== مقاييس المشروع - آخر ${period_days} يومًا ==="
    echo ""
    
    # مقاييس PR
    echo "## إحصائيات طلبات السحب"
    
    local total_prs
    total_prs=$(gh pr list \
        --repo "${repo}" \
        --state all \
        --json "number" \
        --jq 'length')
    
    local merged_prs
    merged_prs=$(gh pr list \
        --repo "${repo}" \
        --state merged \
        --json "number,mergedAt" \
        --jq "[.[] | select(.mergedAt >= \"${since_date}\")] | length")
    
    local open_prs
    open_prs=$(gh pr list \
        --repo "${repo}" \
        --state open \
        --json "number" \
        --jq 'length')
    
    echo "- إجمالي PR: ${total_prs}"
    echo "- PR مدموجة (${period_days} يوم): ${merged_prs}"
    echo "- PR مفتوحة: ${open_prs}"
    
    echo ""
    echo "## إحصائيات المشكلات"
    
    local total_issues
    total_issues=$(gh issue list \
        --repo "${repo}" \
        --state all \
        --json "number" \
        --jq 'length')
    
    local closed_issues
    closed_issues=$(gh issue list \
        --repo "${repo}" \
        --state closed \
        --json "number,closedAt" \
        --jq "[.[] | select(.closedAt >= \"${since_date}\")] | length")
    
    local open_issues
    open_issues=$(gh issue list \
        --repo "${repo}" \
        --state open \
        --json "number" \
        --jq 'length')
    
    echo "- إجمالي المشكلات: ${total_issues}"
    echo "- مشكلات مغلقة (${period_days} يوم): ${closed_issues}"
    echo "- مشكلات مفتوحة: ${open_issues}"
    
    echo ""
    echo "## أهم المساهمين"
    
    gh pr list \
        --repo "${repo}" \
        --state merged \
        --json "author,mergedAt" \
        --jq "[.[] | select(.mergedAt >= \"${since_date}\")] | 
              group_by(.author.login) | 
              map({user: .[0].author.login, count: length}) | 
              sort_by(-.count) | 
              .[:5] | 
              .[] | 
              \"- @\(.user): \(.count) PR\""
}

# تصدير التقرير بتنسيق Markdown
export_metrics_report() {
    local repo="${1}"
    local output_file="${2:-metrics-report.md}"
    
    {
        echo "# تقرير مقاييس المشروع"
        echo ""
        echo "**تاريخ التقرير**: $(date '+%Y-%m-%d')"
        echo "**المستودع**: ${repo}"
        echo ""
        generate_project_metrics "${repo}"
    } > "${output_file}"
    
    echo "تم تصدير التقرير: ${output_file}"
}

إدارة طرق العرض في المشاريع

إنشاء طرق عرض مخصصة

#!/bin/bash
# project-views.sh - إدارة طرق عرض المشاريع

# إنشاء طريقة عرض Sprint
create_sprint_view() {
    local project_number="${1}"
    local sprint_name="${2}"
    local owner="${3}"
    
    # إنشاء طريقة عرض Sprint
    gh api graphql -f query='
    mutation CreateView($projectId: ID!, $name: String!) {
        addProjectV2View(input: {
            projectId: $projectId
            name: $name
            layout: BOARD_LAYOUT
        }) {
            projectView {
                id
                name
            }
        }
    }' \
    -f projectId="${project_number}" \
    -f name="${sprint_name}"
    
    echo "تم إنشاء طريقة عرض: ${sprint_name}"
}

# تحديث تقرير Sprint تلقائيًا
update_sprint_report() {
    local repo="${1}"
    local project_number="${2}"
    local owner="${3}"
    
    # الحصول على إحصائيات Sprint الحالي
    local sprint_stats
    sprint_stats=$(gh project item-list "${project_number}" \
        --owner "${owner}" \
        --format json \
        --jq '{
            total: length,
            completed: [.items[] | select(.status == "Done")] | length,
            in_progress: [.items[] | select(.status == "In Progress")] | length,
            todo: [.items[] | select(.status == "Todo")] | length
        }')
    
    local total=$(echo "${sprint_stats}" | jq -r '.total')
    local completed=$(echo "${sprint_stats}" | jq -r '.completed')
    local in_progress=$(echo "${sprint_stats}" | jq -r '.in_progress')
    local todo=$(echo "${sprint_stats}" | jq -r '.todo')
    
    # حساب نسبة الاكتمال
    local completion_rate=0
    if [[ ${total} -gt 0 ]]; then
        completion_rate=$(( completed * 100 / total ))
    fi
    
    echo "=== حالة Sprint ==="
    echo "الإجمالي: ${total}"
    echo "مكتملة: ${completed} (${completion_rate}%)"
    echo "قيد التنفيذ: ${in_progress}"
    echo "في الانتظار: ${todo}"
    
    # إنشاء تقرير Markdown
    cat << EOF
## تحديث تقدم Sprint

| الحالة | العدد | النسبة |
|--------|-------|--------|
| مكتملة | ${completed} | ${completion_rate}% |
| قيد التنفيذ | ${in_progress} | - |
| في الانتظار | ${todo} | - |
| **الإجمالي** | **${total}** | - |

> **ملاحظة**: تم إنشاء هذا التقرير تلقائيًا في $(date '+%Y-%m-%d %H:%M')
EOF
}

التقارير التلقائية

جدولة التقارير الدورية

#!/bin/bash
# scheduled-reports.sh - جدولة التقارير التلقائية

# تكوين الجدولة (يستخدم مع cron)
# 0 9 * * 1 /path/to/scheduled-reports.sh weekly
# 0 9 * * * /path/to/scheduled-reports.sh daily

generate_scheduled_report() {
    local report_type="${1:-daily}"
    local repo="${2}"
    local slack_webhook="${3}"
    
    local report_content
    
    case "${report_type}" in
        "daily")
            report_content=$(generate_standup_report "${repo}" "" "yesterday")
            ;;
        "weekly")
            report_content=$(generate_project_metrics "${repo}" "7")
            ;;
        "monthly")
            report_content=$(generate_project_metrics "${repo}" "30")
            ;;
    esac
    
    # إرسال إلى Slack
    if [[ -n "${slack_webhook}" ]]; then
        send_standup_to_slack "${report_content}" "${slack_webhook}"
        echo "تم إرسال التقرير إلى Slack"
    else
        echo "${report_content}"
    fi
    
    # حفظ في ملف
    local report_file="${HOME}/.gh-reports/$(date '+%Y%m%d')-${report_type}.md"
    mkdir -p "$(dirname "${report_file}")"
    echo "${report_content}" > "${report_file}"
    echo "تم حفظ التقرير: ${report_file}"
}

# الوظيفة الرئيسية
main() {
    local command="${1:-daily}"
    local repo="${2:-}"
    local webhook="${3:-}"
    
    if [[ -z "${repo}" ]]; then
        repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || "")
    fi
    
    if [[ -z "${repo}" ]]; then
        echo "خطأ: يجب تحديد المستودع"
        exit 1
    fi
    
    generate_scheduled_report "${command}" "${repo}" "${webhook}"
}

main "$@"

الخلاصة

في هذا الجزء الثالث، تعلمنا:

  • إدارة GitHub Projects v2: إنشاء المشاريع والحقول المخصصة وإدارة العناصر بالكامل برمجيًا
  • أتمتة ربط المشكلات: التصنيف التلقائي للمشكلات وإضافتها إلى المشاريع المناسبة
  • تقارير الفريق: أتمتة تقارير الوقوف اليومي والإرسال إلى Slack
  • تعيين المراجعين: نظام ذكي لتعيين مراجعي الكود بناءً على الخبرة
  • لوحات المقاييس: إنشاء تقارير شاملة حول أداء المشروع

المقالات السابقة في السلسلة:

  • الجزء 1: أساسيات GitHub CLI
  • الجزء 2: أتمتة PR وسير العمل
  • الجزء 3: إدارة المشاريع (هذه المقالة)
  • الجزء 4: أتمتة Wiki والوثائق
  • الجزء 5: سير العمل المتقدمة وCI/CD