Skip to main content
← Zurück zum Blog
VercelGit HooksCI/CDDeploymentpnpm

Auf Vercel aus einem git-pre-push-Hook deployen: Org-Repo, Hobby-Plan, keine CI-Secrets

Vercels Git-Integration lehnt ein privates Repo einer GitHub-Organisation im Hobby-Plan ab, und der CI-Weg braucht Repo-Secrets, die du vielleicht nicht anlegen darfst. Hier die leichtere Variante: ein lokaler pre-push-Hook, der genau den gepushten Commit mit der Vercel-CLI deployt, während alle weiter unter ihrem eigenen Namen pushen.

Veröffentlicht 6. Oktober 20267 Min. Lesezeit
Ergänzung zu Von der CI aus auf Vercel deployen, wenn der Committer kein Teammitglied ist. Dort wird aus GitHub Actions deployt, und das ist die bessere Wahl, wenn du Repo-Secrets anlegen kannst. Dieser Beitrag hier ist für den Fall, dass du es nicht kannst — und er kommt ganz ohne Umschreiben von Commits aus.

Das Repo zog in eine GitHub-Organisation um, das Vercel-Projekt liegt in einem Hobby-Team, und alle im Projekt pushen unter ihrem eigenen GitHub-Namen. Ich wollte, dass jeder Push deployt — main in die Produktion, jeder andere Branch als Preview —, ohne zu ändern, wem die Commits gehören.

Mein erster Plan war die native Git-Integration. Sie scheiterte dreimal hintereinander, jedes Mal aus einem anderen Grund:

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)

Drei Mauern zwischen dir und der Git-Integration

  1. Kein GitHub-Login verbunden. Das Vercel-Konto, dem das Team gehört, hatte GitHub nie verbunden, deshalb scheitert das Verknüpfen des Repos mit dem ersten Fehler oben. Die Lösung ist Account Settings → Authentication — aber überleg, bevor du klickst: Ein GitHub-Konto kann immer nur mit einem Vercel-Konto gleichzeitig verbunden sein. Verbindest du deins mit dem Service-Konto, wandert es von deinem persönlichen Vercel weg, und Vercel blockiert dann Deploys deiner persönlichen Projekte („Git author must have access to the project“). Der Hook unten braucht gar keine GitHub-Verbindung, also tu das nur, wenn du wirklich die Git-Integration willst.
  2. Für die Installation der App braucht es einen Organisations-Owner. Die Vercel-GitHub-App muss in der Organisation installiert werden. Ein normales Mitglied kann sie nur beantragen, und die Einstellungsseite antwortet mit „This action must be performed by an organization owner“.
  3. Hobby kann keine privaten Organisations-Repos verbinden. Auch wenn alles oben erledigt ist, antwortet Vercel weiter mit 409. Keine Einstellung behebt das: Der Plan ist die Grenze.

Die ersten beiden sind Berechtigungen, denen du hinterherlaufen kannst. Die dritte heißt: Die Git-Integration ist vom Tisch, bis das Team auf Pro ist. Alles Weitere ist ein Workaround — such dir also einen aus, der ehrlich damit umgeht, ein Workaround zu sein.

Von der Maschine deployen, die pusht

Die Vercel-CLI authentifiziert sich mit einem Login oder einem Token, nicht mit dem Commit-Autor, deshalb führt sie die Mitgliedschaftsprüfung nie aus, die die Integration blockiert. Mein vorheriger Beitrag lässt die CLI in GitHub Actions laufen. Das braucht VERCEL_TOKEN in den Secrets des Repos, also Admin-Rechte auf dem Repo — plus ein --amend des Autors im Runner, damit das Dashboard einen Commit zeigt.

Ein pre-push-Hook erspart beides. Er läuft auf der Maschine des Entwicklers nach git push, deployt den soeben gepushten Commit und fasst den Commit nie an: Der echte Autor bleibt auf GitHub und im Dashboard erhalten. Der Preis: Er existiert pro Maschine — dazu unten mehr.

Der Hook

Speichere ihn als .git/hooks/pre-push und mach ihn ausführbar. Git ruft ihn mit Remote-Name und URL auf und füttert ihn über stdin mit einer Zeile pro gepushter 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

Vier Details tragen das Ganze:

  • Die Remote-URL prüfen, nicht ihren Namen. Git übergibt die URL als $2. Ein Push an irgendein anderes Remote beendet den Hook sofort, sodass ein Mirror oder ein Fork nie deployt.
  • Auf den Push warten, dann prüfen, ob er angekommen ist. Der Hook läuft bevor der Push fertig ist, und auch ein abgelehnter Push startet ihn. Deshalb koppelt er sich ab, wartet, bis der git push-Prozess beendet ist, und deployt nur, wenn git ls-remote deinen SHA auf dem Remote zeigt.
  • Den gepushten Commit deployen, nicht dein Arbeitsverzeichnis. Ein Wegwerf-git worktree auf dem gepushten SHA sorgt dafür, dass nicht committete Änderungen und halbfertige Dateien nie ausgeliefert werden. Er nutzt deine bestehende .vercel-Verknüpfung weiter.
  • main ist Produktion, alles andere ein Preview. Ein einziges --prod-Flag, gewählt anhand der gepushten Ref.

Das Dashboard soll den Commit zeigen, nicht einen Hash

Die Deploys meines ersten Hooks erschienen im Dashboard als zufälliger String wie AowEm2XzF statt als Commit-Nachricht. Die Deployments über die API zurückzulesen zeigte, warum: Das Deployment, das eine Nachricht anzeigte, trug githubOrg, githubRepo und githubDeployment in seinen Metadaten. Die CLI ergänzt diese nur, wenn sie aus einem normalen Checkout ein GitHub-Remote erkennt — aus einem losgelösten Worktree bekam ich gitCommit*-Keys und sonst nichts.

Die Lösung: Gib sie selbst mit -m mit, abgeleitet aus der Remote-URL, wie es der Hook oben tut. Nachricht, Branch und Autor kommen dann aus diesen Keys. So habe ich es in meinem eigenen Projekt beobachtet; Vercel dokumentiert diese Keys nicht. Prüfe also nach dem ersten Deploy dein Dashboard und behandle sie als etwas, das sich ändern kann.

Authentifizierung: ein Token nutzen, keinen Login teilen

Der Hook ruft vercel deploy auf, also braucht die Maschine Zugangsdaten für das Konto, dem das Team gehört. Die CLI liest VERCEL_TOKEN von selbst aus der Umgebung:

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

Nimm lieber ein Token statt vercel login. Ein Token lässt sich einzeln widerrufen; einen geteilten Login kann man einer Person nicht wegnehmen, ohne ihn für alle zu ändern. Tokens sind außerdem das, was Vercel für Automatisierung dokumentiert.

Mit dem Team teilen

Committe das Skript und einen Ein-Befehl-Installer ins Repo, damit niemand Dateien von Hand kopiert:

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

Der Hook selbst liegt in .git/hooks, das git nie pusht — deshalb gibt es den Installer. Eine Zeile musst du ändern, bevor du das Skript wiederverwendest: das Muster der Repo-URL im case ganz oben.

Der Vergleich mit dem CI-Weg

Das ist keine durchweg bessere Version des vorherigen Beitrags. Der Hook tauscht Reichweite gegen Einfachheit:

GitHub Actions (vorheriger Beitrag)pre-push-Hook (dieser Beitrag)
Braucht Admin-Rechte auf dem Repo (Secrets)JaNein
Deployt die Pushes allerJaNur von Maschinen mit dem Hook
Schreibt den Autor im Runner umJa (Amend, nie gepusht)Nein
EinrichtungEinmal pro RepoEinmal pro Entwickler
Pushes aus der GitHub-Weboberfläche, von Bots, von anderen MaschinenWerden deploytWerden nicht deployt
Wo Fehler sichtbar werdenActions-LogEine lokale Logdatei

Wo die Grenze liegt

Ein Hook ist ein Workaround, kein Freibrief. Bevor du ihn einsetzt, sei in drei Punkten ehrlich zu dir selbst:

  • Hobby ist für nicht-kommerzielle Nutzung. Wenn das hier das Produkt einer Firma ist, ist der Plan das eigentliche Problem, und der Hook versteckt es nur. Die Lösung heißt Pro; der Hook verschafft Zeit, bis jemand die Rechnung freigibt.
  • Teile keinen Login, um Seats zu sparen. Ein Konto, das das Deploy-Token des Teams hält, ist in Ordnung. Wenn sich mehrere Leute als dieselbe Person einloggen, um Dashboard-Zugriff zu bekommen, ist das Zugangsdaten-Teilen.
  • Er deployt nur, was du von deiner Maschine pushst. Nichts hindert einen Kollegen ohne den Hook daran, zu pushen und die Produktion veraltet zurückzulassen — entscheide, wer für die Deploys verantwortlich ist.

Fazit

  • Vercels Git-Integration ist für ein privates Organisations-Repo im Hobby-Plan vom Tisch: Keine Berechtigung behebt ein Plan-Limit.
  • Die CLI authentifiziert ein Token, nicht den Commit-Autor, deshalb greift die Mitgliedschaftsprüfung bei ihr nicht.
  • Ein pre-push-Hook deployt von der Maschine des Entwicklers, braucht keine Repo-Secrets und schreibt nie einen Commit um.
  • Warte auf den Push und prüfe mit git ls-remote, dass er angekommen ist, bevor du deployst.
  • Deploye einen Worktree des gepushten SHA und übergib die github*-Metadaten selbst, damit das Dashboard den Commit zeigt.
  • Nutze ein Token, keinen geteilten Login — und behandle Hobby als Übergangslösung, wenn das Projekt kommerziell ist.
  • Ein GitHub-Login ist immer nur mit einem Vercel-Konto gleichzeitig verbunden — verschieb deinen nicht auf das Service-Konto; der Hook braucht ihn nicht.

Der CI-Weg bleibt der bessere Standard, wenn du Secrets anlegen kannst. Der Hook ist für den Tag, an dem du es nicht kannst: ein paar Dutzend Zeilen Shell, einmal pro Entwickler eingerichtet, und jeder Commit trägt weiterhin seinen echten Autor.

Einen Fehler entdeckt?

Ein falscher Fakt, eine schiefe Übersetzung, etwas, das in diesem Artikel falsch klingt? Schreib mir — in deiner eigenen Sprache.