Skip to main content
← Înapoi la blog
VercelGit HooksCI/CDDeploymentpnpm

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.

Publicat 6 octombrie 20267 min de citit
Actualizare la Deploy pe Vercel din CI când cel care face commit nu e membru al echipei. Acel articol face deploy din GitHub Actions și e alegerea mai bună când poți adăuga secrete de repo. Acesta e pentru când nu poți — și nu necesită deloc rescrierea commit-urilor.

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

  1. 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.
  2. 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”.
  3. 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.

.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

Patru 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-remote arată SHA-ul tău pe remote.
  • Fă deploy la commit-ul împins, nu la directorul tău de lucru. Un git worktree temporar 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.
  • main e 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/hooks

Hook-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)DaNu
Face deploy la push-urile tuturorDaDoar de pe mașinile cu hook
Rescrie autorul în runnerDa (amend, niciodată împins)Nu
ConfigurareO dată per repoO dată per dezvoltator
Push-uri din interfața web GitHub, boți, alte mașiniCu deployFără deploy
Unde apar eșecurileLogul ActionsUn 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-push face 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-remote că 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.