Skip to main content
← العودة إلى المدونة
VercelGit HooksCI/CDDeploymentpnpm

النشر إلى Vercel من git pre-push hook: مستودع منظمة، خطة Hobby، بلا أسرار CI

يرفض تكامل Git في Vercel مستودعًا خاصًا تملكه منظمة على GitHub ضمن خطة Hobby، وطريق الـ CI يحتاج أسرارًا في المستودع قد لا يُسمح لك بإضافتها. إليك الخيار الأخف: pre-push hook محلي ينشر الـ commit المدفوع نفسه عبر واجهة Vercel السطرية، بينما يواصل الجميع الدفع كلٌّ باسمه.

نُشر 6 أكتوبر 20267 دقيقة قراءة
تحديث لمقال النشر إلى Vercel من الـ CI حين لا يكون صاحب الـ commit عضوًا في الفريق. ذلك المقال ينشر من GitHub Actions وهو الخيار الأفضل حين تستطيع إضافة أسرار إلى المستودع. أما هذا المقال فلحين لا تستطيع — ولا يحتاج إلى إعادة كتابة أي commit إطلاقًا.

انتقل المستودع إلى منظمة على GitHub، ومشروع Vercel يقع في فريق Hobby، وكل من في المشروع يدفع باسمه الخاص على GitHub. أردت أن تُنشر كل دفعة — main إلى الإنتاج وأي فرع آخر إلى معاينة — دون تغيير من تُنسب إليه الـ commit‑ات.

كانت خطتي الأولى هي تكامل Git الأصلي. فشل ثلاث مرات متتالية، وفي كل مرة لسبب مختلف:

Error: Failed to link your-org/your-repo. You need to add a Login Connection
to your GitHub account first. (400)

This action must be performed by an organization owner

Error: The repository "your-repo" is private and owned by an organization,
which is not supported on the Hobby plan. Upgrade to Pro to continue. (409)

ثلاثة جدران بينك وبين تكامل Git

  1. لا يوجد ربط لتسجيل الدخول عبر GitHub. حساب Vercel الذي يملك الفريق لم يربط GitHub قط، لذا يفشل ربط المستودع بالخطأ الأول أعلاه. الحل في Account Settings → Authentication — لكن فكّر قبل أن تنقر: لا يمكن ربط حساب GitHub إلا بحساب Vercel واحد في الوقت نفسه. وربط حسابك بحساب الخدمة ينقله بعيدًا عن حساب Vercel الشخصي لديك، فيحجب Vercel بعدها عمليات نشر مشاريعك الشخصية (“Git author must have access to the project”). الـ hook أدناه لا يحتاج إلى أي ربط مع GitHub إطلاقًا، فلا تفعل هذا إلا إن كنت تسعى إلى تكامل Git.
  2. تثبيت التطبيق يحتاج إلى مالك المنظمة. يجب تثبيت Vercel GitHub App على المنظمة. العضو العادي لا يستطيع إلا أن يطلبه، وتجيبه صفحة الإعدادات بـ “This action must be performed by an organization owner”.
  3. خطة Hobby لا تستطيع ربط مستودعات المنظمات الخاصة. وبعد إنجاز كل ما سبق، لا يزال Vercel يرد بـ 409. لا إعداد يصلح هذا: الخطة نفسها هي الحدّ.

الجداران الأولان مسألة صلاحيات يمكنك السعي وراءها. أما الثالث فيعني أن تكامل Git خارج الحسبان حتى ينتقل الفريق إلى Pro. وكل ما بعد هذا حلّ التفافي — فاختر حلًّا صريحًا في كونه التفافًا.

انشر من الجهاز الذي يدفع

تستوثق واجهة Vercel السطرية بتسجيل دخول أو بتوكن، لا بصاحب الـ commit، لذا لا تجري أبدًا فحص العضوية الذي يحجب التكامل. مقالي السابق يشغّل الـ CLI في GitHub Actions. وهذا يحتاج VERCEL_TOKEN في أسرار المستودع، أي صلاحيات مدير على المستودع — إضافةً إلى --amend للصاحب داخل المشغّل (runner) كي يُظهر اللوح commit.

يتجاوز pre-push hook الأمرين معًا. يعمل على جهاز المطوّر بعد git push، وينشر الـ commit الذي دُفع للتو، ولا يمسّ الـ commit أبدًا: يبقى صاحبه الحقيقي على GitHub وفي اللوح. والثمن أنه يوجد على كل جهاز على حدة — المزيد عن هذا أدناه.

الـ hook

احفظه باسم .git/hooks/pre-push واجعله قابلًا للتنفيذ. يستدعيه Git باسم الـ remote وعنوانه، ويمرّر إليه عبر stdin سطرًا لكل ref مدفوع.

.git/hooks/pre-push
#!/bin/bash
# Deploy to Vercel after a successful push. Local only: git never pushes .git/hooks.
# main -> production, any other branch -> preview. Runs detached; log: .git/vercel-deploy.log

case "$2" in
  *your-org/your-repo*) ;;
  *) exit 0 ;;
esac

repo_root="$(git rev-parse --show-toplevel)"
gh_path="${2#*github.com[:/]}"; gh_path="${gh_path%.git}"
gh_org="${gh_path%%/*}"; gh_repo="${gh_path#*/}"
log="$repo_root/.git/vercel-deploy.log"
push_pid=$PPID

[ -d "$repo_root/.vercel" ] || { echo "vercel-deploy: run 'vercel link' first, skipping deploy" >&2; exit 0; }

while read -r _ local_sha remote_ref _; do
  [ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
  branch="${remote_ref#refs/heads/}"
  if [ "$branch" = "main" ]; then flag="--prod"; else flag=""; fi

  (
    # wait for the push itself to finish, then deploy exactly the pushed commit
    while kill -0 "$push_pid" 2>/dev/null; do sleep 1; done
    # a rejected or failed push must not deploy: only continue if the commit reached the remote
    landed="$(git ls-remote "$2" "$remote_ref" 2>/dev/null | cut -f1)"
    if [ "$landed" != "$local_sha" ]; then
      echo "=== $(date '+%F %T') $branch @ ${local_sha:0:7} push did not land, deploy skipped" >> "$log"
      exit 0
    fi
    tmp="$(mktemp -d)"
    cd "$repo_root" || exit 1
    git worktree add --detach "$tmp" "$local_sha" >/dev/null 2>&1 || exit 1
    cp -R "$repo_root/.vercel" "$tmp/.vercel"
    cd "$tmp" || exit 1
    {
      echo "=== $(date '+%F %T') $branch @ ${local_sha:0:7} ${flag:-preview}"
      vercel deploy $flag --yes \
        --build-env ENABLE_EXPERIMENTAL_COREPACK=1 \
        -m "githubDeployment=1" \
        -m "githubOrg=$gh_org" -m "githubRepo=$gh_repo" \
        -m "githubCommitOrg=$gh_org" -m "githubCommitRepo=$gh_repo" \
        -m "githubCommitMessage=$(git -C "$repo_root" log -1 --format=%s "$local_sha")" \
        -m "githubCommitSha=$local_sha" \
        -m "githubCommitRef=$branch" \
        -m "githubCommitAuthorName=$(git -C "$repo_root" log -1 --format=%an "$local_sha")" \
        -m "githubCommitAuthorEmail=$(git -C "$repo_root" log -1 --format=%ae "$local_sha")" \
        2>&1 | tail -5
    } >> "$log"
    cd "$repo_root" && git worktree remove --force "$tmp"
  ) >/dev/null 2>&1 &
  disown
done

exit 0

أربع تفاصيل تحمل العبء كله:

  • طابِق عنوان الـ remote لا اسمه. يمرّر Git العنوان بصفته $2. ومع أي دفع إلى remote آخر يخرج الـ hook فورًا، فلا يُنشر mirror أو fork أبدًا.
  • انتظر الدفع ثم تحقّق من وصوله. يعمل الـ hook قبل انتهاء الدفع، والدفع المرفوض يشغّله أيضًا. لذا ينفصل إلى الخلفية، وينتظر خروج عملية git push، ولا ينشر إلا إذا أظهر git ls-remote الـ SHA الخاص بك على الـ remote.
  • انشر الـ commit المدفوع لا شجرة عملك. يضمن git worktree مؤقت عند الـ SHA المدفوع ألا تُنشر أبدًا تعديلات لم تدخل في commit ولا ملفات نصف مكتملة. ويعيد استخدام ربط .vercel لديك.
  • main هو الإنتاج، وكل ما عداه معاينة. راية --prod واحدة، تُحدَّد بحسب الـ ref المدفوع.

اجعل اللوح يُظهر الـ commit لا هاشًا

ظهرت أولى عمليات النشر من الـ hook في اللوح بسلسلة عشوائية مثل AowEm2XzF بدلًا من رسالة الـ commit. وقراءة عمليات النشر عبر الـ API أظهرت السبب: فالنشرة التي عرضت رسالة كانت تحمل في بياناتها الوصفية githubOrg وgithubRepo وgithubDeployment. لا يضيفها الـ CLI إلا حين يتعرّف على GitHub remote من checkout عادي — أما من worktree منفصل فلم أحصل إلا على مفاتيح gitCommit* ولا شيء غيرها.

الحل أن تمرّرها بنفسك عبر -m، مشتقةً من عنوان الـ remote، كما يفعل الـ hook أعلاه. عندها تأتي الرسالة والفرع والصاحب من تلك المفاتيح. هذا ما لاحظته في مشروعي أنا؛ لا يوثّق Vercel هذه المفاتيح، فتحقّق من لوحك بعد أول نشر وعاملها على أنها قد تتغيّر.

المصادقة: استخدم توكنًا ولا تشارك تسجيل دخول

يستدعي الـ hook الأمر vercel deploy، فيحتاج الجهاز إلى بيانات دخول الحساب الذي يملك الفريق. يقرأ الـ CLI قيمة VERCEL_TOKEN من البيئة من تلقاء نفسه:

# A token created for this purpose; the CLI reads it automatically
export VERCEL_TOKEN="..."

فضّل التوكن على vercel login. يمكن إبطال التوكن بمفرده؛ أما تسجيل الدخول المشترك فلا يمكن سحبه من شخص واحد دون تغييره للجميع. والتوكنات أيضًا هي ما يوثّقه Vercel للأتمتة.

مشاركته مع الفريق

أضف إلى المستودع السكربت ومثبّتًا يعمل بأمر واحد، كي لا ينسخ أحد الملفات يدويًا:

pnpm add -g vercel
vercel link --project my-project --scope <team-slug>
bash scripts/vercel-deploy/install.sh   # copies the hook into .git/hooks

يوجد الـ hook نفسه في .git/hooks، وهو مجلد لا يدفعه git أبدًا — ومن هنا جاء المثبّت. وثمة سطر واحد عليك تغييره قبل إعادة استخدام السكربت: نمط عنوان المستودع في case في الأعلى.

مقارنته بطريق الـ CI

هذه ليست نسخة أفضل من كل النواحي من المقال السابق. إنها تضحّي بالشمول مقابل البساطة:

GitHub Actions (المقال السابق)pre-push hook (هذا المقال)
يحتاج صلاحيات مدير على المستودع (الأسرار)نعملا
ينشر دفعات الجميعنعمفقط من الأجهزة التي فيها الـ hook
يعيد كتابة الصاحب في المشغّلنعم (amend لا يُدفع أبدًا)لا
الإعدادمرة لكل مستودعمرة لكل مطوّر
الدفعات من واجهة GitHub على الويب، والبوتات، والأجهزة الأخرىتُنشرلا تُنشر
أين تظهر الأعطالسجل Actionsملف سجل محلي

أين يقع الحدّ

الـ hook حلّ التفافي لا رخصة. قبل أن تتبنّاه، كن صريحًا مع نفسك بشأن ثلاثة أمور:

  • خطة Hobby للاستخدام غير التجاري. إن كان هذا منتج شركة، فالخطة هي المشكلة الحقيقية والـ hook يخفيها فقط. الحل هو Pro؛ والـ hook يكسبك وقتًا حتى يعتمد أحدهم الفاتورة.
  • لا تشارك تسجيل دخول لتفادي دفع ثمن المقاعد. حساب واحد يحمل توكن النشر للفريق أمر مقبول. أما أن يسجّل عدّة أشخاص الدخول كأنهم الشخص نفسه للوصول إلى اللوح فهو مشاركة بيانات الدخول.
  • لا ينشر إلا ما تدفعه من جهازك. لا شيء يمنع زميلًا بلا الـ hook من الدفع وترك الإنتاج على نسخة قديمة — فحدّد من المسؤول عن عمليات النشر.

خلاصات

  • تكامل Git في Vercel خارج الحسبان لمستودع منظمة خاص على Hobby: لا صلاحية تصلح حدّ الخطة.
  • يستوثق الـ CLI بتوكن لا بصاحب الـ commit، فلا ينطبق عليه فحص العضوية.
  • pre-push hook ينشر من جهاز المطوّر، ولا يحتاج إلى أسرار في المستودع، ولا يعيد كتابة أي commit.
  • انتظر الدفع وتحقّق من وصوله عبر git ls-remote قبل النشر.
  • انشر worktree للـ SHA المدفوع، ومرّر بيانات github* الوصفية بنفسك كي يُظهر اللوح الـ commit.
  • استخدم توكنًا لا تسجيل دخول مشتركًا — وعامل Hobby كحلّ مؤقت إن كان المشروع تجاريًا.
  • يرتبط تسجيل الدخول عبر GitHub بحساب Vercel واحد في الوقت نفسه — فلا تنقل حسابك إلى حساب الخدمة؛ الـ hook لا يحتاج إليه.

يظل طريق الـ CI هو الخيار الافتراضي الأفضل حين تستطيع إضافة الأسرار. والـ hook لليوم الذي لا تستطيع فيه: بضع عشرات من أسطر الـ shell، تُعدّ مرة لكل مطوّر، ويظل كل commit يحمل صاحبه الحقيقي.

لاحظت خطأً؟

معلومة خاطئة، ترجمة ركيكة، أو شيء يبدو غير صحيح في هذه المقالة؟ راسلني — بلغتك أنت.