今回は「セルフホスト Runner × プロキシ地獄を突破して CI を安定稼働させた話」です。
よし。セルフホスト + 企業ネットの制約下でも CI/CD をフルパスにすることができた。
— よだか(夜鷹/yodaka) (@yodakablog) May 28, 2025
前回 #237 (2025/5/26) では「壊さず早く回す」基盤づくりとして v 1.0.0 リリース直前のテスト&ビルドフローを固めた。
その後 本番ネット回線相当で動かす E2E が真っ赤になり、丸2日かけて緑化したので経緯と学びを残す。
1. 症状とゴール— “どこが壊れていたのか / どこまで直したいのか”
| 症状 | 何が起きていたか | 体感インパクト | 
|---|---|---|
| ① codeload.github.com への HEAD が 400 / 502 | 社内/ISP フィルタが HTTP HEAD メソッドをブロック。GitHub Runner はアクションのメタ情報取得で HEAD を多用するため通信が途中で落ちる。 | self-hosted Runner が “Missing download info …” と即死。ワークフロー先頭で真っ赤。 | 
| ② actions/upload-artifact@v2ダウンロード失敗 | ①のせいで actions/*のバイナリが取得できず、checkout は通っても upload-artifact で止まる。 | ユニットテストはパスするのにアーティファクト保存ステップだけ常に失敗し、本番 E2E まで辿り着かない。 | 
| ③ アーティファクト upload で 400 “name invalid” | 公式 Artifact API が 2024 Q4 に刷新 → v2 SDK が古いエンドポイントに POST。返ってくる 400 を Runner が「名前が不正」と誤解釈。 | 「名前おかしい?」と首をかしげ続ける負のデバッグループが発生。 | 
1-2. 目指したゴール 🎯
| ゴール | 具体的な判定基準 | 
|---|---|
| A. self-hosted Mac で “Live E2E” が緑 | ✔ GitHub Actions の Live E2E ワークフローが 0 エラーで完走 | 
| B. テストログを GitHub アーティファクトに保存 | ✔ logs/live-e2e.logが Actions 画面右の Artifacts に残る | 
| C. ジョブ失敗時だけ Slack 通知 | ✔ 失敗 run で :x: メッセージが #dev-alerts に届く/成功時は無通知 | 
1-3. 成功後の世界 🌈
- Runner は1日2回(0 時 / 12 時)自動で E2E を叩く
- 任意タイミングで “Run workflow” しても即グリーン
- 障害があれば Slack へ「❌ Live E2E 失敗」とリンク付きで飛ぶ
- テスト出力はアーティファクトに 90 日保管 → 後追い解析も安心
これで “本番に近いネット環境でも 壊れたら 30 分以内に気づける” パイプラインが完成!
2. 採ったアプローチ ― “赤の山を一つずつつぶす”
2-1. ローカルプロキシで HEAD → GET 変換
企業/ISP の FW が HTTP HEAD を弾く ⇒ Runner が落ちる。
そこで Node 40 行 の簡易リバースプロキシを 127.0.0.1:8080 で常駐させ、HEAD を GET に書き換えて転送した。
// proxy.mjs(主要部だけ)
import http  from 'http';
import https from 'https';
import net   from 'net';
const server = http.createServer((cliReq, cliRes) => {
  const opts = {
    hostname: cliReq.headers.host,
    port:     443,
    path:     cliReq.url,
    method:   cliReq.method === 'HEAD' ? 'GET' : cliReq.method,   // ←★変換
    headers:  cliReq.headers
  };
  https.request(opts, res => res.pipe(cliRes)).end();
});
server.on('connect', (req, cSock) => net                 // HTTPS CONNECT は透過
  .connect(443, req.url.split(':')[0], () => {
    cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
  })
  .pipe(cSock).pipe(net.connect(443, req.url.split(':')[0]))
);
server.listen(8080);
https_proxy=http://127.0.0.1:8080 # Runner 全 HTTPS をここ経由
結果:HEAD 400/502 がゼロになり、ダウンロードフェーズ突破。
2-2. upload-artifact を Vendor(リポジトリ内取り込み)
外向き DL を完全排除したい+ v2 は API 互換切れで 400 を吐く。
rm -rf .github/actions/upload-artifact # 旧 v2 git clone --depth 1 --branch v4 \ https://github.com/actions/upload-artifact.git \ .github/actions/upload-artifact rm -rf .github/actions/upload-artifact/.git # 素のファイルに
ワークフロー側はパス参照だけ:
- uses: ./.github/actions/upload-artifact # ← vendored v4
効果:外部依存ゼロ&400 “name invalid” が半分解消。
2-3. プロキシ経由/直通を NO_PROXY で振り分け
アーティファクト用ドメインはプロキシ不要。サブドメイン全部を外すには先頭ドットが効く。
https_proxy=http://127.0.0.1:8080
NO_PROXY=api.github.com,codeload.github.com,.actions.githubusercontent.com,127.0.0.1,localhost
no_proxy=${NO_PROXY}   # 大小どちらを見るライブラリもある
→ pipelines*.actions.githubusercontent.com への POST が 200 に。
2-4. アーティファクト名を run_id で一意化
同名アーティファクトがワークフロー内でロックされると 400。
with:
  name: live-e2e-logs-${{ github.run_id }}   # 例: live-e2e-logs-15290012345
run_id は 64bit で衝突ゼロ。これで “名前は無効” 完全消滅。
2-5. Slack 通知を Docker Action → curl 1 行 に簡素化
macOS self-hosted には Docker デーモンが無い場合が多く、rtCamp/action-slack-notify は container null で死ぬ。
ならば直接叩くほうが早い。
- if: failure()
  run: |
    curl -X POST -H 'Content-Type: application/json' \
         -d "{\"text\":\"❌ Live E2E 失敗\nCommit: ${{ github.sha }}\"}" \
         ${{ secrets.SLACK_WEBHOOK_URL }}
メリット
- 依存ゼロ・起動高速
- Webhook URL だけ変えれば Teams/Discord も同じ書式で流用可
まとめ:
1️⃣ HEAD→GET で通信遮断を回避 → 2️⃣ vendored v4 で API 互換問題を解決 → 3️⃣ NO_PROXY & run_id で残りの 400 を潰す → 4️⃣ Slack を軽量化。
これで 日次 E2E が 完全緑化 + ログ永続 + 障害アラート の三拍子そろった CI に進化した。
3. 完成したワークフロー(要旨)を分解して見る
name: Live E2E          # ── タイトルそのまま “本番相当 E2E”
on:
  schedule:             # ── 毎日 2 回の定期実行
    - cron: "0 0,12 * * *"
  workflow_dispatch:    # ── 手動実行ボタンも有効
jobs:
  live-e2e:
    runs-on: self-hosted        # ── Mac mini 上のセルフホスト Runner
    steps:
      # ① ソースを取る(depth=1 デフォルト)
      - uses: actions/checkout@v3
      # ② buildx でマルチプラットフォーム対応ビルド
      - uses: docker/setup-buildx-action@v2
      # ③ 最新コードでイメージをローカルビルド
      - run: docker compose build mmbot
      # ④ 実ネット E2E を叩いてログを保存
      - run: |
          bash tests/e2e/realnet_checks.sh 2>&1 | tee logs/live-e2e.log
        env:
          MMBOT_MODE: mainnet          # 本番モード
          BYBIT_APIKEY: ${{ secrets.BYBIT_APIKEY }}
          BYBIT_SECRET:  ${{ secrets.BYBIT_SECRET }}
      # ⑤ ログをアーティファクトにアップロード(vendored v4)
      - uses: ./.github/actions/upload-artifact
        with:
          name: live-e2e-logs-${{ github.run_id }}   # run ごとに一意
          path: logs/live-e2e.log                    # ファイル 1 本だけ
      # ⑥ 失敗した run だけ Slack に投げる(超軽量版)
      - if: failure()
        run: |
          curl -X POST -H 'Content-Type: application/json' \
               -d "{\"text\":\"❌ Live E2E 失敗\nCommit: ${{ github.sha }}\"}" \
               ${{ secrets.SLACK_WEBHOOK_URL }}
✏️ 解説ポイント
| ブロック | キモ | 
|---|---|
| runs-on: self-hosted | ローカル Mac に置いた Runner なので、企業ネットと同じ出口 で通信テストできる。 | 
| docker compose build | コンテナを毎回ビルドして “動くバイナリ + 最新依存” を E2E に投入。 (今後は --cache-fromで高速化予定) | 
| realnet_checks.sh | Bybit のメインネット WebSocket / REST を実際に叩く。 → 単体テストでは拾えない “API 変更” や “ネット遮断” を検知。 | 
| upload-artifactvendored v4 | API 互換切れを回避。 run_idでロック衝突しない。 | 
| Slack 通知を curl1 行 | Docker 行きのラッパー Action を使わず macOS でもコケない。 成功時は無通知=ノイジーレス、失敗時だけ鳴る。 | 
🚦 動作フロー
- 0:00 / 12:00(または手動)で Runner が起動
- イメージをビルド → 本番 API で実 E2E → logs/live-e2e.log生成
- ログを GitHub UI に保存
- テスト OK → ジョブ緑 / NG → Slack に ❌ + Commit SHA
結果、「落ちたら 30 分以内に Slack」+「いつでもログ確認」 が保証される。
日次監視と CI の両立、最低限これだけでかなり安心感のある運用になった。
4. 学びメモ
| 気づき | Tips | 
|---|---|
| GitHub Runner は HEAD を投げる | FW が HEAD を弾く場合は GET へ変換するだけで通る | 
| actions/upload-artifact v2 は実質 deprecated | v4 にすると 400 エラーが一撃解消 | 
| no_proxy は前にドットでワイルドカード | .actions.githubusercontent.com→ すべての pipelines サブドメインを直通 | 
| セルフホストで Docker-action は要注意 | macOS に Docker デーモンが無いと container nullエラー。curl に置き換えると早い | 
| プロキシ常駐は pm2 が楽 | pm2 start proxy.mjs && pm2 saveで再起動耐性◎ | 
5. 次の TODO
- pm2 で proxy.mjs常駐+自動再起動
- .envをリポジトリ wiki にサンプルとして残す
- 旧ファイル削除・git gcでサイズ整理
- ACTIONS_STEP_DEBUG=trueを Secret に追加(トラブル時のみ有効化)
これで本番相当ネットでも E2E が 100 % グリーンに!
しばらくはアラートが鳴らない平穏な日々が続く…はず。次回は Slack への成功通知と GHCR への自動 push を整えて “v1.1.0 CI 完全盤” へステップアップ予定。
ChatGPTo3による 評価:82 / 100 点
 
		「今回完成させたスクリプトをプロbotterの目線で採点するなら100点満点で何点くらいつく?シビアに評価してみて。改善点も欲しい。」という指示を出してChatGPTo3に評価してもらいました。
| 観点 | 加点要素 | 減点要素 | 小計 | 
|---|---|---|---|
| 信頼性 | - プロキシ+NO_PROXY 切替でネット障害耐性 ◎ - vendored v4 で外部依存ゼロ | - proxy.js が PM2 常駐前提。落ちると全部巻き添え | 18/20 | 
| セキュリティ | - Secrets 参照のみで値は一切ハードコード無し | - curl -vIで User-Agent 丸出し(FW ログに残る) | 15/20 | 
| 保守性 | - run_id で重複回避、if-no-files-found も設定 - ステップ名が具体的 | - バージョン pin が緩い ( @v3/@v2)- Connectivity Check とテスト本体が 1 ワークフローに混在 | 14/20 | 
| パフォーマンス | - buildx 使用でマルチアーキ OK | - 毎回 docker compose build→ キャッシュ効かず遅い | 10/15 | 
| 可読性 | - コメント丁寧。Slack 失敗通知が分かりやすい | - 長い `curl | tee` ワンライナーは読みにくい | 
| ベストプラクティス | - アーティファクト名一意化 - cron & 手動両対応 | - 成功時通知が無い → “落ちた時だけ” は良いが CI Dashboard が寂しい - actions/checkout@v3など EOL 迫る | 13/20 | 
総合:82 点
「壊れないための工夫」は十分。“毎日回す本番 E2E” としては合格ラインだけど、もう一歩で満点。
改善アイデア 6 連発
| 番号 | 施策 | メリット | 
|---|---|---|
| 1 | proxy.mjs を PM2 常駐化し、 healthcheckエンドポイントを実装 | プロキシ落ちても自動再起動+ランナー側で監視できる | 
| 2 | Docker レイヤキャッシュを actions/cacheorbuildxに追加 | Build ステップが 30 → 数秒に短縮 | 
| 3 | バージョン pin を commit hash に固定 actions/checkout@v3→@v3.6.0 | 予期せぬメジャーアップデート事故防止 | 
| 4 | ステップ分割 or Reusable Workflow ①ネット疎通ヘルスチェック ②E2E テスト本体 | 障害切り分けが楽+再利用性アップ | 
| 5 | 成功時も Slack reaction だけ送信 :white_check_mark:をレスポンスに追加 | チーム側で「緑かどうか」を一目で把握 | 
| 6 | concurrency:キーでジョブ競合防止concurrency: group: live-e2e cancel-in-progress: true | 前回 run がダラけても新しい run を優先できる | 
結論:現状でも CI/監視としては合格。ただ「速さ・保守・次期 GitHub Actions の潮目」を意識して上記6点を磨けば、満点近くまで持っていける!
“o4 で回し → 詰まったら o3” という切り替え戦略のポイント
 
		o4-mini-highとo3を使い分けたのが、私的には重要ポイントだった。基本はo4だけど、3回ほど試してもダメだったらo3に切り替えてピンポイントで潰す、みたいな感じで使った。抽象度を上げて引きの視点で問題解決したい時にはo3の方が良い。
| 使いどころ | o4-mini-high (高速・量産型) | o3 (推論重視型) | 
|---|---|---|
| 長所 | - レスポンスが軽快、試行回数を稼げる - 細かなログ確認やスクリプト添削など“手数”が欲しい作業で強い | - 文脈を俯瞰し、問題の核心を抽象化して提示してくれる - 「そもそも設計を変えた方が早い」系の示唆が得やすい | 
| 短所 | - “目の前のログ” には強いが、大局観はやや弱い - 似た提案を繰り返す場合あり | - 応答がやや重め、細部のコピペ確認は苦手 - ステップバイステップで逐次検証したい局面では冗長に感じる | 
| 相性が良いタスク | ・docker build エラー三段活用 ・YAML インデント修正 ・ grep -Rの一発回答 | ・プロキシ設計の抜本見直し ・NO_PROXY ドメインパターン整理 ・CI フロー再構成の方針決定 | 
使い分けフロー(実際にやったこと)
- まず o4 でラピッド試行
- HEAD→GET 変換案、PF DNAT、nginx proxy_method… と3連投
- ログを貼って「これでいける?」→ ダメならすぐ次案
 
- 3ストライクで o3 にバトンタッチ
- 質問を抽象度アップ:「通信経路を整理するとしたら、依存を3段階で分けて教えて」
- o3 が“プロキシバイパスと SDK の互換切れ”を同時に指摘 → 問題を俯瞰できた
 
- 核心が見えたら再び o4 で実装フェーズへ
- v4 Vendor 手順や .env具体例を高速に生成
- Slack curl ワンライナなどピンポイント改修
 
- v4 Vendor 手順や 
学び
- 高速探索 × 高解像度の切り替えはコスト最小
- o4 で“量”を出して枝切り
- o3 で“芯”を射抜き
- 仕上げを o4 で量産
 
- 抽象レイヤを意識して質問を変える
- o4 には “このログの 400 を 200 にしたい” と具体
- o3 には “CI ネットワーク全体の設計を洗い直すと?” と俯瞰
 
このハイブリッド運用で、最短ルートよりむしろ “寄り道しつつも深い理解が得られる” 開発体験になった。
