はじめに
Vercel と GitHub をつないでいると、main に push した瞬間に本番デプロイが始まります。CI の結果は待ってくれません。 つまり、テストが赤でも本番は更新されます。
この記事では、Vercel の Git 連携デプロイを止めて、GitHub Actions の全チェックが通ったときだけ Vercel CLI で本番に出す構成を紹介します。vercel.json と ci.yml をコピペすれば動く形にしてあり、実際に踏んだハマりどころもまとめています。
対象読者: Vercel + GitHub Actions で個人開発・小規模開発をしている人。特に、無料のプライベートリポジトリでブランチ保護が使えない人。
TL;DR
- Vercel の Git 連携は CI と並行して走るので、CI が赤でも本番が更新される
- 普通はブランチ保護(必須チェック)で防ぐが、GitHub Free のプライベートリポジトリでは使えない
- そこで
vercel.jsonのgit.deploymentEnabled: falseで Git 連携デプロイを止め、Actions のdeployジョブ(needsで全チェック通過が条件)からvercel build→vercel deploy --prebuilt --prodで出す - ハマりどころ①:
deploymentEnabledをブランチ個別指定にすると、指定していないブランチはデプロイされる - ハマりどころ②③: ビルドが Actions に移るので、Sensitive な環境変数が届かない問題とUTC 問題が表に出る
なぜこの構成にしたのか
問題:テストが赤でも本番に出てしまう
Vercel の Git 連携は、main に push されると CI の結果とは無関係に本番ビルドを始めます。GitHub Actions と Vercel が並行して走るので、こうなります。
普通の解決策が使えない
素直な解決策は、GitHub のブランチ保護で「CI が通らないと main にマージできない」ようにすることです。ただし、GitHub の公式ドキュメントによると、GitHub Free(個人アカウント)のプライベートリポジトリは機能が制限されていて、保護ブランチは GitHub Pro 以上で使える機能として挙げられています。
発想を変えて、デプロイそのものを Actions に握らせる
- Vercel の Git 連携デプロイは止める
- Actions の
deployジョブが、全チェックが通ったときだけ Vercel CLI で本番に出す
こうすると「本番に出ている=全テストを通ったコード」が構造で保証されます。E2E が落ちても本番は前回の正常版のまま残るので、main を revert して直せば済みます。
全体像
Vercel 側は git.deploymentEnabled: false で、Git 連携のデプロイを全ブランチ止めておきます。
設定1:vercel.json で Git 連携デプロイを止める
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": false
}
}
これで止まるのは Git の push をきっかけにした自動デプロイだけです。トークンを使って CLI から明示的に行うデプロイ(vercel deploy --prod)は止まりません。
ハマりどころ①:ブランチ個別指定だと「指定外のブランチ」はデプロイされる
deploymentEnabled はブランチごとに指定する書き方もできます。
{
"git": {
"deploymentEnabled": {
"main": false
}
}
}
main だけ止めればいいと思ってこう書いたのですが、公式ドキュメントにもあるとおり、指定していないブランチはデフォルトの true(=デプロイする)扱いです。
その結果、PR のブランチ(Dependabot のブランチなど)では Vercel のプレビュービルドが走り続け、次のことが起きました。
- Actions と同じビルドを Vercel でも重複して実行していた
- プレビュー環境に必要な環境変数を登録していなかったので、プレビュービルドが毎回失敗し、**PR に偽の赤(失敗マーク)**が付いていた
対策: 上の例のように "deploymentEnabled": false で全ブランチを止めます。PR ごとのプレビューが欲しくなったら、後述の「拡張」を参照してください。
設定2:deploy ジョブで全チェックの通過を条件にする
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
lint-build:
runs-on: ubuntu-latest
env:
# ビルドに必要なダミー値(実際には接続しない)
NEXT_PUBLIC_API_URL: https://example.com
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck # tsc --noEmit(テストファイルも型チェックする)
- run: npm test
- run: npm run build
e2e:
runs-on: ubuntu-latest
# フォークや Dependabot の PR では Secrets が渡らないのでスキップ。
# push では常に実行する=デプロイ前のゲートは維持される。
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }}
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
E2E_EMAIL: ${{ secrets.E2E_EMAIL }}
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
# 全部通った main への push のときだけ本番デプロイ
deploy:
runs-on: ubuntu-latest
needs: [lint-build, e2e]
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# ビルド時に必要な値は Actions Secrets から渡す(ハマりどころ②)
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npx --yes vercel@latest pull --yes --environment=production
- run: npx --yes vercel@latest build --prod
- run: npx --yes vercel@latest deploy --prebuilt --prod
ポイント
| 設定 | 役割 |
|---|---|
needs: [lint-build, e2e] |
前のジョブがすべて成功したときだけ走る。どれかが失敗すれば deploy はスキップ=本番に出ない |
if: github.event_name == 'push' && github.ref == 'refs/heads/main' |
main への push のときだけに限定(PR ではデプロイしない) |
vercel pull --environment=production |
Vercel のプロジェクト設定と本番の環境変数を取得 |
vercel build --prod |
Actions のランナー上で本番ビルド |
vercel deploy --prebuilt --prod |
ビルド済みの成果物をアップロードして本番に出す |
--prebuilt なので Vercel 側でもう一度ビルドしません。「テストしたコード」と「本番に出るもの」のずれを小さくできます。この3コマンドの流れは Vercel 公式のガイドでも紹介されている形です。
vercel@latest は毎回最新版を取ります。再現性を重視するなら vercel@<バージョン> で固定しましょう(そのぶん更新の手間は増えます)。
必要な Secrets
| 名前 | 取得方法 |
|---|---|
VERCEL_TOKEN |
Vercel のアカウント設定でトークンを発行(スコープは対象のチームに絞る) |
VERCEL_ORG_ID |
ローカルで vercel link すると作られる .vercel/project.json の orgId
|
VERCEL_PROJECT_ID |
同じく projectId
|
登録は GitHub の Settings → Secrets and variables → Actions からブラウザで行います。トークンの文字列をシェルのコマンドラインに打ち込まない(履歴に残さない)ためです。
ビルドが Actions に移ることで起きること
この構成で一番の変化は、ビルドを実行するのが Vercel ではなく Actions のランナーになることです。ここから2つのハマりどころが出てきます。
ハマりどころ②:Sensitive な環境変数がビルドに届かない
Vercel の環境変数には Sensitive(機密) という設定があり、公式ドキュメントによると、作成後は値を読み出せない形で保存されます。
私の環境では、Sensitive にしていた変数が vercel pull で取得されず、Actions 上のビルドに値が届きませんでした。具体的には次のことが起きました。
- エラー監視サービスの DSN が Sensitive だったため、値の代わりにプレースホルダの文字列がビルドに埋め込まれ、本番で「DSN が不正」というエラーが出ていた
- ソースマップをアップロードするための認証トークンも、Vercel の環境変数に入れても届かなかった
対策: ビルド時に必要な値は GitHub Actions Secrets に登録して deploy ジョブの env で渡します。
- Next.js の
NEXT_PUBLIC_*のように、ビルド時にコードへ埋め込まれる値 - ビルド中に使うトークン(ソースマップのアップロードなど)
実行時(サーバー側で動くとき)にだけ使う値は、これまでどおり Vercel の環境変数に置いて大丈夫です。
ハマりどころ③:CI は UTC で動く
GitHub Actions のランナーは UTC で動きます。日付の境目をまたぐテストが、ローカル(日本時間)では通るのに CI では落ちます。
実際、日付を扱うテストが時間帯によって落ち、デプロイが連続で止まりました。「CI が赤なら本番に出ない」構成にしたからこそ、こういう不安定なテストがそのままデプロイを止めます。
対策: テストのタイムゾーンを固定します。Vitest ならこうです。
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
env: {
TZ: 'Asia/Tokyo',
},
},
})
ローカルで CI と同じ状況を再現するなら TZ=UTC npm test を実行します。
Git 連携を止めて失うもの・残るもの
| 項目 | どうなるか |
|---|---|
| コミット SHA などの Git 情報 | Git 連携が付けていた VERCEL_GIT_COMMIT_SHA などが入らなくなる。バージョン表示に使っていたら VERCEL_GIT_COMMIT_SHA || GITHUB_SHA のように Actions 側の値にフォールバックする |
| PR ごとのプレビュー URL | 自動では作られなくなる |
| ダッシュボードからの Redeploy・ロールバック |
使える(デプロイ自体は Vercel にある)。緊急時はローカルから vercel deploy --prod でも出せる |
| Actions の実行時間 | 増える(ビルドが Actions 側に移るため) |
拡張:PR のプレビューが欲しい場合
PR 用のジョブで vercel deploy(--prod なし)を実行すれば、プレビューを出せます。その場合は、Vercel のプレビュー環境にも必要な環境変数を登録しておきましょう(ハマりどころ①の二の舞になります)。
ついでにやるとよいこと
型チェックをビルドと別に回す
next build の型チェックだけに頼っていたら、テストファイルの中の型エラーが CI を素通りしていました。tsc --noEmit を別のステップで回すと確実です。
{
"scripts": {
"typecheck": "tsc --noEmit"
}
}
CI のステップはパイプを通さない
npm test | tail -n 20 のようにパイプを通すと、終了コードがパイプの最後のコマンド(tail)のものになり、テストが失敗しても緑になることがあります。この構成では CI の赤が唯一のゲートなので、ステップは素で実行しましょう。
Dependabot と組み合わせる
Dependabot の PR には Secrets が渡らないので、上の ci.yml では E2E をスキップしています。代わりにマージ後の push で E2E → deploy を通すので、ゲートは保たれます。Dependabot の自動マージまで回す話は別記事にまとめました。
導入手順
- ローカルで
vercel linkを実行し、.vercel/project.jsonからorgId/projectIdを控える - Vercel でトークンを発行する
- GitHub の Settings → Secrets and variables → Actions に
VERCEL_TOKEN/VERCEL_ORG_ID/VERCEL_PROJECT_IDと、ビルドに必要な値を登録する -
ci.ymlを置いて push し、deployジョブで本番に出ることを確認する -
最後に
vercel.jsonで Git 連携デプロイを止める - わざとテストを落としたコミットを push して、
deployがスキップされることを確かめる
順番に注意: 先に vercel.json で Git 連携を止めてしまうと、Actions のデプロイが動くようになるまで本番に出す手段がない状態になります。Actions 側のデプロイが動くのを確認してから止めましょう。
代替案
| 方法 | 向いているケース |
|---|---|
| GitHub のブランチ保護(必須チェック) | 有料プランか公開リポジトリなら、これが一番素直 |
| Vercel の Deployment Checks | Git 連携はそのままで、指定した GitHub Actions のチェックが通るまで本番ドメインへの割り当てを保留できる。ビルドは Vercel 側に残したい場合に |
| この記事の構成 | ビルドも CI に寄せて、「テストした成果物をそのまま本番に出す」ことを重視する場合に |
Deployment Checks は、本番デプロイ自体は作られるものの、チェックが通るまでカスタムドメインに割り当てない仕組みです(詳細は下記の公式ドキュメント参照)。今回はビルドを Actions に一本化したかったので CLI デプロイ方式を選びましたが、Vercel の設定だけで済ませたいならこちらも有力です。
まとめ
- Vercel の Git 連携は CI を待たないので、テストが赤でも本番が更新される
-
git.deploymentEnabled: falseで Git 連携デプロイを止め、Actions のdeployジョブ(needsで全チェック通過が条件)からvercel build→vercel deploy --prebuilt --prod -
deploymentEnabledのブランチ個別指定は指定外のブランチがデプロイされるので、全体をfalseにする - ビルドが Actions に移るので、ビルド時の値は Actions Secrets から渡す。テストのタイムゾーンも固定する
- 切り替えは「Actions のデプロイを確認 → Git 連携を止める」の順番で
関連記事: