أتمتة تنظيف 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"