✳ClaudeModsClaude Code mod directory
☰
● 0 online · Views 0 times
+ Submit a project
← Back to projects
GitHub repositories · Git & code review

pr-bridge-watch

Watch CI status for new pull requests in real time through a WebSocket bridge.

iippoan@ippoan
Translated

About this mod

Watch CI status for new pull requests in real time through a WebSocket bridge.

This project is maintained by a community author. See the original README below for features, version requirements and configuration. This site provides permission scan results; inspect the source and external dependencies before installing.

Installation

Check the author's README for the marketplace and plugin name first. Commands may change as the repository evolves.

claude plugin marketplace add ippoan/gh-actions-live
claude plugin install pr-bridge-watch
Original text / 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)
View original source ↗

Similar projects