【GitHub Actions無料枠パンク脱出録】『2秒でFailed』と.npmrc混入エラーを解決し、macOS標準『launchd』で完全無料の自動実行基盤を作った全記録
作成日:
更新日:

【GitHub Actions無料枠パンク脱出録】『2秒でFailed』と.npmrc混入エラーを解決し、macOS標準『launchd』で完全無料の自動実行基盤を作った全記録


🔧 論旨

毎日のニュース収集や AI レポート生成などの定期バッチ処理を「無料だから」と GitHub Actions のスケジュール実行(cron)に任せていると、画像が増えた非公開(プライベート)リポジトリである日突然 「Failed in 2 seconds(わずか 2 秒で失敗)」という英語のエラーメールが毎日届く悪夢 に見舞われます。当サイトでも、Mac 用の RAMDisk 設定(.npmrc)混入による Linux 権限クラッシュを直した直後に、この無料枠上限の壁に激突しました。

結論として、個人開発の定期スクリプトを無理にクラウド上で毎回 1GB クローンして回す必要はありません。本記事では、GitHub Actions の定期実行をスパッと廃止し、手元の Mac 標準『launchd(LaunchAgents)』へ完全移行することで、スリープ復帰直後の自動補完・Wi-Fi 再接続待機・RAMDisk 自動利用を備えた「完全無料・失敗メールゼロ」の自動化基盤を構築した全手順を公開します。
🔧 合わせて読みたい深まる関連情報
🔧 対象となる方
  • 非公開(プライベート)リポジトリで GitHub Actions を使っていたら、月の後半から突然『Failed in 2 seconds』のエラー通知メールが連発して困っている方

  • ローカルの Mac 開発環境を快適にする設定ファイル(.npmrc 等)をうっかりコミットし、Linux ランナー(ubuntu-latest)をクラッシュさせた経験がある方

  • 従来の cron ではなく、「指定時刻に Mac がスリープしていても、起きた瞬間に自動で未実行分を補完してくれる launchd」 の実践的な設定方法を知りたい方

  • nvm で入れた Node.js が launchd から見つからない問題や、スリープ復帰直後の Wi-Fi 未接続エラーを防ぐ堅牢な実装 をそのまま使いたい方

吹出しアイコン

まいったぞ……!毎日朝と夕方に自動で最新ニュースを集めて AI に要約させとった GitHub Actions が、先週からピタッと止まってしもうたんじゃ!しかも直したと思ったら、今度は「Failed in 2 seconds(2 秒で失敗)」っていう不気味なメールが届くんじゃよ!

吹出しアイコン

おじさん、実はそれ、まったく別々の 「2 段階の罠」 を連続で踏んでいるんです!1 つ目は「Mac 専用のパス設定が Linux サーバーに混入した事故」、そして 2 つ目の「2 秒で即死」はクラウド無料枠ならではの構造問題なんですよ。順番に解剖していきましょう!


定期実行が突然止まった2つのリアルな原因

Mac用.npmrc混入によるLinux権限エラー

まず最初に定期バッチが止まった直接原因(確定事実)は、意外な伏兵でした。手元の iMac の内蔵 SSD 寿命を守るため、数日前にプロジェクト直下へ次のような .npmrc ファイルを作成していたのです。

# MacローカルのRAMDiskへキャッシュを逃がすための .npmrc
cache=/Volumes/RAMDisk/yourname/npm-cache
tmp=/Volumes/RAMDisk/yourname/tmp

Mac 上ではこれで超高速・SSD 書き込みゼロになって大満足だったのですが、うっかり .gitignore に .npmrc を追加し忘れたまま GitHub へプッシュしてしまいました。

すると何が起きたか?GitHub Actions の仮想マシンは macOS ではなく Linux(ubuntu-latest) で動いています。ジョブの最初で npm ci が走った瞬間、npm が律儀にこの .npmrc を読み込んで Linux のルート直下に /Volumes/RAMDisk/... フォルダを作ろうとし、EACCES: permission denied, mkdir '/Volumes'(権限エラー)でスクリプトに到達する前に 100% 強制終了 していたのです!

これは git rm --cached .npmrc で手元の Mac のファイルは残したまま Git の追跡から外し、.gitignore に .npmrc を登録することで即座に解決しました。

わずか2秒で落ちる非公開リポジトリ無料枠の壁

ところが、その 2 日後に今度は 「Failed in 2 seconds(Annotations: 2)」 という通知メールが届きました。ここで冷静に引き算をしてみます。

  • 当ブログのリポジトリは下書きや管理資料を含むため 非公開(プライベート)リポジトリ にしており、画像が増えた結果 .git 履歴が 約 1.0GB、node_modules が 約 536MB まで育っていました。
  • もし仮想マシンが起動して 1 番目のステップ(Checkout repository:1GB の取得)に 1 ミリでも入っていれば、物理的に 2 秒では絶対に終わりません(最低でも 20〜40 秒かかります)。
  • つまり、スクリプトのバグではなく 「GitHub 側がジョブの起動そのものを入り口(0〜2 秒)で拒否している」 状態です。

公開(パブリック)リポジトリなら GitHub Actions は無制限ですが、非公開(プライベート)リポジトリの GitHub Free 無料枠には「月間 2,000 分」および「Actions キャッシュ・アーティファクト合計 500MB」という厳格な上限 があり、支払い上限(Spending limit)が初期値の $0 になっていると上限到達と同時に全ジョブが即時ブロックされます。推論ですが、ワークフロー内の cache: 'npm' が約 536MB のパッケージ群を保存して 500MB ストレージ枠(または月後半での分数枠)を使い切ったことで、10 月下旬から起動ロックがかかったと見ると時系列も 100% 一致します。


クラウド課金よりMac標準launchdが最強な理由

cronと違いスリープ復帰直後に自動補完される強み

ここで「GitHub に追加課金して枠を広げるか、キャッシュを削って延命するか」と考えたとき、ふと気づきました。「そもそも毎日自分が座って作業している母艦 Mac(64GB メモリ+16GB RAMDisk 搭載)が目の前にあるのに、なぜわざわざ毎回クラウド上で 1GB のデータをクローンして 500MB のパッケージを入れ直しているんだろう?」 と。

そこで GitHub Actions 側の schedule(定期実行)を完全に削除し、macOS に最初から標準搭載されているシステム管理機構 launchd(~/Library/LaunchAgents/) へ移行しました。

比較項目GitHub Actions(非公開リポジトリ)従来の cron(Mac ローカル)macOS 標準 launchd(今回採用)
費用・容量制限無料枠は月 2,000 分・500MB まで。超過すると即エラー停止。完全無料・容量制限なし。完全無料・容量制限なし・失敗メールゼロ。
準備時間と通信量毎回リポジトリ取得と npm 導入で数十秒〜数分を消費。手元の既存ファイルをそのまま 0 秒で実行。手元の既存環境+RAMDisk で即座に実行開始。
スリープ中の予定時刻クラウドなので PC 電源に関係なく動く。スリープ中だとスキップ(飛ばされて未実行)になる。StartCalendarInterval ならスリープ復帰直後に自動で補完実行される!

最大の決め手は 3 行目です。昔ながらの cron は「朝 6 時 17 分にちょうど Mac が寝ていたら、その日の朝バッチは飛ばされて終わり」になります。しかし macOS 純正の launchd で StartCalendarInterval を指定しておけば、夜中に Mac をスリープさせていても、朝コーヒーを淹れて Mac を開いた瞬間に「あ、6 時 17 分の分がまだだったね!」と自動で追いかけて実行してくれる のです。

nvm環境のNodeフルパス指定plistの書き方

launchd を使う際に初心者が一番ハマりやすいのが、「ターミナルで手動実行すると動くのに、launchd から自動実行すると node: command not found で落ちる」 という罠です。

理由は、launchd は人間のログインシェル(.zshrc 等)を読み込まず最小限の PATH(/usr/bin:/bin 等)だけで起動するため、nvm や Homebrew で入れた Node.js の場所を知らないからです。必ず which node で調べた Node 本体のフルパス を ProgramArguments と EnvironmentVariables の両方に明記します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.daily-ai-news</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/yourname/.nvm/versions/node/v24.20.0/bin/node</string>
        <string>/Users/yourname/projects/my-blog/scripts/generate_daily_ai_news.js</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/Users/yourname/projects/my-blog</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/Users/yourname/.nvm/versions/node/v24.20.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
        <key>HOME</key>
        <string>/Users/yourname</string>
    </dict>

    <!-- 毎日 朝06:17 と 夕方18:23 に自動実行(スリープ時は復帰直後に自動補完) -->
    <key>StartCalendarInterval</key>
    <array>
        <dict>
            <key>Hour</key>
            <integer>6</integer>
            <key>Minute</key>
            <integer>17</integer>
        </dict>
        <dict>
            <key>Hour</key>
            <integer>18</integer>
            <key>Minute</key>
            <integer>23</integer>
        </dict>
    </array>

    <key>StandardOutPath</key>
    <string>/tmp/daily-ai-news.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/daily-ai-news.err.log</string>
    <key>ProcessType</key>
    <string>Background</string>
</dict>
</plist>

この設定ファイルを ~/Library/LaunchAgents/com.example.daily-ai-news.plist に配置し、launchctl load ~/Library/LaunchAgents/com.example.daily-ai-news.plist(または launchctl bootstrap gui/$(id -u) ...)で登録すれば準備完了です。


スリープ復帰とRAMDisk運用の4大エッジケース対策

Wi-Fi再接続60秒待機とRAMDisk自動切替

ただし、「スリープから復帰した瞬間に自動で走る」ということは、裏を返せば 「Mac が目を覚ましてから 0.5 秒後、まだ Wi-Fi や有線 LAN が繋がり切る前にスクリプトが走り出して通信エラーでコケる!」 というローカル特有のエッジケースが起きます。

そこで、実行される Node.js スクリプトの冒頭に次の 4 つの防御ガード を組み込み、どんなタイミングで復帰しても 100% 失敗しない設計に仕上げました。

🔧 🛡️ ローカル『launchd』運用を無敵にする4つのエッジケース対策
  1. スリープ復帰直後のネットワーク疎通待機(最大 60 秒) : いきなり RSS 取得や API 通信を始めるのではなく、まず dns.promises.lookup('generativelanguage.googleapis.com') 等で名前解決ができるか 3 秒おきにチェックし、Wi-Fi が再接続されるまで最大 60 秒行儀よく待ってから本処理へ入ります。

  2. RAMDisk 未マウント時の /tmp 自動フォールバック : 再起動直後などでまだ /Volumes/RAMDisk がマウントされていない瞬間に走ってもクラッシュしないよう、fs.existsSync('/Volumes/RAMDisk') を確認し、無ければ標準の /tmp を使う二段構えにします。

  3. 3 時間以内の重複実行スキップ(多重発火防止) : 朝と夕方の両方ともスリープしていた後に復帰した際、2 回分が同時に連続発火して API クォータを無駄食いしないよう、最新の生成ファイルの更新時刻を確認し「直近 3 時間以内に成功済みならスキップする(手動テスト時は --force フラグで強制実行可能)」ガードを入れました。

  4. RSS タイムアウトと API レート制限リトライ : 海外サイトの RSS が応答しない時に永久フリーズしないよう 15 秒タイムアウトを設定し、日報生成の直後に下書き生成を呼ぶ際は 10 秒のクールダウン待機と 429 / 503 エラー時の指数バックオフ再試行を組み込みました。

// スリープ復帰直後のWi-Fi未接続を吸収する待機ガード(Node.js)
import dns from 'node:dns/promises';

async function waitForNetwork(maxWaitMs = 60000) {
  const start = Date.now();
  while (Date.now() - start < maxWaitMs) {
    try {
      await dns.lookup('generativelanguage.googleapis.com');
      return true; // 通信開通を確認!
    } catch {
      await new Promise((r) => setTimeout(r, 3000)); // 3秒待って再確認
    }
  }
  throw new Error('ネットワーク接続が確認できないため安全に終了しました');
}

3時間以内の重複発火防止とAPIレート制限対策

吹出しアイコン

なるほどなぁ〜!ただ launchd に登録するだけじゃなく、「寝起きでまだ Wi-Fi が繋がっとらん数秒間を待ってやる」っていう気配りを入れておくだけで、エラー率がゼロになるんじゃな!

吹出しアイコン

そうなんです!しかも毎回 1GB のリポジトリをクラウドへコピーする必要もないので、手元の Mac でスッと静かに数秒〜数十秒で終わり、GitHub から不吉なエラー通知メールが届くことも二度となくなりました!


まとめ:個人開発の定期バッチは母艦Macで賢く回す

なんでもかんでもクラウドの CI/CD で回すのが正解とは限りません。特に画像や記事資産が増えた非公開リポジトリでは、GitHub Actions は「手動実行(workflow_dispatch)や最小限のチェック」に留め、日々の重い定期バッチは手元の母艦 Mac に任せるほうが圧倒的にシンプルで経済的です。

  1. OS 固有のパスを書いた .npmrc などは必ず .gitignore に入れ、Linux 環境への混入事故を防ぐ
  2. 非公開リポジトリの無料枠(500MB / 2,000 分)に悩まされたら、スリープ復帰補完つきの macOS 標準 launchd へ切り替える
  3. Node のフルパス指定と「寝起き最大 60 秒のネットワーク疎通待機」を入れれば、ローカル自動実行は驚くほど安定する

GitHub からの Failed in 2 seconds メールにモヤモヤしていた方は、ぜひ頼もしい手元の Mac の LaunchAgents を活用してみてくださいね!

朝の光が差す部屋で凛とした表情を見せるノルウェージャンフォレストキャット

▲ 朝起きたときにはもう情報収集が終わっている、静かで賢いローカル自動化は一度作ると手放せません