Cursor Originの3本目です。前編でOriginが何かを、後編でchangeモデルと「PRごとのチェックはAppsとrulesetで組む」という設計を書きました。この記事は、その設計を実験リポジトリのPull Requestで実際に組んだ実録です。CIを載せ、rulesetで必須化し、自動マージの手前まで一晩で通しました。正式リリース当日の話なので、細部は変わるかもしれません。
CIの選択肢は実質3つ
Origin本体にCIの実行基盤はありません。選択肢は次の3つです。
1つ目と2つ目はApps経由で、BuildkiteとDepotです。どちらも既存のGitHub Actionsワークフローを実行でき、Buildkiteはネイティブパイプラインも書けます。役割が同じなので、両方入れる必要はありません。注意点として、AppsはGitHubからミラーリングしたリポジトリでは使えません。Originでホストしているリポジトリ専用です。
3つ目が、この記事で選んだ内部アプリ(Internal Apps)とOrigin APIのchecksエンドポイントです。CIの実行は手元(またはGitHub Actionsのまま)にして、結果の報告だけ自前で組みます。外部ベンダーのアカウントが要らず、仕組みが全部見えるので、最初の1本にはこちらが向いています。
最初の発見。checksは人間のトークンでは書けない
近道を探して、ログイン済みCLIの origin api からcheck-runを直接POSTしてみました。結果は403です。
{"code":7,"message":"This Origin endpoint does not support user principals"}
checksの書き込みはアプリ(installation token)専用で、人間のトークンでは書けません。つまり、ログインユーザーが手元から「CI通ったことにする」を偽装できない設計です。CIステータスの書き込み主体をアプリに限定して、誰が書いたかを追える形にしている。後編で「本当の差別化は権限と監査に出る」と書きましたが、その芽がこんなところにありました。
内部アプリを作る
手順は4つです。まずローカルでEd25519のキーペアを作ります。
openssl genpkey -algorithm ED25519 -out origin-app-private.pem
openssl pkey -in origin-app-private.pem -pubout -out origin-app-public.pem
Cursorへ渡すのは公開鍵だけで、秘密鍵はローカルに残ります。次に、Codebase SettingsのAppsで + New から内部アプリを作ります。Slugは必須(local-ci-reporter のような小文字ハイフン形式)、Webhook URLやRedirect URIは受け口サーバーを立てないなら全部空欄で構いません。権限は最小の3つにしました。Read repository metadata(必須で外せない)、Read check suites and runs、Create and update check suites and runs です。
作成画面で公開鍵PEMをsigning keyとして登録し、最後にアプリをリポジトリへインストールします。ここでひとつ引っかかりました。アプリを作っただけではinstallationが空(APIが 200 {} を返す)で、Apps画面からのインストール操作をして初めてtokenが取れるようになります。
check-runを報告する
認証は3段のチェーンです。秘密鍵でApp JWTを署名し(alg: EdDSA、aud: origin-apps、寿命約5分)、そのJWTで installation token(oit_、最長15分)を取り、tokenでcheck-runをPOSTします。
エンドポイントは POST /v1/origin/repos/{owner}/{repo}/check-runs で、upsert型です。同じ (リポジトリ, head SHA, suite.key, run.key) への再送は上書きになり、リトライしても重複しません。externalId は試行ごとに新しい値、externalUpdatedAt は増加するタイムスタンプにして、古いリトライが新しい状態を上書きしないようになっています。細かい罠として、checkRun.externalId を忘れると400で弾かれます。
この一連をNode標準ライブラリだけの小さなスクリプトにして、ローカルで xcodebuild test(全テストパス)を回した結果をPOSTしました。そのまま origin pr checks に載ります。
CI checks:
- Local CI (Local CI Reporter)
- xcodebuild test (macOS): completed (success)
suiteの表示名が「Local CI」、その下に個々のcheck runが並ぶ構造です。Web画面のChecksタブにも同じものが出ます。
rulesetで必須化する
checksが一度報告されると、rulesetの「Require status checks to pass」のAdd checksで選べるようになります。逆に言うと、1回も報告していないcheckは選択肢に出ないので、順番は「先に1回流す、それからrulesetで選ぶ」です。
今回は「承認1件」と「Local CIの成功」の両方を必須にして保存しました。その瞬間からマージがブロックされます。
Mergeable: no
CI passing: yes
Meets approvals: no
Rules verdict: blocked
Rule evaluations:
- Protect default branch / pull_request: fail — Pull request review requirements have not been satisfied.
CIは緑、足りないのは承認だけ、という状態です。素のOriginはready後のマージを何も止めないので、rulesetが唯一の関所になります。
自己承認はできない
ここで、後編に「未確認」と書いていた疑問に答えが出ました。作者による自己承認を試したところ、こう拒否されます。
Error: Pull request authors cannot approve their own pull request
GitHubと同じ仕様です。つまり一人運用のリポジトリで「承認1件必須」にすると、誰も承認できずマージ不能になります。対処は、rulesetから承認要件を外してstatus checksだけにするか、チームの別アカウントで承認するかの二択です。個人開発とエージェントの組み合わせだと「書くのは自分とエージェント、承認できる人間がいない」構図は普通に起こるので、承認の主体にエージェント(Bugbotのようなレビュアー)を立てられるようになるかが、次の論点だと思います。
修正、push、versionは勝手に切られる
rulesetを組んだあと、レビューで見つけた不具合の修正を1件流しました。ここで後編に書いた罠をもう一度踏んでいます。修正をコミットして「pushした」つもりが、行き先はGitHubの origin で、Originの cursor remoteには届いていませんでした。remoteが2つある運用では、pushの行き先は毎回明示するに限ります。
git push cursor <branch> をやり直すと、Originはpushを検知して新しいversionを自動で切ります。refresh を呼ぶ必要はありません。あとは新しいhead SHAでテストを回し、同じsuite/runキーで報告するだけです。
git push cursor refactor/routing-source-text-window # version 3が自動で切られる
xcodebuild test ... # ローカルCI
node scripts/origin-report-check.mjs --sha <新しいSHA> ... # 新SHAへ報告
Versions:
#1 cdbb87defbf5 ← 6affc8d542b4
#2 ced866f66504 ← 6affc8d542b4
* #3 d218a1f88ccd ← 6affc8d542b4
rulesetの「Selected checks must pass on the latest changes」は最新版に対する判定なので、報告し直せばCI passingが緑に戻ります。修正、push、版が切られる、テスト、報告。このループが日常の形になります。
merge-when-readyで締める
最後に origin pr merge --auto --squash を実行しました。
Merge-when-ready enabled for pull request #1.
The server merges automatically once requirements pass.
この時点ではまだ承認要件が残っていてマージされません。rulesetを編集して承認要件を外すと、人間がマージボタンを押さなくても、条件が揃った瞬間にサーバー側がマージします。(投稿前に: 実際にマージが走ったことを確認してここに1行追記)
一晩でできた、が正直な感想
まとめると、今夜組めたのはこういう構成です。CIの実行はローカル(この先はGitHub Actionsに載せ替え可能)、結果の報告は内部アプリ経由でchecks API、必須化はruleset、マージはmerge-when-ready。外部CIベンダーなしで、GitHubの「チェックが通らないとマージできない」と同じ状態がOriginにできました。Web画面での操作はアプリの作成とインストール、rulesetの編集だけで、それ以外は全部CLIとAPIで完結しています。
知見を並べておきます。
- checksの書き込みはアプリ専用。人間のトークンは403で、CIステータスの偽装ができない
- check-runはupsert型。
suite.key+run.keyが同一性で、rulesetの必須checkもこのkeyに紐づく - rulesetのAdd checksは、一度報告されたcheckしか選べない。先に1回流す
- 自己承認は不可。一人運用で承認必須はデッドロックになる
- pushすればversionは自動で切られる。refreshは呼ばなくていい
- 素のOriginは何も止めない。関所はrulesetだけなので、本番につながるAppを入れる前に組む
後編で「エージェントネイティブな機能はまもなく、とリリース文にある」と書きましたが、checksの書き込み主体をアプリに限定する設計や、pushで勝手に積もるversionを見る限り、土台はすでにエージェント前提で作られています。足りないのは承認モデル(エージェントを承認者に立てられるか)で、そこが埋まると、一人と数十本のエージェントでレビュー付きのマージフローが完結するようになります。次はそこを試すつもりです。
前編はこちら: https://qiita.com/Kinopee/items/639d401573c09ab24667
後編はこちら: https://qiita.com/Kinopee/items/747de22525344a796eed



