2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cursor OriginでCIを回す。内部アプリとchecks APIで「チェックが通らないとマージ不可」を作る

2
Posted at

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 です。

内部アプリのDefault App Permissions。metadata read(必須)とchecksのread/writeだけにチェック

作成画面で公開鍵PEMをsigning keyとして登録し、最後にアプリをリポジトリへインストールします。ここでひとつ引っかかりました。アプリを作っただけではinstallationが空(APIが 200 {} を返す)で、Apps画面からのインストール操作をして初めてtokenが取れるようになります。

Codebase SettingsのAppsで、Internalに作ったアプリがInstalledになっている画面

check-runを報告する

認証は3段のチェーンです。秘密鍵でApp JWTを署名し(alg: EdDSAaud: 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で選ぶ」です。

Edit ruleset。Require approving reviews(1 approval)とRequire status checks to pass(Local CI選択済み)、EnforcementはActive

今回は「承認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が唯一の関所になります。

Rページの「Merging is blocked」。checksは緑でEnable Auto-Mergeボタンが出ている状態

自己承認はできない

ここで、後編に「未確認」と書いていた疑問に答えが出ました。作者による自己承認を試したところ、こう拒否されます。

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

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?