GitHub
GitHub リポジトリでセッションを動かし、承認した結果をブランチへ push し、そのブランチで PR を開く
GitHub との間でやることは三つある。開始リポジトリがあるセッションは開始時に、ないセッションはエージェントが attach したときにリポジトリを clone する。人が承認した commit を agent/<session-id> ブランチへ push する。そのブランチで PR を開き、コメントを付ける。clone は worker が API のプロキシ経由で行い、push と PR はプラットフォーム(API)が直接行う。どちらの場合も、リポジトリの認証情報は worker に渡らない。
運用者がやること
認証情報は GitHub App で、owner か admin がコンソールの 設定 > ワークスペース > 連携 > GitHub で扱う。
- App を追加: GitHub で新しい App を作成する(コンソールが App ID と鍵を受け取って保存する)か、既存の App を App ID と private key(PEM ファイル)で接続する。作成用 App とレビュー承認用 App を複数置けて、一覧では役割のバッジで見分ける。鍵は暗号化して保存し、画面にも API の応答にも二度と出ない。鍵を替えるときは App の行の ⋯ メニューの 新しい鍵を登録 を使う。
- 組織を追加: App を組織か個人アカウントにインストールする。インストールの表にアカウントごとの種類・リポジトリの範囲・App・接続した人・状態が出て、行の ⋯ メニューの GitHub で管理 がそのインストールの設定画面を開く。
- リポジトリを管理: エージェントが選んだリポジトリをインストールごとに見て、デフォルトブランチとそのリポジトリを担当する App を変える。リポジトリはエージェントの GitHub タブで選ぶとでき、別に登録はしない。
- エージェントが使える機能: ワークスペース全体の上限である。スイッチを変えるとすぐに保存される。有効にした機能に App の権限が足りないと、その App の行に必要な権限と GitHub で権限を追加 が出る。権限を追加すると GitHub がインストール先のアカウントごとに owner へ承認を依頼し、再インストールは要らない。
- 自分の PR のマージ: 既定はオフである。オンにすると、人がマージを指示したとき、エージェントは自分のセッションが開いた PR を squash でマージする。必須チェックとレビューは GitHub が判断し、マージ要求は ruleset の回避をオフにして送る。変更できるのは owner と admin だけで、ワークスペースの GitHub 設定 API(
PUT /v1/workspaces/{id}/github-settingsのceiling.merge)で行う。オンにしても新しいリリースは要らず、次の呼び出しから効く。
コンソールで新しい App を作るには、API に CONSOLE_ORIGIN(ブラウザがコンソールを開く origin。例: https://werft.internal.example)が必要です。GitHub が App を作った後、このアドレスにブラウザを戻すためです。ないと、GitHub App を作成 から開く 新しい App を作成 画面で GitHub で作成 ボタンが無効になり、「CONSOLE_ORIGIN が設定されていないため、GitHub がコンソールに戻る先がありません」という案内が出ます。運用者に値の設定を頼むか、既存の App を接続 を使います。単一サーバーでは env ファイルに入れて upgrade で再適用します(単一サーバー)。GitHub が passkey の確認(sudo mode)を求めたときは、その画面で App が作られず、空の App フォームに移ることがあります。確認を済ませてコンソールに戻り、GitHub で作成 をもう一度押します。
| 方式 | 必要な権限 |
|---|---|
| GitHub App | Repository permissions: Contents Read and write, Pull requests Read and write |
Workflows 権限は「ワークフローファイルの変更」機能を有効にするときだけ App に加える。push される commit はエージェントが書いたものなので、この権限があるとエージェントが書き換えた .github/workflows/ のファイルも push され、CI で実行される。デフォルトブランチはリポジトリの ruleset でも保護しておく。設定方法は運用ドキュメントにある。
App の Setup URL
コンソールで作った App は、Setup URL が <CONSOLE_ORIGIN>/settings/workspace/integrations/github で入る。すでにあった App を連携した場合は、GitHub の App 設定(Developer settings > GitHub Apps > その App > General)で Setup URL をこの住所に合わせる。組織を追加すると GitHub がブラウザーをこの住所へ戻し、コンソールは戻ってきた値でインストールを記録する。Setup URL が古い /settings/workspace/github を指している App は新しい住所に変える。その経路はなくなった。すでにインストール済みのアカウントで、インストールの設定を保存したあとコンソールへ戻るのは、App の Redirect on update がオンのときだけである。
ユーザーがやること
- そのリポジトリの
repository_idでセッションを作る(開始)。 - 結果をリクエストし、承認する。リクエストが
pushedになると、リポジトリにagent/<session-id>ブランチができる。 - PR を開く。
sessions:approveが必要である。下の例はリクエストと承認のsessionIdとpush(HTTP API はSID、PUSH_ID)を使う。
const opened = await werft.sessions.pushRequests.openPullRequest(sessionId, push.id, {
title: "Add release notes",
body: "Written by the agent, approved by me.",
});
console.log(opened.pull_request.number, opened.pull_request.url, opened.created);この呼び出しには Idempotency-Key がない。そのため SDK は、レスポンスを受け取れなかった場合に再送しない(リトライと Idempotency-Key)。head は agent/<session-id>、base はカタログに書いたブランチである。リクエスト一つにつき PR は一つである。そのブランチの PR がすでに開いている場合は、新しく開かずにその PR を返す(created: false)。この呼び出しは PR を承認・マージせず、レビュアーやラベルも決めない。
- コメントを付ける。同じ
marker(デフォルトはstatus)のコメントがすでにある場合は、新しく付けずにそのコメントを書き換える。
await werft.sessions.pushRequests.commentPullRequest(sessionId, push.id, {
body: "Tests pass on the agent branch.",
});タイトル・本文・コメントは GitHub に送る前に整形される。秘密情報に見える行は伏せ、@メンション や Closes #N のような close キーワードは、通知や自動クローズが起きないように置き換える。
PR 本文の末尾には、プラットフォームが書く末尾ブロックが付く。セッション id とコミット、セッションのアドレス、エージェントとリリース、誰がどの経路で開始したか、その push を依頼した人(Requested by。通知が飛ばないよう @ なしで login だけを書く)が入る。そのブランチにもう一度 push して PR をもう一度要求すると、末尾ブロックだけが新しいコミットに変わり、その上に人が書いた本文はそのまま残る。
セッションの中のエージェントは、組み込み MCP ツールで同じことをする。open_pull_request で開き、update_pull_request でタイトルと本文を直し、comment_pull_request でコメントする。close_pull_request と reopen_pull_request で、このセッションの push 要求が開いた PR をマージせずに閉じ、また開く。人の承認は受けない。ほかのセッションの PR や、許可されていないリポジトリの PR は閉じられない。閉じた PR を閉じたり、開いている PR を再オープンしたりすると、何も変えずに changed: false で答える。マージ済みの PR は PULL_REQUEST_MERGED で拒否する。閉じても agent/<session-id> ブランチは消さない。そのため再オープンできる。
ワークスペースが許可していれば、merge_pull_request でその PR を squash でマージする。許可していなければ、GitHub を呼ぶ前に GITHUB_REFUSED(merge_off)で拒否する。マージは push 要求が push した commit に固定する。PR の head がその commit でなければ head_moved で、必須チェックの待ち・レビュー待ち・未解決の会話・競合など GitHub が今はマージしない PR は merge_blocked で拒否する。この拒否の details には、GitHub が返した状態(mergeable_state、mergeable、draft)と、どの条件が満たされなかったかを述べる reason の文が載る。reason は状態を述べるだけで、どの必須チェックやレビューかまでは述べない。開いたときと別の base に移された PR、または base が別の開いている PR の head である PR(積まれた PR)も merge_blocked である。GitHub の非同期マージは、その PR が積まれている下の PR も一緒にマージするためである。マージ要求を GitHub が受け付けても待つ間に終わらなければ forge_failed で答える。マージはあとから終わることがあるので、PR を確かめてからもう一度呼べばよい。すでにマージ済みの同じ commit は changed: false で答える。同じ commit ですでにマージ済みの PR には changed: false で答える。管理者による回避、自動マージ、ほかのマージ方式は使わない。権限確認のないモード(bypassPermissions)のセッションは、オンの間は指示がなくてもマージできる。マージ要求はルールの回避をオフにして(bypass_rules: false)GitHub の非同期マージ API に送るため、作成用 App がルールセットの回避対象でも、その権限で必須チェックとレビューを飛ばせない。それでもマージ用の App は回避対象に入れないほうが安全である。エージェントの Actions 許可リストに PR をマージするワークフローを入れると、このスイッチはそのマージを止められない。
push の後に PR が作られる仕組み
push リクエストに PR のリクエストが付いていると、push が pushed で終わった後に control-host の中の action-broker が PR を作成します。エージェントが続きのターンで open_pull_request をもう一度呼ぶ必要はありません。この処理は control-host の起動と一緒に始まり、再起動しても、すでに始まっている PR の段階は最後まで処理されます。REST では、push が終わる前に POST /v1/sessions/{id}/push-requests/{push_request_id}/pull-request?schedule=true を呼ぶと、409 PUSH_NOT_PUSHED の代わりに 202 と status: "scheduled" が返り、push が終わり次第 PR が作成されます。schedule クエリがない場合や、従来フロー(flow = legacy)で作成した push リクエストの場合は従来どおり 409 です。
PR の段階は push リクエストごとに 1 つで、状態は次のように変わります。
| 状態 | 意味 |
|---|---|
due | push が終わり、PR を作る番です。作成に失敗すると間隔を空けて再試行します(最大 3 回) |
opening | action-broker の 1 つが作成中です。その処理が止まると別の replica が引き継ぎます |
opened | PR が作成され、記録されました。番号とリンクは PR の行にあります |
closed_unrecorded | この push リクエストの PR が、プラットフォームが記録する前にクローズまたはマージされました。新しい PR は作りません |
uncertain | PR があるかどうかを確認できませんでした(照会が 3 回失敗したか、最初の照会が上限の 10 件で埋まり、次のページにまだあるかもしれません)。または当てはまる PR が複数あります。プラットフォームは選びません |
failed | PR を作成できませんでした。理由は pr_error に残ります |
cancelled | push が失敗したため、作成する PR がありません。理由は pr_error に残ります |
作成の前に、プラットフォームはブランチをすべての状態(オープン・クローズ・マージ済み)で先に探します。応答を失った後で再試行しても PR がもう 1 つ増えないのはこのためです。すでにオープンの PR があればそれを使い、末尾だけを新しい commit に書き換えます。見つけた PR や新しく作った PR の head が push した commit と違えば、何も記録せず head_moved で終わります。ただし GitHub は push 直後にしばらく古い head を返すことがあるため、その head が push した commit の祖先であれば終わらせず、次の試行まで待ちます(最後の試行の前まで、比較リクエストは 1 回)。祖先でない場合や比較が失敗した場合は、その場で終わります。作られた PR は閉じないので、人が確認します。PR 本文の目印は誰でも書けるため、同じリポジトリ・head・base の PR だけを候補にします。
結果はセッションのスレッドに通知として届きます(作成した、作成できなかった、すでにクローズ済み、確認できなかった)。文言はセッションを始めた人の言語に合わせ(依頼した人ではありません)、結果ごとに 1 回だけ作られます。通知は、セッションが Slack のスレッドにつながっていて、ミュートや権限の制限がないときだけ作られます。そうでなければ状態だけが変わり、通知の行はできません。通知が作られる場合は状態を変えるトランザクションと同じトランザクションで記録されるため、状態だけが変わって通知が抜けることはありません。
既知の制限があります。プロセスが呼び出しの直前に長く止まり、すでに送ったリクエストが遅れて届くと、同じブランチに PR がもう 1 つできることがあり、プラットフォームは作成後にそれを探しません。GitHub には条件付き作成がないため、プラットフォームは呼び出しの直前に再確認し、作成の前にブランチを探すところまでしかしません。作成時に当てはまる PR が複数あれば、新しく作らず uncertain のままにして通知します。自動ではクローズしないので、GitHub で人が選びます。
セッションの途中でリポジトリを付ける
GitHub 設定があるエージェントのセッションはリポジトリなしで始まる(設定がない古いエージェントはリリースのリポジトリで始まる)。エージェントは作業の途中で、タスクが読むか変更するリポジトリだけを、内蔵 MCP ツール attach_repository({ repository_id }) で付ける。付けられるのは、エージェントのリリースの GitHub 設定(github.repositories)にあり、プロファイルが許可するリポジトリだけだ。人の承認は受けない。そのリポジトリへの書き込みは、これまでどおり push 要求の承認を通り、付けたリポジトリへの push が人の承認なしで進むのは、push 承認を切り、付けたリポジトリの push ポリシーも allow にしたエージェントだけである(どのリポジトリにも同じルール)。付けられるリポジトリはエージェントが list_repositories で見る(付けたもの attached、取得中か失敗したもの pending・failed、まだ付けていないもの not_attached。セッションを作るときに repository_id で指定したリポジトリは attached と表示される)。セッションは始めたときのリリースを使い続ける。そのため、あとで有効なリリースにだけ加えたリポジトリは allowed_in_newer_release として別に表示され、attach_repository も repository_not_allowed ではなく allowed_in_newer_release で拒否する。もう一度許可する必要はなく、新しいセッション(Slack では新しいスレッド)で付けられる。エージェントの設定で動き、付けたリポジトリの一覧を持つセッションは異なる。開始後にエージェントが一覧でオンにしたリポジトリはすぐに付けられ、一覧から外すか GitHub をオフにすると、付けていたリポジトリ(セッションを作るときに指定したリポジトリを含む)は revoked と表示され、プラットフォームはエージェントによるそのリポジトリへの GitHub 操作と、他のリポジトリの新しい attach を拒否する。セッションを作るときに指定したリポジトリへの attach は何もせず、すでに付いているものとして答え、プラットフォームが記録済みの PR を読む処理は続く。作業領域のコピーは削除せず、再びオンにすればそのまま使える。
セッションを作るときに attached_repository_ids を送らなければ、何も付けずに始まる(repository_id を送ったセッションにはそのリポジトリだけがある)。最初から必要なリポジトリはその一覧に入れ、残りはエージェントが必要なときに付ける。push 要求は 1 つのリポジトリを送り出すので、リポジトリが複数あるときはリポジトリごとに 1 回ずつ出す。
attach が接続エラー(upstream connection failed の応答、または HTTP 502・503)で失敗した場合、それは一時的な失敗で、リポジトリについては何も示していない。同じ引数でやり直しても安全である。repository_not_allowed は接続エラーではなくエージェント設定による拒否なので、やり直しても同じ答えになる。
- 付けたリポジトリは、作業スペースで名前が
repository_idの git リモートになり、デフォルトブランチを<repository_id>/<branch>に取得する。エージェントはトークンを見ない。 - 同じリポジトリをもう一度付けても何も変えずに成功する。取得中なら
pendingとfinal: falseをすぐ返す。wait_seconds(0〜7 秒、既定 0)を渡すと、取得が終わるかその秒数が過ぎるまで待ってから答え、終わっていればfinal: trueになる。 - 取得には 300 秒と 2 GiB の上限がある。超えると付けるのは失敗で終わり、
failureに理由(fetch_timeout、fetch_too_large)が残る。 - 付けるたびに
attach_repositoryのレシートが残る。要求した人と attempt、リポジトリ、リリース、取得したコミット、拒否の理由(repository_not_allowed・allowed_in_newer_release)が入る。 - セッションが別の worker で再開すると(restore)、付けたリポジトリのリモートをもう一度付ける。付けたリポジトリの CLAUDE.md は指示として読まない。
自分の GitHub アカウントを連携する
コミットの作成者は常に GitHub App の bot アカウント(<App slug>[bot])だ。自分の GitHub アカウントを連携しておくと、自分が依頼したターンのコミットに Co-authored-by: トレーラーで自分の名前も入り、GitHub でそのコミットが自分の貢献としても表示される。
- owner か admin が 設定 > ワークスペース > 連携 > GitHub の「メンバーの GitHub アカウント連携」の行で、連携に使う作成用 App を選び(App が複数あるとき)、⋯ メニューの callback URL をその App の Callback URL に入れ、その App で作った client secret を入力する。この準備がないと連携ボタンは無効になっている。
- 設定 > 連携アカウント の GitHub の行で「連携」を押し、GitHub の同意画面で承認する。プラットフォームは GitHub アカウントの id と login だけを読み、GitHub のトークンは保管しない。
- 連携した行に、コミットに入る行(
Co-authored-by: <login> <<id>+<login>@users.noreply.github.com>)が表示される。連携を解除すると、それ以降に依頼するターンからは入らない。
Web コンソール、個人 API キー、Slack から依頼したターンは、そのターンを依頼した人の連携に従う。一つのセッションで複数の人が続けて依頼した場合、コミットごとにそのターンの依頼者が入る。エージェントキーや運用者キーで依頼したターンには依頼した人がいないため、共同作成者は入らない。どちらの場合も、コミットには Agent-Session: トレーラーが付く。運用者がコンソールのアドレスを設定した環境ならセッションのアドレス、そうでなければセッション id だ。セッション詳細の成果物タブの最後の行で、そのセッションがどちらかを確認できる。
GitHub アカウント1つは1人にだけ連携される。他の人がすでに連携したアカウントは連携できない。
ローカルスタックでは
ローカルの sample-app はスタック内の Git サーバーに GitHub App として登録されているため、承認した push は pushed になり、PR も開ける。push されていないリクエスト(拒否したものなど)に PR を要求すると 409 PUSH_NOT_PUSHED になる。承認で rejected として終わったリクエストで確認する。
import { isApiError } from "@werft-ai/sdk";
const requests = await werft.sessions.pushRequests.list(sessionId);
const rejected = requests.find((request) => request.state === "rejected");
if (rejected) {
try {
await werft.sessions.pushRequests.openPullRequest(sessionId, rejected.id, {
title: "Add release notes",
});
} catch (error) {
if (!isApiError(error)) throw error;
console.log(error.status, error.code);
}
}409 PUSH_NOT_PUSHED| レスポンス | 意味 |
|---|---|
409 PUSH_NOT_PUSHED | リクエストがまだ pushed ではない |
409 PULL_REQUEST_MISSING | コメントの前に PR を先に開く必要がある |
409 CATALOG_MISMATCH | リクエストの後に運用者がリポジトリ設定を変更した |
403 FORBIDDEN | sessions:approve がない、セッションを見ているだけで承認する人ではない、または GitHub ではないリポジトリである(no_forge) |
503 BACKEND_UNAVAILABLE | GitHub 呼び出しがリトライの後も失敗した |