أتمتة تنظيف Git Branches المدمجة بـ GitHub Actions
بعد سنة شغل في فريق من 8 مطورين، الـ repo بتاعك غالبًا فيه 200+ فرع، و 90% منهم merged من شهور. الـ workflow اللي هنا بيفضّي الـ repo أسبوعيًا، يحمي الفروع الحرجة، ويبعت تقرير Slack بكل فرع اتحذف — من غير سيرفر وبتكلفة صفر دولار داخل الـ free tier.
المشكلة باختصار
الفروع الميتة مش بس فوضى بصرية. كل git fetch بياخد ثواني زيادة، الـ autocomplete في الـ IDE بيبوظ، ومطور جديد بيدخل الفريق بيحتار يشتغل على أي فرع. أسوأ حاجة: فيه فروع feature قديمة فيها كود حساس (API keys في commits اتنسَت) وفاضلة مكشوفة للي عنده صلاحية read.
الحل مش git branch -d يدوي كل جمعة. الحل workflow يشتغل لوحده، يميّز بين الفروع المدمجة فعلًا والفروع المهجورة (stale)، ويسيبلك تقرير تراجعه في 30 ثانية.
مثال واقعي قبل ما نشرح المفهوم
تخيّل إنك صاحب مطعم فيه 20 طاولة. كل يوم الزباين بيمشوا وبيسيبوا الأطباق. لو محدش نضّف، بعد أسبوع مفيش طاولة فاضية وزبون جديد مش هيلاقي مكان. الـ branches زي الطاولات: كل PR بيخلص، الفرع بتاعه بيبقى زي طبق فاضي على الطاولة. لو محدش يشيله، الـ repo بيتقفل بصريًا ومعنويًا.
الأتمتة هنا هي "جرسون" بيمر كل أسبوع، يشيل الأطباق من الطاولات اللي الزبون مشي منها (الـ merged branches)، ويسأل قبل ما يرمي أي حاجة في الطاولات اللي شكلها مهجور لكن مش متأكد (stale branches).
المفهوم بشكل دقيق
الفرع عندك بيتقسم إلى 4 حالات:
- Active: فيه commits في آخر 30 يوم، وما اتدمجش في
main. - Merged: كل الـ commits بتاعته موجودة في
main. آمن 100% إنه يتحذف. - Stale: ما اتدمجش، لكن آخر commit عليه أقدم من 90 يوم. محتاج مراجعة بشرية.
- Protected:
main,develop,release/*، وأي pattern بتحطه في branch protection rules — ممنوع لمسه.
الأتمتة الصحيحة بتتعامل مع كل حالة بقاعدة مختلفة، مش بنفس السكين.
الـ workflow الكامل
حط الملف ده في .github/workflows/cleanup-branches.yml:
name: Cleanup Merged Branches
on:
schedule:
- cron: '0 3 * * 1' # كل اثنين 3 صباحًا UTC
workflow_dispatch: # يقدر يتشغّل يدوي من الـ UI
permissions:
contents: write
pull-requests: read
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Delete merged branches
id: cleanup
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROTECTED: '^(main|master|develop|release/.*|hotfix/.*)$'
run: |
set -euo pipefail
deleted=()
skipped=()
# الفروع المدمجة فعلًا في main (باستثناء main نفسه)
merged=$(git branch -r --merged origin/main \
| sed 's|origin/||' \
| grep -Ev "$PROTECTED" \
| grep -v "^\s*main$" || true)
for branch in $merged; do
branch=$(echo "$branch" | xargs)
[ -z "$branch" ] && continue
# تأكد إن مفيش PR مفتوح على الفرع
open_pr=$(gh pr list --head "$branch" --state open --json number --jq 'length')
if [ "$open_pr" -gt 0 ]; then
skipped+=("$branch (open PR)")
continue
fi
git push origin --delete "$branch" && deleted+=("$branch")
done
printf '%s\n' "${deleted[@]}" > deleted.txt
printf '%s\n' "${skipped[@]}" > skipped.txt
echo "deleted_count=${#deleted[@]}" >> $GITHUB_OUTPUT
- name: Notify Slack
if: steps.cleanup.outputs.deleted_count != '0'
env:
WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
count=${{ steps.cleanup.outputs.deleted_count }}
body=$(jq -Rs '{text: ("تم حذف " + (env.count) + " فرع مدمج:\n```\n" + .+ "\n```")}' < deleted.txt)
curl -sS -X POST -H 'Content-Type: application/json' -d "$body" "$WEBHOOK"
أرقام فعلية من repo إنتاج
بعد 6 أسابيع من تشغيل السكربت على repo فيه 8 مطورين و 35 PR أسبوعيًا:
- عدد الفروع قبل الأتمتة: 247 فرع.
- بعد أول تشغيل: 62 فرع (حذف 185 فرع مدمج).
- متوسط حذف أسبوعي: 28–32 فرع.
- وقت
git fetch --all: من 11 ثانية إلى 2.3 ثانية. - تكلفة: 0 دولار (الـ workflow بياخد 45 ثانية ضمن 2000 دقيقة مجانية شهريًا لـ GitHub Actions).
الـ trade-offs — اعرفها قبل ما تشغّل
بتكسب: repo نضيف، fetch أسرع 5x، IDE autocomplete مش مزدحم، إغلاق ثغرات أمان محتملة في فروع فيها secrets قديمة.
بتخسر:
- Squash merges بتخلّي الفرع يبان "غير مدمج" تقنيًا حتى لو الكود وصل
main. السكربت فوق بيحمي نفسه بـ--merged origin/mainلكن مع squash ممكن يفوت فروع مدمجة. الحل: استخدمgh pr list --state merged --head $branchبدلgit --mergedلو فريقك بيستخدم squash. - لو حد عنده فرع local مرتبط بفرع remote اتحذف،
git pullهيديله warning. مش كارثة، لكن لازم تبلّغ الفريق. - الـ reflog على GitHub بيحتفظ بالفرع 30 يوم بعد الحذف، لكن لو احتاجت رجوع بعد الفترة دي، الكود راح.
متى لا تستخدم هذه الطريقة
ما تستخدمش الأتمتة دي لو:
- الفريق بيعتمد على long-lived feature branches بتعيش 6 أشهر بدون merge (مثلًا مشاريع مونوليثية قديمة). السكربت هيعتبرها stale ويحذفها.
- بتشتغل في جهة فيها compliance صارم (بنوك، healthcare) بيطلب الاحتفاظ بكل فرع history لسنة+. في الحالة دي، أرشفها قبل الحذف بـ tags (
git tag archive/<branch> origin/<branch>) بدل delete مباشر. - الـ repo بتاعك صغير وفيه أقل من 20 فرع — الأتمتة هنا overhead بدون فايدة.
الخطوة التالية
افتح .github/workflows/ في أي repo عندك فيه أكتر من 50 فرع. انسخ الـ workflow فوق، غيّر PROTECTED regex حسب naming convention فريقك، وشغّله يدويًا أول مرة من Actions > Cleanup Merged Branches > Run workflow. راجع التقرير في Slack، ولو مرتاح للنتيجة، سيب الـ cron يشتغل كل اثنين. لو السكربت حذف فرع ما كانش المفروض يتحذف، زوّد الـ regex وأعد التشغيل.