✳ClaudeModsClaude Code 模组目录
☰
● 0 人在线 · 浏览 0 次
+ 提交作品
← 返回作品集
GitHub 仓库 · Git 与代码审查

pr-bridge-watch

通过 WebSocket 桥接实时观察新拉取请求的 CI 状态。

iippoan@ippoan
已翻译

关于这个 mod

通过 WebSocket 桥接实时观察新拉取请求的 CI 状态。

此作品由社区作者维护,具体功能、版本要求与配置方法请查看下方原始 README。本站提供权限扫描结果;安装前请检查源码及外部依赖。

安装

请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。

claude plugin marketplace add ippoan/gh-actions-live
claude plugin install pr-bridge-watch
原文 / README

GitHub Actions Live

GitHub の repo の Actions ページを起点に、実行状態をポーリングせず受け取る Chrome 拡張。 repo ごとの列 + run カードで並べるので、同一 repo で並列に走っている run が全部同時に見える。

何をどこから取るか

1. GET https://github.com/<owner>/<repo>/actions   (cookie 付き)  = スナップショット
     ├─ .Box-row[id^=check_suite_]        → 全 run (畳まれていない)
     ├─ link[rel=shared-web-socket]       → alive の socket URL
     └─ data-channel                      → 署名済み購読トークン
2. wss://alive.github.com/_sockets/u/<userId>/ws?session=...
     → {"subscribe": {"<トークン>": null}}  →  {"e":"ack","off":"...","health":true}
   **この socket は github.com のタブ (content script) で張る。** 拡張ページから張ると
   Origin が chrome-extension:// になり alive が握手直後に 1006 で切る。
   background が監視対象 repo の Actions ページを pinned タブで開いて場所を用意する
   (pinned タブを閉じても / 他サイトへ移しても background が即ダッシュボードへ `closed` を伝え、数秒で開き直す #36)
3. push が来たら、その run の partial (/actions/workflow-run/<checkSuiteId>, 約 10KB) だけ
   取り直して状態を確定する

別の API を叩かないのが要点。Actions ページの取得そのものがスナップショットなので、 初期状態を取るための追加エンドポイントは要らない。

3 は GitHub 自身のクライアント (js-updatable-content + data-url) と同じ方針で、 未文書な push ペイロードの形に依存しないための作り。

なぜ集約ダッシュボードの WebSocket を使わないか

CI 状態を集約して WebSocket で配信するダッシュボードは珍しくないが、その手の実装は たいてい repo あたり「最新の in-progress」と「最新の completed」だけに畳んでいる。 一覧としては見やすいが、同一 repo で複数の workflow が並列に走るとそれらが 1 件に潰れる。

同じ tag に対して CI と複数の deploy worker が同時に走るような構成では、 畳まれた側からは 1 本しか見えない。潰れないのは GitHub 自身の alive socket (check_suites:<id> 単位) のほうで、本拡張がそちらを直接読む理由がこれ。

なお webhook 経由の集約は取りこぼしが起きる。実運用の集約基盤が 「一定時間 in_progress のまま残った run を API から取り直す」補償を持っているのは そのためで、webhook は「来たら早い」が「来ないことがある」経路だと考えたほうがよい。

なぜブラウザ拡張なのか

| | タブに JS を挿す | Rust / Worker | 拡張 | |---|---|---|---| | タブを開いたままにする必要 | 要る | 不要 | 不要 (このウィンドウが本体) | | セッションを外に持ち出す | 不要 | user_session を貼る必要あり | 不要 | | 変化を外に push できる | しにくい | できる | できる |

user_session は 2FA を素通りするアカウント全権の資格情報なので、ブラウザの外に出さないのが要点。 拡張は「ブラウザの中」なので、host_permissions があれば fetch に cookie が自動で付く。

認証の境界 (実測)

| | ログイン有 | 無認証 | |---|---|---| | data-channel トークン | 26 個 | 26 個 (貰える) | | link[rel=shared-web-socket] | あり | 無し (HTML に alive の言及 0 件) | | partial /actions/workflow-run/<id> | 200 / 10.9KB | 404 | | フルページ | 200 / 440KB | 200 / 440KB |

→ push はログイン機能。無認証でできるのは HTML / atom のポーリングまで。

ウィンドウ

ツールバーのアイコンをクリックすると、タブバーもアドレスバーも無い独立ウィンドウで開く。 設定画面から 独立ウィンドウ / 最大化 / 全画面 / タブ を選べる。

chrome.windows.create({ url, type: 'popup', width: 1280, height: 820 })  // アプリ風
chrome.windows.create({ url, type: 'normal', state: 'maximized' })       // 最大化
chrome.windows.create({ url, type: 'popup',  state: 'fullscreen' })      // 全画面

このウィンドウが fetch も WebSocket も描画も全部持つ。service worker は ウィンドウを開くのと通知を出すだけなので、MV3 の service worker 寿命問題に当たらない。

インストール

MSI は 1 つで、既定は perUser (admin 不要)。管理端末向けの機能は ALLUSERS=1 を付けたときだけ出る。

| 端末 | コマンド | 何が起きるか | |---|---|---| | 非管理 (Windows Home / WORKGROUP / 個人 PC) — こちらが既定 | msiexec /i gh-actions-live-x.y.z-x64.msi (ダブルクリックでも同じ) | %LOCALAPPDATA%\Programs\gh-actions-live\extension に配置 + 自動更新タスクを登録。Chrome への初回読み込みだけ手動 | | 管理端末 (AD / Entra / Chrome Enterprise Core 登録済み) | msiexec /i gh-actions-live-x.y.z-x64.msi ALLUSERS=1 (UAC 昇格) | C:\Program Files\gh-actions-live に配置 + HKLM に ExtensionSettings (force_installed)。Chrome 再起動だけで入り、更新も Chrome が拾う | | 非管理端末を Chrome Enterprise Core に登録したい | … ALLUSERS=1 ENROLLMENTTOKEN=<token> | 上に加えて CloudManagementEnrollmentToken を書く。token は Google Admin で発行 |

なぜ分かれるか: Chrome の公式仕様で、Windows では Web Store 外 の拡張を force_installed できるのは 管理端末だけ。非管理端末ではレジストリに正しく書けていても Chrome が黙って捨てる (chrome://policy に [BLOCKED] + 警告で出る)。詳細は #9。 自分の端末は dsregcmd /status で分かる (AzureAdJoined / DomainJoined / EnterpriseJoined が全部 NO なら非管理)。

非管理端末での手順 (既定)

  1. Releases から gh-actions-live-*-x64.msi を落とし、 設定ごと インストールする (admin 不要):

    msiexec /i gh-actions-live-x.y.z-x64.msi REPOS=owner/repo,owner/repo2 BRIDGEURL=ws://host:8799 NOTIFY=0
    

    property は全部任意。渡した値は extension\config.json に書かれ、拡張が起動時に取り込む。 設定画面での手入力は不要。拡張を入れ直して chrome.storage が消えても config.json から戻る。 あとから変えたいときは同じコマンドを渡し直す (upgrade で上書き)。 ダブルクリックで入れた場合は設定画面から手で入れる (従来どおり)

  2. chrome://extensions → 右上「デベロッパー モード」ON → 「パッケージ化されていない拡張機能を読み込む」→ %LOCALAPPDATA%\Programs\gh-actions-live\extension

  3. ID が oaadakmclelmnaieokjbhldfacfckaaj になっていることを確認。その Chrome プロファイルで GitHub にログインしていること

以降の更新はダッシュボードの「更新」ボタン 1 クリック。MSI を入れ直す必要は無い。

更新の仕組み

Chrome は Web Store 外の拡張の update_url を相手にしないので自前で組む。拡張はディスクに 書けないため、MSI が小さな native messaging host (host.bat → host.ps1) を HKCU\Software\Google\Chrome\NativeMessagingHosts に登録する (Policies 配下ではないので admin 不要)。

  1. ヘッダに v0.0.12 → v0.0.13 あり と出たら 「更新」ボタンを押す
  2. 拡張が host を呼び、host が update.ps1 を実行: update.xml の版を見て gh-actions-live-extension.zip を落とし、sha256 を照合して extension\ を差し替える (ログは update.log)
  3. background が reloadSelf(): 先にダッシュボードのタブを about:blank に差し替えてから chrome.runtime.reload()。reload 後は同じタブにダッシュボードを読み込み直す (何もせず reload すると空ウィンドウが残る → #30。閉じて開き直すと新規ウィンドウが 前面に出ない → #32)

Linux 側 (bridge) から {"command":"update"} を送っても同じことが起きる。 定期的に勝手に上げたい人は update.ps1 -Register でタスク スケジューラに登録できる (既定では登録しない)。

MSI を使わずに zip を展開して読み込むこともできる。その場合は host が無いので、 新しい zip を同じフォルダに上書き展開して拡張カードの ↻ を押す。

再読込 / 更新の手動確認 (実機でしか分からない)

拡張の再起動 (chrome.runtime.reload()) はページを殺すだけでウィンドウは閉じないので、 放っておくと空のウィンドウが 1 枚残る (#30)。かといって先にタブを閉じると、復活が新規ウィンドウに なり、reload 直後の service worker からでは Windows の foreground lock で前面に出ない (#32)。 reload は background の reloadSelf() に一本化してあり、先にダッシュボードのタブを about:blank に差し替えてから reload し、onInstalled で同じタブに読み込み直す。 実機で見るのはここ:

  1. ダッシュボードを開いた状態で「再読込」→ 同じウィンドウのまま中身が復活し、前面にあること (空白のウィンドウが増えていない / ウィンドウが消えていない)
  2. bridge から {"command":"update"} / {"command":"reload"} を送っても同じこと
  3. 10 分ごとの self-update (ディスクの版が上がったとき) でも同じこと

管理端末での手順

ALLUSERS=1 を付けて実行 (UAC 昇格が 1 回) → Chrome を再起動。それだけで入る。 HKLM の ExtensionSettings は update_url が releases/latest/download/... の固定 URL なので、 版が上がってもポリシーを書き換えなくてよい。Chrome に「組織によって管理されています」が出て、 ユーザーは拡張を Chrome から削除できない (アンインストールは「アプリと機能」から MSI を消す)。

HKCU\Software\Policies はユーザー権限で書けないため、ポリシー系は ALLUSERS=1 (HKLM) のときだけ出る。

トラブルシュート

  • chrome://policy に ExtensionSettings が載っていない → ALLUSERS=1 で入れたか。 reg query "HKLM\SOFTWARE\Policies\Google\Chrome" /v ExtensionSettings で単一の REG_SZ に JSON が入っているか。Chrome は再起動したか
  • 載っているが [BLOCKED] + 警告 → 端末が非管理。仕様なので perUser (既定) の手順を使う
  • 「更新」ボタンが失敗する → %LOCALAPPDATA%\Programs\gh-actions-live\update.log を見る。 「native host が見つかりません」なら MSI で入れていない (zip 展開) か、 reg query HKCU\Software\Google\Chrome\NativeMessagingHosts\jp.ippoan.gh_actions_live が無い

リリースサイクル

バージョンは自動採番する。直近の v* tag の patch を +1、tag が無ければ 0.0.1 から。

  • stable: main への push で自動的に次の版を採番し、vX.Y.Z の Release を作る (Latest)
  • auto-merge: PR (non-draft) の CI が全部緑になると、org 標準の reusable (ippoan/ci-workflows/.github/workflows/auto-merge.yml) が squash merge を queue する。 PR を出して緑になれば、そのまま新しい版が公開される
  • dev: PR (non-draft) の CI が dev-<run_number> の prerelease を PR head に打つ。 merge を待たずに MSI を試せる。 版は「その PR が出す予定の stable と同じ」
  • 手動: 任意の版を切りたい場合は Release workflow を workflow_dispatch で 実行し、version に X.Y.Z を指定する

tag は Release 作成時に打たれる。手で tag を push する経路は持たない (二重採番になるため)。 manifest の version は tag から stamp されるので、repo に入っている値は開発用の目安。

リポジトリ構成

extension/          MV3 拡張本体 (これを Chrome に読み込む)
installer/main.wxs  MSI (WiX v4+、perUserOrMachine)。配置 + 自動更新タスク + (ALLUSERS=1 で) Chrome ポリシー
installer/update.ps1 更新スクリプト (native host / 手動 / 任意でタスク登録)
installer/host.ps1   native messaging host (「更新」ボタンの実体)
bridge/             Claude Code (Linux) 側の常駐リレー (Rust / systemd --user。下記)
mods/pr-bridge-watch Claude Mod: PR を作ったら branch の CI を bridge の /watch に繋ぐ (下記)
.claude-plugin/     上の mod を配る plugin marketplace

既知の制約

  • 未テスト。alive のプロトコル (接続・購読・ack)、認証境界、partial のサイズは 実機で確認済みだが、拡張として読み込んだ状態での動作確認はまだ。
  • push ペイロードの形は未確認。生サンプルを 50 件まで保存して設定画面に出すので、 形が分かったら ws.onmessage を絞り込める。
  • data-channel のトークンは発行時刻 t 入りの時限。20 分ごとにページを取り直して更新する。
  • 未文書の内部プロトコルなので、GitHub 側の変更で黙って壊れうる。

Claude から設定を入れる (github.com 経由)

拡張は externally_connectable で https://github.com/* からのメッセージを受ける。 Claude in Chrome は github.com のタブで JS を実行できるので、設定画面を触らずに そこから設定を流し込める (bridge URL が未設定でも届く):

// github.com のタブで
chrome.runtime.sendMessage('oaadakmclelmnaieokjbhldfacfckaaj',
  { command: 'set-config', repos: ['owner/repo'], bridgeUrl: 'ws://host:8799', notify: false },
  r => console.log(r));
// command: get-config / open-dashboard / access-login / update / check-update / native-ping / status / alive-reset / reload も同じ経路で使える

Claude Code への途中通知 (bridge)

拡張は GitHub を見ているだけなので、そのままでは Claude Code (別マシン) に何も届かない。 bridge/ (Rust の常駐サーバー gh-actions-bridge) を Linux 側で 1 本だけ 動かし、拡張からそこへ outbound で WebSocket を張ると双方向になる。Claude Code の各セッションは bridge に繋ぐだけで、 bridge 自体を起動しない (セッションが Monitor で起動していた頃は 8799 を取り合い、起動したセッションが 終わると bridge ごと落ち、全 repo の変化がそのセッションにだけ流れていた)。

Windows Chrome 拡張 ──ws://<linux>:8799/?role=extension──▶ gh-actions-bridge (systemd --user)
        ▲                                                        │ ws /watch?repo=…&run=…
        └──── {"type":"command",...} ◀── POST /cmd               ▼ (条件に合う run だけ、1 フレーム 1 行)
                                                       Claude Code の各セッション (Monitor の ws ソース)

導入 (Linux 側、1 回だけ):

cargo install --path bridge
cp bridge/gh-actions-bridge.service ~/.config/systemd/user/
systemctl --user enable --now gh-actions-bridge

bridge 自体の出力 (全 repo の変化・接続ログ) は journalctl --user -u gh-actions-bridge -f。

  • 見張り (/watch): セッションごとに条件を付けて繋ぐ。一致した run の変化だけがテキストで届く

    Monitor({ ws: { url: "ws://127.0.0.1:8799/watch?repo=ippoan/rust-alc-api&workflow=CI&run=1619" }, ... })
    

    条件は repo / ref / run / workflow (部分一致・大小無視) / by。同じ key を重ねると OR、違う key は AND。 条件なしは 400 (全量が流れるのを防ぐ。本当に全部見るなら all=1)

    • 繋いだ時点の状態を bridge の写しから出す (進行中の run と、run 指定の run)。 既に終わった run を指定しても待ちぼうけにならない
    • run を指定すると、その run が全部終わった時点で bridge が socket を閉じる (= Monitor の見張りが終わる)。 run 番号は workflow ごとなので workflow と併用する。re-run は同じ番号で走り直すので、閉じた後に re-run したら繋ぎ直す
    • ref は完全一致 (拡張が 40 文字で切るので長い branch 名は先頭 40 文字で比べる)。ref だけの見張りは閉じない
    • 拡張 (ダッシュボード) が bridge から外れた / 戻ったときも 1 行出す (目隠しを黙らない)。bridge が落ちれば socket が閉じる
  • 生の JSON が欲しい購読者は /?role=listener (繋いだ時点で bridge の写しを snapshot として 1 回受ける。送った JSON は拡張へのコマンド)

  • Linux → 拡張 (設定): curl -X POST localhost:8799/cmd -d '{"command":"set-config","repos":["owner/repo"],"notify":false}' で repo を変えられる。get-config で現在値を返す。repos は全セッション共通なので、 自分の PR に絞るのは set-config ではなく /watch の条件で

  • Linux → 拡張: curl -X POST localhost:8799/cmd -d '{"command":"open-dashboard","mode":"popup"}' でウィンドウを遠隔で開ける。refresh / snapshot も受ける

  • Cloudflare Access のログイン承認: curl -X POST localhost:8799/cmd -d '{"command":"access-login","url":"<cloudflared access login が出した URL>"}' で承認ページを前面のウィンドウに開く。トークンを取りに行くのは Linux 側の cloudflared 自身なので、 ブラウザは「Approve」を押すだけでよく、Windows の Chrome で承認しても通る。 bridge には認証が無く、8799 に届く者が Chrome で任意のページを開けると capability の穴になるので、 開ける URL は 3 条件を全部満たすものだけ:

    1. スキームが https:
    2. ホストが accessLoginHosts に完全一致 (後方一致・ワイルドカードは無し。 endsWith('.ippoan.org') 型は evil-ippoan.org のような取り違えを招くため)。 未設定・空配列なら全部拒否 (deny-by-default) — 設定を入れ忘れた環境が一番危険になる既定は採らない
    3. パスが /cdn-cgi/access/cli ちょうど (new URL() が .. を畳んだ後の値で完全一致)

    加えて userinfo 付きの URL は無条件で拒否する。https://[email protected]/... は文字列としては正規のホストに見えるのに実際に開く先は evil.example.com で、 URL.host に userinfo は入らないためホスト検査だけでは防げない (なりすまし)。

    許可ホストの設定 / 確認:

    curl -X POST localhost:8799/cmd -d '{"command":"set-config","accessLoginHosts":["dtako.ippoan.org"]}'
    curl -X POST localhost:8799/cmd -d '{"command":"get-config"}'   # accessLoginHosts が返る
    

    弾いた URL はログにも応答にも全体を出さない (token= の nonce が乗っている。出すのはホスト名とパスまで)

  • 診断: curl -X POST localhost:8799/cmd -d '{"command":"status"}'。ダッシュボードからは {"type":"status", alive:{connected, fails, lastState, lastMessageAt, idleMs, idleLimitMs, idleResets, watchdogArmed, idleArmed, reconnectPending, background:{state, lastMessageAt, idleMs, relay:{readyState, tokens, lastFrameAt, sinceLastFrameMs, frames}}}}、 service worker からは {"type":"ack","command":"status", alive:{tab, state, lastMessageAt, idleMs, relay}} が返る (relay.readyState は 0=CONNECTING 1=OPEN 2=CLOSING 3=CLOSED、null = socket 無し)

  • connected:true は「生きている」の証明にならない (#28)。half-open (TCP が静かに死ぬ) だと readyState は OPEN のままで ws.send() も通るので、購読し直しは成功したように見える。 生死は フレームを受け取った時刻 で見る: alive.idleMs (ダッシュボードが最後にフレームを受けてからの ms) と alive.background.relay.sinceLastFrameMs (socket を持つ github.com のタブ側の実測)。 idleMs が idleLimitMs (既定 10 分) を超えるとダッシュボードが自分で close → connect で張り直し、 idleResets が 1 増える。ダッシュボードのヘッダにも「最終受信 HH:MM:SS」として出る

  • 強制再接続: curl -X POST localhost:8799/cmd -d '{"command":"alive-reset"}'。relay の socket を閉じ、 ダッシュボードが張り直す (閉じていれば開く)。status が connected:false のまま戻らないときに

  • 拡張側は 設定画面の「Linux 側リレーの URL」 に ws://<host>:8799 を入れる

  • service worker も 1 本張っていて、ダッシュボードが閉じていても open-dashboard を受けられる (リレーの 20 秒 ping → pong の往来で MV3 の service worker が生き続ける)

  • 認証は無い。tailnet / LAN 内で使う前提。外に出すなら前段に Access 等を置く

PR を作ったら bridge に自動で繋ぐ (Claude Mod)

mods/pr-bridge-watch は Claude Code の function hooks (製品名 Claude Mods、early access) で書いた plugin。 Bash の tool.call を包み、gh pr create / pr-push.sh が成功して PR の URL が出たら:

  1. gh pr view <url> --json headRefName で branch を取る (失敗したら command の --head)
  2. bridge の GET / で生死を見る
  3. $.tool.call({ tool: 'Monitor', ws: { url: 'ws://127.0.0.1:8799/watch?repo=…&ref=<branch>' }, persistent: true })
  4. 結果に context を 1 行足す (「繋いだ (task …)。gh run list で polling しない」)

bridge が落ちている / Monitor が拒否されたときは張らずに、自分で張る Monitor(...) の引数を context に書く (見張りが黙って欠けるより model に拾わせる)。同じセッションで同じ branch は二度張らない。

張らない環境が 2 つある。どちらも「自分で張れ」は返さず、1 行の事実だけを context に足す:

  • Claude desktop (Code タブ) では張ろうとしない (CLAUDE_CODE_ENTRYPOINT が claude-desktop)。app 側が PR の CI を 監視していて、model が Monitor で CI を見張ることを禁じているため。bridge も gh pr view も叩かない
  • auto mode では張れない。 hook の発した Monitor に分類器が判定を返さない (gave no verdict)。 model に張らせる指示は出さず、「CI は PR の監視か gh pr checks で確認する」とだけ返す

Monitor は archive の前に止める。 ref の /watch は bridge が閉じないが、archive_session は生きた background task を持つセッションを畳まない (still has live work)。放っておくと見張りが archive を塞ぐので:

  • Actions の通知 (task-notification) が来るたびに全部の見張りを突合し、local の branch (git --git-dir=<共通 dir> show-ref refs/heads/<ref>) が消えていたら TaskStop。 PR は親セッションが作るので、子の archive → worktree-janitor で branch が消える → 親に次の通知が来た時点で止まる。 gh の polling や timer は持たない
  • archive_session { session_id: "self" } の tool.call を包み、先に全部の Monitor を止めてから通す
  • 他のセッションの archive が live work で断られたら、相手へ [pr-bridge-watch:stop] 入りの send_message を送って 3 秒後に 1 回やり直す。受けた側の mod は session.receive (届かなければ prompt.submit) で要求を飲み込み (turn を起こさない)、全部止める。子が自分で PR を作ったときの経路。pinned 等の拒否ではやり直さない

task ID (Monitor の結果) が取れないとき・PR の branch が local に無いときは自動では止まらないので、context にそう書く。 installed plugin は起動中のセッションでは読み直されない (更新は新しいセッションから効く)。

導入 (Claude Code 2.1.260 以上。function hooks は既定 off):

# ~/.claude/settings.json の env に "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" (起動中のセッションにも効く)
claude plugin marketplace add ippoan/gh-actions-live
claude plugin install pr-bridge-watch@gh-actions-live

bridge が別ホストなら userConfig の bridgeUrl (既定 ws://127.0.0.1:8799) を変える。

開発: 型は build ごとに生成する (commit しない。mods/*/types/ は gitignore)。

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude -p "/plugin-types mods/pr-bridge-watch/types"
tsc -p mods/pr-bridge-watch/tsconfig.json
claude plugin validate mods/pr-bridge-watch
npm test    # test/pr-bridge-watch.test.mjs が偽の $ で register を回す (Node 24 の type stripping で .ts を直接 import)
查看原始来源 ↗

更多类似作品