Deploy pe Vercel dintr-un hook git pre-push: repo de organizație, plan Hobby, fără secrete CI
Integrarea Git a Vercel refuză un repo privat deținut de o organizație GitHub pe planul Hobby, iar varianta CI cere secrete de repo pe care poate nu ai voie să le adaugi. Iată varianta mai ușoară: un hook pre-push local care face deploy exact la commit-ul împins, cu CLI-ul Vercel, în timp ce toți continuă să dea push sub propriul nume.
Repo-ul s-a mutat într-o organizație GitHub, proiectul Vercel stă într-o echipă Hobby, iar toți cei din proiect dau push sub propriul nume GitHub. Voiam ca fiecare push să facă deploy — main în producție, orice altă ramură într-un preview — fără să schimb cui aparțin commit-urile.
Primul meu plan a fost integrarea Git nativă. A eșuat de trei ori la rând, de fiecare dată dintr-un motiv diferit:
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)Trei ziduri între tine și integrarea Git
- Nicio conexiune de login cu GitHub. Contul Vercel care deține echipa nu conectase niciodată GitHub, așa că legarea repo-ului eșuează cu prima eroare de mai sus. Soluția e Account Settings → Authentication — dar gândește-te înainte să dai click: un cont GitHub poate fi conectat la un singur cont Vercel la un moment dat. Dacă-l conectezi pe al tău la contul de serviciu, îl muți de pe Vercel-ul tău personal, iar Vercel blochează apoi deploy-urile proiectelor tale personale („Git author must have access to the project”). Hook-ul de mai jos nu are nevoie de nicio conexiune GitHub, așa că fă asta doar dacă mergi pe integrarea Git.
- Instalarea aplicației cere un owner al organizației. Vercel GitHub App trebuie instalată în organizație. Un membru obișnuit poate doar s-o ceară, iar pagina de setări răspunde „This action must be performed by an organization owner”.
- Hobby nu poate conecta repo-uri private de organizație. Cu toate cele de mai sus făcute, Vercel tot răspunde 409. Nicio setare nu rezolvă asta: planul e limita.
Primele două sunt permisiuni după care poți umbla. A treia înseamnă că integrarea Git e exclusă până când echipa trece pe Pro. Tot ce urmează e un workaround — așa că alege unul care recunoaște sincer că e un workaround.
Fă deploy de pe mașina care dă push
CLI-ul Vercel se autentifică cu un login sau un token, nu cu autorul commit-ului, așa că nu rulează niciodată verificarea de apartenență care blochează integrarea. Articolul meu anterior rulează CLI-ul în GitHub Actions. Asta cere VERCEL_TOKEN în secretele repo-ului, adică drepturi de admin pe repo — plus un --amend al autorului în runner, ca dashboardul să arate un commit.
Un hook pre-push le ocolește pe amândouă. Rulează pe mașina dezvoltatorului după git push, face deploy la commit-ul abia împins și nu atinge niciodată commit-ul: autorul real rămâne pe GitHub și în dashboard. Prețul e că există per mașină — mai multe despre asta mai jos.
Hook-ul
Salvează-l ca .git/hooks/pre-push și fă-l executabil. Git îl apelează cu numele și URL-ul remote-ului și îi dă pe stdin câte o linie pentru fiecare ref împins.
#!/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 0Patru detalii duc tot greul:
- Filtrează după URL-ul remote-ului, nu după nume. Git transmite URL-ul ca
$2. La un push către orice alt remote, hook-ul iese imediat, deci un mirror sau un fork nu face niciodată deploy. - Așteaptă push-ul, apoi verifică dacă a ajuns. Hook-ul rulează înainte ca push-ul să se termine, iar un push respins îl declanșează la fel. Așa că se detașează, așteaptă să se încheie procesul
git pushși face deploy doar dacăgit ls-remotearată SHA-ul tău pe remote. - Fă deploy la commit-ul împins, nu la directorul tău de lucru. Un
git worktreetemporar la SHA-ul împins înseamnă că modificările necomise și fișierele neterminate nu ajung niciodată în deploy. Refolosește link-ul tău.vercel. maine producție, restul sunt preview-uri. Un singur flag--prod, ales după ref-ul împins.
Fă dashboardul să arate commit-ul, nu un hash
Primele mele deploy-uri prin hook apăreau în dashboard ca un șir aleatoriu de tipul AowEm2XzF în loc de mesajul commit-ului. Citind deploy-urile prin API, am văzut de ce: cel care afișa un mesaj avea în metadate githubOrg, githubRepo și githubDeployment. CLI-ul le adaugă doar când recunoaște un remote GitHub dintr-un checkout obișnuit — dintr-un worktree detașat am primit chei gitCommit* și nimic altceva.
Soluția e să le transmiți tu cu -m, derivate din URL-ul remote-ului, cum face hook-ul de mai sus. Mesajul, ramura și autorul vin apoi din aceste chei. Asta am observat pe propriul proiect; Vercel nu documentează aceste chei, așa că verifică-ți dashboardul după primul deploy și tratează-le ca pe ceva ce se poate schimba.
Autentificare: folosește un token, nu partaja un login
Hook-ul apelează vercel deploy, deci mașina are nevoie de acreditive pentru contul care deține echipa. CLI-ul citește singur VERCEL_TOKEN din variabilele de mediu:
# A token created for this purpose; the CLI reads it automatically
export VERCEL_TOKEN="..."Preferă un token în locul lui vercel login. Un token poate fi revocat separat; un login partajat nu poate fi retras de la o persoană fără a-l schimba pentru toți. Tokenurile sunt, de altfel, și ce documentează Vercel pentru automatizare.
Cum îl distribui echipei
Fă commit în repo la script și la un installer de o singură comandă, ca nimeni să nu copieze fișiere de mână:
pnpm add -g vercel
vercel link --project my-project --scope <team-slug>
bash scripts/vercel-deploy/install.sh # copies the hook into .git/hooksHook-ul în sine stă în .git/hooks, pe care git nu-l împinge niciodată — de aceea există installerul. O singură linie de schimbat înainte să refolosești scriptul: șablonul de URL al repo-ului din case-ul de la început.
Cum se compară cu varianta CI
Nu e o versiune strict mai bună a articolului anterior. Sacrifică acoperirea în favoarea simplității:
| GitHub Actions (articolul anterior) | hook pre-push (acest articol) | |
|---|---|---|
| Cere admin pe repo (secrete) | Da | Nu |
| Face deploy la push-urile tuturor | Da | Doar de pe mașinile cu hook |
| Rescrie autorul în runner | Da (amend, niciodată împins) | Nu |
| Configurare | O dată per repo | O dată per dezvoltator |
| Push-uri din interfața web GitHub, boți, alte mașini | Cu deploy | Fără deploy |
| Unde apar eșecurile | Logul Actions | Un fișier de log local |
Unde e linia
Un hook e un workaround, nu un permis. Înainte să-l adopți, fii sincer cu tine în privința a trei lucruri:
- Hobby e pentru uz necomercial. Dacă e produsul unei companii, planul e adevărata problemă, iar hook-ul doar o ascunde. Soluția e Pro; hook-ul câștigă timp până când cineva aprobă factura.
- Nu partaja un login ca să eviți plata locurilor. Un singur cont care ține tokenul de deploy al echipei e în regulă. Mai mulți oameni care se loghează ca aceeași persoană ca să capete acces la dashboard înseamnă partajare de acreditive.
- Face deploy doar la ce împingi de pe mașina ta. Nimic nu-l oprește pe un coleg fără hook să dea push și să lase producția în urmă — stabilește cine răspunde de deploy-uri.
Concluzii
- Integrarea Git a Vercel e exclusă pentru un repo privat de organizație pe Hobby: nicio permisiune nu rezolvă o limită de plan.
- CLI-ul autentifică un token, nu autorul commit-ului, așa că verificarea de apartenență nu i se aplică.
- Un hook
pre-pushface deploy de pe mașina dezvoltatorului, nu cere secrete de repo și nu rescrie niciodată un commit. - Așteaptă push-ul și verifică cu
git ls-remotecă a ajuns, înainte de deploy. - Fă deploy la un worktree al SHA-ului împins și transmite tu metadatele
github*, ca dashboardul să arate commit-ul. - Folosește un token, nu un login partajat — și tratează Hobby ca pe o soluție de avarie dacă proiectul e comercial.
- Un login GitHub se conectează la un singur cont Vercel la un moment dat — nu-l muta pe al tău pe contul de serviciu; hook-ul nu are nevoie de el.
Varianta CI rămâne alegerea implicită mai bună când poți adăuga secrete. Hook-ul e pentru ziua în care nu poți: câteva zeci de linii de shell, configurate o dată per dezvoltator, iar fiecare commit își păstrează autorul real.
Ai găsit o greșeală?
Un fapt greșit, o traducere stângace, ceva ce sună fals în acest articol? Scrie-mi — în limba ta.