デプロイが自動化できた、で終わっていないか

SSG(Hugo、Astroなど)で作った静的サイトのビルド・デプロイを、macOSのlaunchdcronで毎日自動実行させること自体は、それほど難しくありません。設定ファイルを1つ書いて、スケジュールを組めば動きます。

難しいのは、その先です。「自動化できた」と「安全に自動化できている」は別の話です。

この記事では、個人サイト(Hugo製)の日次自動デプロイをlaunchdで実際に組んだ実例をもとに、2つのポイントを解説します。1つは、launchd特有のPATH問題を本番発火させずに事前検証するenv -iの使い方。もう1つは、デプロイの公開確認を「200 OKが返ってくること」だけで済ませず、「未公開の記事が404を返すこと」まで確認するネガティブテストという視点です。

launchdはログインシェルを経由しない

launchdは、現行macOSにおいてPID 1として動作するジョブ管理基盤です。かつてのinitを置き換えた存在であり、ログインシェルの起動プロセスとは別の経路でジョブを起動します。そのため、.zshrc.zprofileのようなログインシェルの設定ファイルは一切読み込まれません。

launchd.plistの公式仕様では、環境変数を扱うEnvironmentVariablesキーが次のように定義されています。

“This optional key is used to specify additional environmental variables to be set before running the job.”1

ジョブ実行前に 追加で 設定する環境変数、という説明であり、ログインシェルのdotfileへの言及はどこにもありません。つまり、手動のターミナル操作では通っていたPATHが、launchd経由の自動実行では素の状態(あるいはそれに近い状態)まで削られます。コミュニティの実測報告では、launchdデーモンのデフォルトPATH/usr/bin:/bin:/usr/sbin:/sbin程度で、/usr/local/binすら含まれないと報告されています2。手動実行では動くコマンドが、自動実行時にだけcommand not foundで落ちる典型的な原因です。

対処はシンプルで、plistのEnvironmentVariablesPATHを明示します。今回の実例では、次のように指定しました。

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>

もう1つ、launchd特有の癖として押さえておきたいのがStartCalendarIntervalによるスケジュール実行の挙動です。公式ドキュメントには次のように書かれています。

“If you schedule a launchd job by setting the StartCalendarInterval key and the computer is asleep when the job should have run, your job will run when the computer wakes up.”3

Macがスリープ中に予定時刻を過ぎても、起床後に1回だけ遅延実行されます(電源そのものが切れていた場合は、次回の指定時刻まで実行されません)。「その時刻に起きている」必要はなく、「その時刻以降に一度は起動する」で足りるということです。

ただし、これを過信するのは禁物です。Apple Developer Technical SupportのエンジニアはDeveloper Forumsで、次のように明言しています。

“launchd doesn’t guarantee anything about the power state of the system when it starts your job based on StartCalendarInterval.”4

StartCalendarIntervalはスリープからの復帰を助けてくれますが、電源状態そのものについてlaunchdが何かを保証しているわけではありません。「起床後に必ず実行される」と読み替えるのは言い過ぎで、あくまで実装上の振る舞いとして理解しておくのが安全です。

本番発火で確かめない ── env -iによるローカル事前検証

PATH問題への対処が必要だとわかっても、それを 本番のlaunchd発火で確かめる のはリスクが高いやり方です。デプロイジョブである以上、本番発火はpushを伴います。設定ミスに気づかないまま発火させてしまえば、その時点で事故です。

そこで使えるのがenv -iです。BSD版env-iオプションは、次のように定義されています。

“Execute the utility with only those environment variables specified by name=value options. The environment inherited by env is ignored completely.”5

指定した環境変数だけでコマンドを実行し、それ以外の継承環境は完全に無視する、という動作です。これを使えば、「launchdが渡してくる、ほぼ何も持たないPATH状態」を、pushを伴わずに手元で安全に再現できます。実際に使ったコマンドは次の通りです。

env -i PATH=/opt/homebrew/bin:/usr/bin:/bin HOME=$HOME /opt/homebrew/bin/mise run build

2026年8月9日時点の記録では、このコマンドで3,006ページのビルドが27秒で成功しました。同じコマンドを2026年8月11日に再実行しても、pushを伴わずに成功することを確認しています(このときは3,025ページ・約28〜30秒でした。サイトの記事数は日々増えるため、ページ数や所要時間はそのときどきで変わる値です。数値を引用する際は、必ず計測した日付とセットで扱ってください)。

なお、このコマンドで指定しているPATHmise自身と、hugogitbashが置かれているディレクトリだけです。ビルドタスクの実行時にmiseが必要なツール(今回のケースではperl)を自動的に用意してくれるため、plistのPATHにそれらを個別に足す必要はありませんでした。

「200 OK」だけでは防げない ── ネガティブテストという視点

env -iでPATH問題を潰せたら、次はデプロイ後の公開確認です。ここでよくあるのが、「公開記事にアクセスして200 OKが返ってくることを確認して終わり」というやり方です。しかし、これだけでは防げない事故があります。

Hugoには-F--buildFuture)というフラグがあります。

“-F, —buildFuture include content with publishdate in the future”6

公式ドキュメントには、このフラグを付けない場合のデフォルト挙動として、公開日が未来のコンテンツをビルドしない旨が明記されています7。逆に言えば、もしこのフラグの扱いを誤って有効化してしまえば、 未公開の記事まで全部ビルド・公開されてしまう ということです。「公開済み記事が200を返すか」だけを見ていては、この種の設定ミスには気づけません。

そこで、実際の運用では公開確認をもう一歩広げ、次の3本のURLを確認しました。

対象期待結果(08-09時点)結果(08-11再検算)
公開済み記事(当日公開分)200200200
公開済み記事(前日公開分)200200200
未公開の未来日付記事404404404

「公開されるべきものが公開されている」ことに加えて、「公開されるべきでないものが公開されていない」ことまで確認する。これがネガティブテストという発想です。単純な成功/失敗の二値判定だけでは、こうした「全部が一律で成功してしまっている」型の事故は見抜けない、という指摘は一般的な信頼性設計の議論でもよく語られます。

ただし、ここは正直に書いておく必要があります。この200/404チェックは、デプロイスクリプト自体に組み込まれた自動検証ではありません。実際にデプロイスクリプトの中身を確認したところ、200/404を検証するようなコードは存在せず、記録として残っていたのも、発火後に人間が手動でcurlを打って確認した結果でした。

curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/2026/08/08/070705/"

「自動化されたネガティブテスト」ではなく、「デプロイ後の手動チェックリスト」というのが、この実例の正確な位置づけです。過大に言わないことも、検収基準を疑う姿勢の一部だと考えています。

自分のSSGスタックへの持ち帰りチェックリスト

launchdやHugoに限らず、cronやGitHub Actions、他のSSGを使っている場合でも、応用できる考え方は同じです。

  • ジョブ実行環境(launchd/cron/CI)のPATHを明示しているか
  • 本番発火の前に、env -iのような手段でその環境を安全にローカル再現できているか
  • デプロイ後の確認は、「公開されるべきものが公開されているか」だけでなく「公開されるべきでないものが公開されていないか」まで見ているか
  • その確認は自動化されているか、それとも運用者の記憶に依存した手動チェックのままか

最後の1点については、今回の実例も「まだ手動」の段階です。次にやるべきことは、この2本のcurlチェックをデプロイスクリプトに組み込んで自動化することでしょう。「自動化できた」で満足せず、検収の基準そのものを疑い続けることが、安全なデプロイ自動化への近道だと考えています。

ご自身のSSG運用の自動化やデプロイ検証設計についてお困りの際は、コンタクトフォームよりお気軽にご相談ください。

Footnotes

  1. launchd.plist(5) Mac OS X Manual Page

  2. Where is my PATH, launchd? - Medium

  3. Scheduled Jobs - Apple Developer (Archive)

  4. launchd job power state guarantee - Apple Developer Forums Thread 815034

  5. env - macOS command reference (ss64)

  6. hugo build command reference

  7. Usage - Hugo Documentation