AIエージェント用のスキル定義を1行直してpushした。数十秒後、Vercelのダッシュボードに「Building」の文字が出る。出力に関係ないコミットのはずが、依存のインストールから next build まで律儀に一周していく。
AIエージェントと開発していると、設定や作業メモ、ドキュメントのように、サイトの出力に入らないコミットが目に見えて増える。画面が1ピクセルも変わらないビルドに、毎回付き合う必要はあるだろうか。
この記事は、そうした変更でVercelのビルドを省きたい人向けのガイドだ。結論から言うと、vercel.json の ignoreCommand で前回デプロイした commit からの差分を見て、ビルドに関係ないパスだけならビルドを止める。判定に迷ったらビルドする側に倒す、が設計の芯になる。
前提は、VercelのGit連携でデプロイしていること。CMSの更新をデプロイフックの再ビルドで反映しているプロジェクトは対象外だ(理由は後述)。Hobbyプランは無料(1日100デプロイ、同時ビルド1本まで)。Proは月$20のプラットフォーム料金に$20分の利用クレジットが付く。既定の設定(Elasticビルドマシン)では、ビルド時間がそこから従量で引かれる(料金ページ)。
なぜ「前回デプロイとの差分」で判定するのか
ignoreCommand に書いたコマンドは、デプロイがビルドに入るときに、プロジェクトのRoot Directoryで実行される。Vercelが見るのは終了コードだけだ(公式ドキュメント、Ignored Build Stepのガイド)。
- exit 0: ビルドを中止し、デプロイはCanceledになる
- exit 1以上: いつもどおりビルドする
シェルでは0が成功なのに、ここでは0が「ビルドしない」を意味する。読み違えると判定が丸ごと逆になり、ドキュメントの修正だけがビルドされて、アプリの変更が黙って飛ぶ。
差分の起点には VERCEL_GIT_PREVIOUS_SHA を使う。公式の定義は「そのプロジェクトとブランチで最後に成功したデプロイのsha」で、ブランチの初回デプロイでは空になる。Ignored Build Stepを設定したときだけ渡される変数でもある(システム環境変数)。
Canceledは成功したデプロイではないので、定義どおりなら起点は「最後に実際にビルドしたcommit」に留まる。そうであれば、ドキュメントだけのcommitが続く間、その差分は次の判定に積み上がる。
公式の例 git diff --quiet HEAD^ HEAD ./ は、直前の1 commitしか見ない。この HEAD^ を起点にしたまま除外パスを足すと、困ったことが起きる。アプリを変えたcommitの上にドキュメントだけのcommitを重ねてpushしたとき、最後の1 commitだけを見てビルドを止めてしまう。アプリの変更は反映されない。
もう1つ、同じガイドによれば、Vercelはリポジトリを --depth=10 の浅いcloneで取ってくる。前回のshaがcloneに入っていなければ、git diff は失敗する。
終了コードの向き、前回のshaが空になる場合、浅いclone、の3つを踏まえると、判定は次の流れになる。
止まる出口は1つだけで、残りはすべてビルドに流れる。判定が壊れて黙ってデプロイが止まるより、余分に1回ビルドするほうが安くつく。
手順1: 除外してよいパスを洗い出す
基準は「ビルドがそのファイルを読むかどうか」の1つだけ。読まないものだけを外し、迷ったら外さない。
| パスの例 | 除外 | 理由 |
|---|---|---|
docs/、README.md などの .md | する | ビルドが読まない。原稿を .md で書いているなら外さない |
.github/ | する | CIの定義で、Vercelのビルドには入らない |
__tests__/ などのテスト | する | 出力は変わらない。型チェックで壊れていないかはCI側で見る |
| AIエージェントの設定ディレクトリ | する | 出力に入らない |
vercel.json | しない | 判定コマンドもビルド設定もここにある |
package.json、lockfile | しない | 依存が変わる |
一番危ないのは *.md の一括除外だ。git のパス指定では * がディレクトリの区切りもまたぐので、サブディレクトリの .md もまとめて外れる。先に、リポジトリ内の .md を一覧で見ておく。
git ls-files '*.md'
ビルド時に読み込む原稿が混ざっていたら、*.md ではなくディレクトリ単位で外す。
手順2: vercel.jsonに判定を書く
プロジェクトのルートの vercel.json に次を書く。除外するパスは手順1の結果に合わせて差し替える。
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"ignoreCommand": "[ -n \"$VERCEL_GIT_PREVIOUS_SHA\" ] && git diff --quiet \"$VERCEL_GIT_PREVIOUS_SHA\" HEAD -- . ':(exclude).claude' ':(exclude).github' ':(exclude)docs' ':(exclude)__tests__' ':(exclude)*.md'"
}
- 前回のshaが空なら、先頭の
[ -n ... ]でexit 1になる。 空のshaでもgit diffが失敗してビルドに倒れるが、偶然に頼らず明示しておく。HEAD^で代用すると、初回デプロイで最後の1 commitしか見なくなる git diff --quietの終了コードがそのまま判定になる。 差分が無ければ0、あれば1、shaが見つからないなどのエラーは128。0のときだけ止まる- 除外は長い書き方
:(exclude)で揃える。 短い':!__tests__'は、_がマジック記号と読まれてfatal: Unimplemented pathspec magic '_'で失敗する。gitの定義では、英数字・glob・正規表現の特殊文字・コロン以外の記号はすべてマジック記号だ(git help glossary)。失敗はexit 128でビルドに倒れるので、エラーなのに見た目は設定する前と変わらず、気づきにくい
この設定は、ダッシュボードのIgnored Build Stepより優先される(リファレンス)。
手順3: 手元で判定を試す
pushの前に、同じ文字列を手元のシェルで動かす。vercel.json から取り出して実行すれば、書き写しの誤りも拾える。
cmd=$(node -p 'require("./vercel.json").ignoreCommand')
VERCEL_GIT_PREVIOUS_SHA=$(git rev-parse HEAD~1) sh -c "$cmd"; echo "exit=$?"
VERCEL_GIT_PREVIOUS_SHA= sh -c "$cmd"; echo "exit=$?"
2行目は直前のcommitを前回デプロイに見立てている。最新のcommitがドキュメントだけなら exit=0 になる。3行目は初回デプロイの再現で、必ず exit=1 になる。
検証用のリポジトリで、sh・bash・zsh・dashから上の vercel.json の文字列を実行した結果が次の表だ。同じ状態で2回続けて実行しても結果は変わらなかった。
| 状況 | 終了コード | Vercelの動き |
|---|---|---|
| 前回のshaが空(初回デプロイ) | 1 | ビルドする |
| 前回のshaがcloneに無い(存在しないsha、浅いcloneの外) | 128 | ビルドする |
| 除外パスだけの変更 | 0 | ビルドしない |
| アプリの変更を含む | 1 | ビルドする |
| アプリのcommitの上にドキュメントのcommitを重ねてpush | 1 | ビルドする(同じ除外で HEAD^ と比べる書き方では0で止まる) |
vercel.json だけの変更 | 1 | ビルドする |
手順4: デプロイ後に効いているか確かめる
- ドキュメントだけのcommitをpushし、デプロイ一覧でそのデプロイがCanceledになるのを見る
- アプリの変更をpushし、いつもどおりビルドされるのを見る
- 新しいブランチを切ってpushし、初回デプロイがビルドされるのを見る
省けるのはビルド時間で、デプロイの数ではない。止めたデプロイも、1日のデプロイ数の上限(Hobbyは100、Proは6000)に数えられる。同時ビルドの枠も同じだ(前掲の公式ドキュメントの注記、上限一覧)。
止まらないとき、止まりすぎるとき
CLIからのデプロイでの扱いは確かめていない。
| 状況 | 起きること | 対処 |
|---|---|---|
| ブランチの初回デプロイ | 前回のshaが空なので必ずビルドする | 仕様どおり。初回の1回分は払う |
| 前回ビルドから10 commit以上空いた | shaが浅いcloneに無く、128でビルドする | 仕様どおり。止めすぎるよりよい |
| 環境変数を変えて同じcommitを再デプロイ | 前回のshaとHEADが同じなので、判定を通すと止まる見込み(手元で前回のshaをHEADにすると0) | 再デプロイ画面で「Use project's Ignore Build Step」のチェックを外す |
| CMSの更新などで、デプロイフックから同じcommitを再ビルドする | commitが変わらず差分が無いので止まる(Vercelのメンテナーも、コンテンツだけが変わったビルドは止まると回答している) | デプロイフックで再ビルドしているプロジェクトには入れない。Next.jsならISRなど、再ビルドせずに内容を更新する形に寄せる |
| monorepoでRoot Directoryをサブディレクトリにしている | . がそのディレクトリだけを指し、共有パッケージの変更を見落とす | ':(top)packages/ui' のように依存先を足す |
原稿やドキュメントサイトを .md で書いている | *.md の除外で、原稿の更新がビルドされない | ディレクトリ単位で外す |
ほかの止め方との比較
| 方法 | 向いている場面 | 弱いところ |
|---|---|---|
| ダッシュボードの「Only build if there are changes in a folder」 | アプリが1つのフォルダに収まっている | 設定がリポジトリに残らず、レビューできない |
HEAD^ を起点に除外を足す書き方(公式の例の延長) | 1回のpushが常に1 commit | まとめてpushすると途中の変更を見落とす |
| CIのbuildも止める(paths-ignoreなど。Vercelは止まらない) | CIのbuildも一緒に省きたい | Vercelのビルドは別に走るので、ignoreCommand と併用し、両方に除外を書く |
まとめ
| 決めること | 選び方 |
|---|---|
| 差分の起点 | HEAD^ ではなく VERCEL_GIT_PREVIOUS_SHA |
| 判定に迷ったとき | shaが空、判定の失敗は、どちらもビルドに倒す |
| 除外するパス | ビルドが読まないものだけ。:(exclude) で書く |
| 確かめ方 | vercel.json の文字列を手元の sh -c で実行する |
同じ考え方は、Vercel以外のビルドの省略にもそのまま使える。止める条件を狭く書き、それ以外は全部動かす形にしておけば、判定が壊れても失うのはビルド1回分で済む。
AIエージェントを使った開発の進め方やCIの設計を外から整えたい場合は、AI開発支援で相談を受け付けている。



