Skip to main content
← Назад до блогу
VercelGit HooksCI/CDDeploymentpnpm

Деплой на Vercel із git pre-push хука: репо в організації, план Hobby, без CI-секретів

Git-інтеграція Vercel відмовляється підключати приватне репо GitHub-організації на плані Hobby, а CI-шлях вимагає секретів у репо, які тобі можуть не дозволити додати. Ось легший варіант: локальний pre-push хук, який деплоїть саме запушений коміт через Vercel CLI, а всі й далі пушать під власним іменем.

Опубліковано 6 жовтня 2026 р.7 хв читання
Оновлення до Деплой на Vercel із CI, коли автор коміту не член команди. Той пост деплоїть із GitHub Actions і є кращим вибором, якщо ти можеш додати секрети в репо. Цей — на випадок, коли не можеш, і тут не треба переписувати коміти взагалі.

Репо переїхало в GitHub-організацію, Vercel-проєкт сидить у Hobby-команді, а всі на проєкті пушать під власним GitHub-іменем. Я хотів, щоб кожен пуш деплоївся — main у продакшн, будь-яка інша гілка в прев’ю — і щоб при цьому не змінювалось, кому належать коміти.

Перший план — нативна 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 login connection. Vercel-акаунт, що володіє командою, ніколи не підключав GitHub, тож підключення репо падає з першою помилкою вище. Лікується в Account Settings → Authentication — але подумай, перш ніж клікати: GitHub-акаунт можна підключити лише до одного Vercel-акаунта водночас. Підключивши свій до сервісного, ти забираєш його зі свого особистого Vercel, і Vercel починає блокувати деплої твоїх особистих проєктів («Git author must have access to the project»). Хуку нижче GitHub-підключення не потрібне взагалі, тож роби це, лише якщо йдеш у Git-інтеграцію.
  2. Для встановлення застосунку потрібен власник організації. Vercel GitHub App треба поставити на організацію. Звичайний учасник може лише надіслати запит, а сторінка налаштувань відповідає «This action must be performed by an organization owner».
  3. Hobby не підключає приватні репо організацій. Навіть коли все вище зроблено, Vercel відповідає 409. Жодне налаштування це не лікує: межа — це сам план.

Перші дві — це права, які можна вибити. Третя означає, що Git-інтеграція відпадає, поки команда не на Pro. Усе далі — обхідний шлях, тож обирай такий, що чесно називає себе обхідним.

Деплоїти з машини, яка пушить

Vercel CLI авторизується логіном або токеном, а не автором коміту, тож перевірка членства, яка блокує інтеграцію, до нього не застосовується. Мій попередній пост запускає CLI в GitHub Actions. Це вимагає VERCEL_TOKEN у секретах репо, а отже адмін-прав на репо, плюс --amend автора в раннері, щоб дашборд показав коміт.

pre-push хук обходить обидва. Він запускається на машині розробника після git push, деплоїть щойно запушений коміт і не чіпає сам коміт: справжній автор лишається і на GitHub, і в дашборді. Ціна в тому, що хук існує окремо на кожній машині, про це нижче.

Хук

Збережи його як .git/hooks/pre-push і зроби виконуваним. Git викликає його з назвою й URL ремоута та подає на stdin по рядку на кожен запушений реф.

.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

Чотири деталі тримають усю конструкцію:

  • Порівнюй URL ремоута, а не його назву. Git передає URL як $2. Пуш в інший ремоут одразу виходить, тож дзеркало чи форк ніколи не деплояться.
  • Дочекайся пушу й перевір, що він долетів. Хук запускається до завершення пушу, і відхилений пуш його теж запускає. Тому він відв’язується, чекає завершення процесу git push і деплоїть, лише якщо git ls-remote показує твій SHA на ремоуті.
  • Деплой запушеного коміту, а не робочої директорії. Одноразовий git worktree на запушеному SHA означає, що незакомічені правки й недописані файли ніколи не потраплять у деплой. Він перевикористовує твій лінк .vercel.
  • main — продакшн, решта — прев’ю. Один прапорець --prod, який обирається з запушеного рефа.

Зробити так, щоб дашборд показував коміт, а не хеш

Мої перші деплої з хука виглядали в дашборді як випадковий рядок на кшталт AowEm2XzF замість повідомлення коміту. Прочитавши деплої назад через API, я побачив причину: той, що показував повідомлення, мав у метаданих githubOrg, githubRepo і githubDeployment. CLI додає їх лише коли розпізнає GitHub-ремоут зі звичайного чекауту, а з detached worktree я отримував тільки ключі gitCommit*.

Рішення — передати їх самому через -m, вивівши з URL ремоута, як це робить хук вище. Повідомлення, гілка й автор беруться з цих ключів. Це те, що я спостерігав на власному проєкті; Vercel ці ключі не документує, тож перевір дашборд після першого деплою й вважай, що вони можуть змінитись.

Авторизація: токен, а не спільний логін

Хук викликає 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

Сам хук живе в .git/hooks, який git ніколи не пушить, тому інсталятор і потрібен. Один рядок змінити перед повторним використанням: шаблон URL репо в case на початку.

Як це порівнюється з CI-шляхом

Це не суворо краща версія попереднього посту. Це обмін охоплення на простоту:

GitHub Actions (попередній пост)pre-push хук (цей пост)
Потрібен адмін на репо (секрети)ТакНі
Деплоїть пуші всіхТакЛише з машин із хуком
Переписує автора в раннеріТак (amend, ніколи не пушиться)Ні
НалаштуванняРаз на репоРаз на розробника
Пуші з вебінтерфейсу GitHub, боти, інші машиниДеплоятьсяНе деплояться
Де видно збоїЛог ActionsЛокальний лог-файл

Де межа

Хук — це обхідний шлях, а не ліцензія. Перш ніж його брати, будь чесним із собою щодо трьох речей:

  • Hobby — для некомерційного використання. Якщо це продукт компанії, справжня проблема — це план, а хук її лише ховає. Лікує Pro; хук виграє час, поки хтось не схвалить рахунок.
  • Не ділись логіном, щоб не платити за seat-и. Один акаунт із деплой-токеном команди — нормально. Кілька людей, що заходять як одна й та сама особа заради доступу до дашборду, — це credential sharing.
  • Деплоїться лише те, що ти пушиш зі своєї машини. Ніщо не заважає колезі без хука запушити й лишити продакшн застарілим — вирішіть, хто відповідає за деплої.

Головне

  • Git-інтеграція Vercel для приватного репо організації на Hobby відпадає: жодні права не лікують ліміт плану.
  • CLI авторизує токен, а не автора коміту, тож перевірка членства до нього не застосовується.
  • pre-push хук деплоїть з машини розробника, не потребує секретів у репо й ніколи не переписує коміт.
  • Дочекайся пушу й перевір, що він долетів, через git ls-remote перед деплоєм.
  • Деплой worktree запушеного SHA і передавай метадані github* самому, щоб дашборд показував коміт.
  • Токен, а не спільний логін, і вважай Hobby тимчасовим рішенням, якщо проєкт комерційний.
  • GitHub-логін підключається лише до одного Vercel-акаунта водночас — не переносьте свій на сервісний акаунт; хуку він не потрібен.

CI-шлях і далі кращий за замовчуванням, коли можеш додати секрети. Хук — на той день, коли не можеш: кілька десятків рядків shell, налаштованих раз на розробника, і кожен коміт досі несе справжнього автора.

Помітили помилку?

Невірний факт, кривий переклад, щось звучить неправдиво в цій статті? Напишіть мені — своєю мовою.