毎朝記事を自動生成する仕組みを作った。そして14日間、止まっていることに誰も気づかなかった
個人の自動化は「作れるか」ではなく「壊れたと気づけるか」で決まります。実行ログを数えたら55回中24回が失敗、うち2回は50日と14日の長期停止でした。実際の失敗ログをもとに、静かに壊れる自動処理の見つけ方を書きます。
結論:自動化の本当の難所は「作る」ではなく「壊れたと気づく」
毎朝9時に記事の下書きを1本自動生成する仕組みを、個人で運用しています。
実行ログを数えたところ、こうなっていました。
| 回数 | |
|---|---|
| 実行 | 55回 |
| 成功 | 31回 |
| 失敗 | 24回(44%) |
しかも失敗の内訳が問題でした。53日間の連続停止と、14日間の連続停止が含まれています。
そしてどちらも、止まっていることに長期間気づきませんでした。 毎朝きっちり9時に起動して、きっちり失敗して、静かに終了していたからです。
何を作ったのか
構成はシンプルです。
launchd(毎朝9:00)
└→ auto_post.sh
├→ topics.txt からテーマを1つ選ぶ(循環)
├→ 記事を生成
└→ ~/Documents/note下書き/ に保存
macOSの launchd で毎朝9時にシェルスクリプトを起動し、テーマを1つ選んで記事を生成し、Markdownで保存します。投稿は手動です。AIが書いたものをそのまま公開しない方針なので、生成までで止めています。
テーマは30本用意して、上から順に使い、一巡したら先頭に戻ります。
1回目の長期停止:53日間
原因は1行でした。
auto_post.sh: line 27: mapfile: command not found
mapfile はファイルを配列に読み込むbashの組み込みコマンドですが、macOS標準のbashは3.2で、mapfile は入っていません。 bash 4.0からの機能です。
つまり初日から一度も動いていませんでした。 それに53日間気づきませんでした。
修正は while read ループへの置き換えです。
TOPICS=()
while IFS= read -r line; do
TOPICS+=("$line")
done < <(grep -v '^[[:space:]]*#' "$TOPICS_FILE" | grep -v '^[[:space:]]*$')
macOS向けにシェルスクリプトを書くなら、bash 4以降の機能は使えないと思っておくべきでした。mapfile、連想配列(declare -A)、${var^^} あたりが該当します。
2回目の長期停止:14日間
今度はこれでした。
RuntimeError: claude CLI の認証が切れています。
詳細: Failed to authenticate: OAuth session expired and could not be refreshed
記事の生成にはAIのCLIを使っています。そのOAuthセッションが期限切れになり、更新にも失敗していました。
再ログインすれば直ります。問題は、切れたことを知る手段がなかったことです。
しかもこれは初めてではなく、過去にも同じ原因で止まったことがあります。 つまり定期的に起きる種類の障害で、対策なしでは必ず再発します。
なぜ気づけないのか
2つの停止に共通する構造があります。
エラーが誰の目にも触れない場所に出る。
launchdはバックグラウンドで動くので画面に何も出ない- エラーはログファイルに書かれるが、そのファイルを毎日見る人はいない
- 通知も飛ばない
- 成功しても失敗しても、体感は同じ「何も起きない」
出力が増えていくタイプの自動化は特に危険です。「今日もファイルが増えているはず」という思い込みが働くので、フォルダを開く動機がなくなります。
実際、気づいたきっかけは別件でフォルダの中身を確認したときでした。偶然です。
対策:生存確認を仕組みに入れる
作ったのは、「静かに壊れているものを探しに行く」スクリプトです。
# 最新の生成物が3日以上前なら警告
latest=$(ls -t "$DRAFT_DIR"/*.md | head -1)
mtime=$(stat -f %m "$latest")
days=$(( ( $(date +%s) - mtime ) / 86400 ))
[ "$days" -le 3 ] && ok "最新は ${days}日前" || warn "最新が ${days}日前。停止の可能性"
見ているのは3種類です。
| 確認対象 | 検出できること |
|---|---|
| 出力の鮮度 | 最新ファイルが古い=止まっている |
| 前回の終了コード | launchctl list の3列目が0以外=失敗 |
| 依存している認証 | 実際に軽く叩いてみて認証が生きているか |
3つ目が肝でした。今回の原因は認証切れなので、「認証が生きているか」を直接確かめないと検出できません。出力の鮮度だけを見ていると、気づくのに最低3日かかります。
そしてこのスクリプトを、週次の定例作業に組み込みました。 単体で存在するチェックスクリプトは実行されなくなるので、必ず実行される流れの中に置くのが重要です。
設計として正しかったこと
反省ばかりではなく、助かった判断もありました。
失敗したときにテーマのインデックスを進めない設計にしていたことです。
テーマ1 → 失敗 → インデックスは0のまま
テーマ1 → 失敗 → インデックスは0のまま
(復旧)
テーマ1 → 成功 → インデックスが1に進む
もし失敗してもインデックスを進める作りにしていたら、14日ぶんのテーマが消費されて、生成されないまま飛んでいました。 復旧したら止まった位置から自動で再開できたのは、この1点のおかげです。
バッチ処理を書くときは「失敗したら状態を進めない」を原則にすると、復旧が単純になります。
つまずきやすい点
ログを出しているだけでは監視ではない
ログファイルへの書き込みは、誰かが見に行ったときだけ意味を持ちます。 見に行く動機がない限り、ログは存在しないのと同じです。
監視というのは「異常のときに、こちらから知らせに来る」ことです。ログを出すことと監視することは別物でした。
「成功したときだけ通知」は失敗を検出できない
成功時に通知を飛ばす作りだと、通知が来ないときに「今日は何もなかった」と「壊れている」の区別がつきません。沈黙は成功の証拠になりません。
検出したいのは失敗のほうなので、失敗と沈黙の両方を拾える形にする必要があります。出力の鮮度を見る方式にしたのはこのためです。
認証は必ず切れる前提で作る
OAuthのセッション、APIキー、Cookie。どれも有効期限があります。「切れたら気づける仕組み」を最初から入れておくべきでした。
特にCLIツールの認証は、手元では動くのに自動実行では切れている、ということが起こります。
よくある質問
launchdで動かした処理が失敗しているか確認する方法は?
launchctl list の出力で、該当ジョブの行の2列目が前回の終了コードです。0以外なら前回の実行が失敗しています。またplistのStandardErrorPathで指定したログファイルにエラー出力が残ります。
macOSのシェルスクリプトでmapfileが使えないのはなぜですか?
macOSに標準で入っているbashはバージョン3.2で、mapfileはbash 4.0以降の機能だからです。連想配列(declare -A)なども同様に使えません。while readループなど、bash 3.2の範囲で書く必要があります。
自動化が止まったことに気づくにはどうすればいいですか?
出力ファイルの最終更新日時を定期的に確認するのが最も簡単です。あわせて、依存している認証が有効かどうかを実際に叩いて確かめると、出力が途絶える前に検出できます。これらのチェックを週次の定例作業に組み込むと、実行され続けます。
まとめ
個人の自動化で本当に難しいのは、作ることではありませんでした。
- エラーは誰の目にも触れない場所に出る
- 成功も失敗も、体感は同じ「何も起きない」
- 沈黙は成功の証拠にならない
対策は特別なものではなく、出力の鮮度・前回の終了コード・依存する認証の3つを、定期的に見に行くだけです。それを既存の定例作業に組み込めば、実行され続けます。
自動化を作ったら、同時に「壊れたと気づく手段」も作る。55回中24回失敗という数字を見て、そこが本体だと考えを改めました。
関連:Claude Codeで学習アプリを作った全記録 / エックスサーバーで取ったドメインをNetlifyで使う正しい手順