Skip to content

feat(github-app): per-repo Octokit resolution and multi-installation queries (#283 PR 2/7)#296

Merged
coji merged 1 commit intomainfrom
feat/issue-283-query-octokit
Apr 8, 2026
Merged

feat(github-app): per-repo Octokit resolution and multi-installation queries (#283 PR 2/7)#296
coji merged 1 commit intomainfrom
feat/issue-283-query-octokit

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Apr 8, 2026

Summary

Issue #283 の実装 stack PR 2/7 — query / mutation / Octokit 解決層を複数 installation 対応にする。アプリケーションの動作はまだ変えない(fallback で互換維持)。

設計根拠: docs/rdd/issue-283-multiple-github-accounts.md
作業計画: docs/rdd/issue-283-work-plan.md

依存: #288 (PR 1: schema)

変更内容

query 層 (app/services/github-integration-queries.server.ts)

  • getGithubAppLinks(orgId) 配列返却(ORDER BY createdAt ASC で決定的順序)
  • getGithubAppLinkByInstallationId(installationId) 追加
  • assertInstallationBelongsToOrg(orgId, installationId) 追加 — クライアント由来の installationId をサーバ側で検証する境界 guard
  • getGithubAppLink() は最古の active link を返す互換 shim(@deprecated

mutation 層 (app/services/github-app-mutations.server.ts)

  • disconnectGithubAppLink(orgId, installationId) 追加 — 単一 installation の soft-delete + 最後の active を失った時のみ method='token' に戻す + audit log 書き込み
  • 1 transaction 内で完結(idempotent)
  • disconnectGithubApp() は legacy UI 互換 wrapper として 1 transaction で全 link 一括 soft-delete に書き換え(@deprecated

audit log writer (app/services/github-app-link-events.server.ts) 新規追加

  • logGithubAppLinkEvent() helper
  • event_type / source / status の string union 型を export
  • Kysely<DB> | Transaction<DB> を受け取り、呼び出し側のトランザクションに乗せられる
  • PR 1 で追加した github_app_link_events table の 初回 writer(disconnect 経由)

Octokit 解決 (app/services/github-octokit.server.ts)

  • resolveOctokitForInstallation(installationId) 追加
  • resolveOctokitForRepository({ integration, githubAppLinks, repository }) 追加 — repository ごとの解決
    • repository.githubInstallationId がセットされている場合は厳密にそれを使う(suspended は弾く)
    • github_app モードで未割当の repository に対する移行期間 fallback:
      • active link 1 件 → そのまま使う
      • 0 件 → エラー(PAT 自動 fallback はしない、RDD ルール)
      • 2+ 件 → エラー(曖昧、明示的な assignment が必要)
    • token モード: privateToken があれば PAT、無ければ未接続エラー
  • IntegrationForOctokit.method'token' | 'github_app' | (string & {}) のユニオンに型を絞る
  • resolveOctokitFromOrg() は legacy 互換 wrapper(@deprecated、PR 4 で削除予定)

batch shape 更新 (batch/db/queries.ts)

  • getGithubAppLinkByOrgIdgetGithubAppLinksByOrgId(配列返却)
  • getAllGithubAppLinksMap.groupBy で書き換え
  • getOrganization() / listAllOrganizations()githubAppLinks: [] を返すよう変更

crawl / backfill ジョブ (app/services/jobs/{crawl,backfill}.server.ts)

  • 単一 Octokit 共有から per-repository 解決に変更
  • load-organization step 内で github_app + active 0 / token + privateToken null の早期エラー検出
  • repository ループ内で resolveOctokitForRepository() を呼び、解決失敗時は warn ログ + skip(crawl 全体は止めない)

tsconfig

  • libES2024 に bump(Map.groupBy を使うため)

tests

  • app/services/github-octokit.server.test.tsresolveOctokitForRepository の 11 ケース追加
    • 明示 installation id (一致 / 不一致 / suspended)
    • 移行期間 fallback (active 1 / 0 / 2+ / suspended 除外)
    • token モード (PAT あり / なし)
    • integration null

満たす受入条件

Stack 位置

PR 1 (#288): schema
└ [PR 2: query/octokit] ← this PR
  └ PR 3 (webhook/membership)
    └ PR 4 (UI)
      └ PR 5 (repo UI)
        └ PR 6 (backfill)
          └ PR 7 (strict)

後続 PR への影響

  • PR 3: webhook handler 群がここで追加した getGithubAppLinkByInstallationId / logGithubAppLinkEvent / canonical reassignment helper(PR 3 で実装)を使う
  • PR 4: UI loader / action から getGithubAppLink()getGithubAppLinks() に移行 + assertInstallationBelongsToOrg を loader 境界で呼ぶ
  • PR 7: 移行期間 fallback (activeLinks.length === 1 分岐) を削除し、github_installation_id IS NULL を strict エラーにする

テスト

  • `pnpm validate` (lint / format / typecheck / build / test 全 331 tests)
  • `resolveOctokitForRepository` の主要 11 ケースをユニットテストで検証

🤖 Generated with Claude Code

Summary by CodeRabbit

リリースノート

  • 新機能

    • GitHub Appのリンク接続・切断イベントに対する監査ログ機能を追加しました。
  • バグ修正

    • リポジトリごとのGitHub認証解決ロジックを改善し、複数のアクティブなリンク存在時のエラーハンドリングを強化しました。
    • GitHub Appの削除状態をより厳密に追跡するようにしました。
  • 改善

    • GitHub統合に関する検証とエラー処理を堅牢化しました。

…queries (#283 PR 2/7)

query 層:
- getGithubAppLinks() で配列返却
- getGithubAppLinkByInstallationId() / assertInstallationBelongsToOrg() 追加
- getGithubAppLink() は 1 件目を返す互換 shim として deprecated 維持

mutation 層:
- disconnectGithubAppLink(orgId, installationId) を追加
- 最後の active link を失った時のみ method=token に戻す
- github_app_link_events への audit log writer (table の初回 writer)
- disconnectGithubApp() は全 link を順次切る互換 wrapper として deprecated

Octokit 解決:
- resolveOctokitForRepository() 追加。repository ごとに installation を選び、移行期間 fallback (active 1 件) を含む
- resolveOctokitForInstallation() 追加
- resolveOctokitFromOrg() は deprecated 互換 wrapper

batch shape:
- getOrganization() / listAllOrganizations() が githubAppLinks: [] を返すよう変更
- crawl / backfill ジョブを per-repo 解決に書き換え

audit log:
- app/services/github-app-link-events.server.ts 新規追加
- logGithubAppLinkEvent() helper を export

tests:
- resolveOctokitForRepository の 11 ケース追加 (active 1/0/2+/suspended/explicit installation の組み合わせ)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

ウォークスルー

GitHub Appの複数インストレーション対応を実現するため、監査ログ機能の追加、インストレーション単位でのOctokit解決、マルチリンク対応のクエリ更新、ジョブの内部実装変更が行われました。これらは単一インストレーション前提の既存コードを段階的に置き換え、組織あたり複数のGitHub Appリンクの管理を可能にします。

変更内容

コホート / ファイル 概要
GitHub App リンク監査ログ
app/services/github-app-link-events.server.ts
新しいサーバー側サービスモジュールとして、型付きの監査ログイベントドメインモデルを定義。logGithubAppLinkEvent非同期関数により、githubAppLinkEventsテーブルに機構化データとともにエントリを記録します。
GitHub App マテーション
app/services/github-app-mutations.server.ts
インストレーション単位の新規関数disconnectGithubAppLinkを追加し、ソフトデリート時にlink_deleted監査イベントを記録。既存disconnectGithubAppは「全削除」ヘルパーに変更し、インストレーション単位ごとに監査ログを出力します。
GitHub App 統合クエリ
app/services/github-integration-queries.server.ts
複数リンク対応としてgetGithubAppLinks(全アクティブリンク配列返却)を新規追加。既存関数を更新し、getGithubAppLinkは非推奨化、getGithubAppLinkByInstallationIdは選択列セットを拡張。インストレーション所属検証関数assertInstallationBelongsToOrgを追加します。
Octokit 解決機能
app/services/github-octokit.server.ts, app/services/github-octokit.server.test.ts
新しい型IntegrationMethodIntegrationForOctokitGithubAppLinkForOctokitRepositoryForOctokitを追加。インストレーション単位解決関数resolveOctokitForInstallationとリポジトリ単位解決関数resolveOctokitForRepositoryを新規追加。resolveOctokitFromOrgを非推奨化。テスト層でstubGithubAppEnvヘルパーと新規テストスイートを追加します。
ジョブ更新
app/services/jobs/backfill.server.ts, app/services/jobs/crawl.server.ts
org-scoped認証検証から、per-repository Octokit解決への移行。ジョブステップ内でインテグレーション方式チェックとリポジトリ単位でのOctokit構築を実施。エラーハンドリングを個別リポジトリ段階で行い、全体失敗を回避します。
バッチクエリ
batch/db/queries.ts
githubAppLinkColumns選択セットを拡張。getGithubAppLinkByOrgIdをマルチ行返却のgetGithubAppLinksByOrgIdに置き換え。getAllGithubAppLinksと統計関数をMap.groupByで複数レコード対応に変更。出力型のgithubAppLinkを配列githubAppLinksに統一します。
TypeScript 設定
tsconfig.json
コンパイラオプション libES2022 から ES2024 に更新。

シーケンス図

sequenceDiagram
    participant Job as バックフィル/クロールジョブ
    participant Query as 統合クエリ層
    participant Integration as インテグレーション取得
    participant OctokitRes as Octokit解決
    participant GithubAPI as GitHub API

    Job->>Query: 組織と全リポジトリ取得
    Query-->>Job: 組織、統合メソッド、リンク配列

    Job->>Job: リポジトリをループ

    alt github_app メソッド
        Job->>Integration: githubAppLinks 参照
        Integration-->>Job: アクティブリンク確認
        
        alt 明示的installationId あり
            Job->>OctokitRes: resolveOctokitForRepository<br/>(統合, リンク, リポジトリ)
            OctokitRes->>OctokitRes: 一致するリンク検索
            OctokitRes->>OctokitRes: サスペンド状態チェック
            OctokitRes-->>Job: リポジトリ用Octokit
        else 明示的installationId なし
            Job->>OctokitRes: resolveOctokitForRepository<br/>(統合, リンク, リポジトリ)
            OctokitRes->>OctokitRes: アクティブリンク フィルタ
            OctokitRes->>OctokitRes: 件数チェック<br/>(0 or >1 なら例外)
            OctokitRes-->>Job: リポジトリ用Octokit
        end
    else token メソッド
        Job->>OctokitRes: resolveOctokitForRepository<br/>(統合, リンク, リポジトリ)
        OctokitRes->>OctokitRes: privateToken チェック
        OctokitRes-->>Job: トークン用Octokit
    end

    Job->>GithubAPI: Octokit経由でAPI呼び出し
    GithubAPI-->>Job: データ取得
    Job->>Job: バックフィル/クロール実行
Loading
sequenceDiagram
    participant User as ユーザー
    participant Mutation as GitHubアプリマテーション
    participant Query as クエリ層
    participant DB as データベース
    participant Audit as 監査ログサービス

    User->>Mutation: disconnectGithubAppLink<br/>(organizationId, installationId)
    
    Mutation->>Query: インストレーション検証
    Query->>DB: 所属確認クエリ
    DB-->>Query: インストレーションデータ
    Query-->>Mutation: 検証完了

    Mutation->>DB: トランザクション開始
    
    Mutation->>DB: ソフトデリート実行<br/>(deletedAt 設定)
    DB-->>Mutation: 更新行数
    
    Mutation->>Query: 他アクティブリンク確認
    Query->>DB: 残存リンククエリ
    DB-->>Query: リンク数
    
    alt 最後のリンクだった場合
        Mutation->>DB: integrations.method<br/>を 'token' に変更
        Mutation->>DB: appSuspendedAt をクリア
    end
    
    Mutation->>Audit: logGithubAppLinkEvent<br/>('link_deleted', ...)
    Audit->>DB: githubAppLinkEvents<br/>に挿入
    
    Mutation->>DB: トランザクションコミット
    DB-->>Mutation: 完了
    Mutation-->>User: 切断成功
Loading

推定コードレビュー工数

🎯 4 (Complex) | ⏱️ ~75 分

関連する可能性のあるissue

  • GitHub App複数インストレーション対応(issue #283):このPRで実装される複数githubAppLinks、インストレーション単位の切断、リポジトリ単位のOctokit解決、クエリ変更が、直接的にこのissueの目的に対応します。

関連する可能性のあるPR

  • PR #287:複数GitHub Appインストレーション対応(マルチローgithubAppLinks、Octokit解決、クエリ/マテーション、バックフィル動作)として、このPRの基盤となる変更を提供します。
  • PR #288:このPRの新規イベント監査ログサービスとgithub_app_link_eventsテーブル参照が、PR #288で導入されたスキーマに直接依存します
  • PR #245:GitHub Appマイグレーションで導入・修正されたgithubAppLinks・disconnect処理・integrations経路に対し、このPRのイベントログと切断ロジックが直接動作します。

🐰 複数のインストール、今や花開き
リンク単位での切断、監査ログ記す
Octokit、リポジトリごとに賢く選び
型安全にマルチ対応、やさしく統合
スキーマはマッピング、データベースで踊る

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main changes: per-repository Octokit resolution and multi-installation query support, which are the core improvements across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-283-query-octokit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Owner Author

coji commented Apr 8, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown
Owner Author

coji commented Apr 8, 2026

Merge activity

  • Apr 8, 11:11 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 8, 11:11 AM UTC: @coji merged this pull request with Graphite.

@coji coji merged commit bae1045 into main Apr 8, 2026
6 of 7 checks passed
@coji coji deleted the feat/issue-283-query-octokit branch April 8, 2026 11:11
coji added a commit that referenced this pull request Apr 8, 2026
…rship initialization (#283 PR 3/7) (#290)

## Summary

Issue #283 の実装 stack **PR 3/7** — webhook handler / canonical reassignment / membership 初期投入の中核実装。stack で最重要 PR。

設計根拠: [`docs/rdd/issue-283-multiple-github-accounts.md`](./docs/rdd/issue-283-multiple-github-accounts.md)
作業計画: [`docs/rdd/issue-283-work-plan.md`](./docs/rdd/issue-283-work-plan.md)

依存: #288 (PR 1: schema), #296 (PR 2: query/octokit)

## 変更内容

### setup callback (`app/routes/api.github.setup.ts`)

- `(organizationId, installationId)` 単位 upsert(複合主キー対応)
- `github_account_type` を保存(personal / Organization の UI 分岐用)
- membership 初期投入: `fetchInstallationRepositories` → `initializeMembershipsForInstallation` → `membership_initialized_at = now`
- GitHub API 失敗時は link のみ保存し、`membership_initialized_at IS NULL` のまま auto-repair に委譲
- audit log: `link_created` / `membership_initialized` (success / failed)

### installation webhook (`app/services/github-webhook-installation.server.ts`)

- `findActiveLinkByInstallationOrAccount` 削除
- すべての lookup を `installation_id` で行う
- `installation.deleted`:
  - 該当 link のみ soft-delete
  - 最後の active link を失った時のみ `integrations.method = 'token'` に戻す
  - `link_deleted` 監査ログ
  - tenant 側で canonical reassignment を呼ぶ
- `installation.suspend` / `unsuspend`: `github_app_links.suspended_at` を更新(旧 `integrations.app_suspended_at` から移行)
- `installation_repositories.added/removed`:
  - membership upsert / soft-delete
  - canonical reassignment 呼び出し(removed 時)
  - bulk owner/repo 解決(1 query で N+1 解消)
- `installation.created`: setup callback が正本のため、既存 link が無ければ no-op

### canonical reassignment (`app/services/github-app-membership.server.ts`) 新規

- `reassignCanonicalAfterLinkLoss(orgId, lostInstallationId, source)`:
  - tenant DB の `repository_installation_memberships` を正本とする
  - 候補は active / non-suspended / `membership_initialized_at IS NOT NULL` の link のみ
  - 候補数で判定:
    - 1 → 自動 reassign + `canonical_reassigned`
    - 0 → null + `canonical_cleared` (or `assignment_required` if 未初期化 link 残存)
    - 2+ → null + `assignment_required`
  - **未初期化 link ガード**: 未初期化 link が残っている org では、候補 0 でも `canonical_cleared` ではなく `assignment_required` に倒す
  - LEFT JOIN + bulk update で N+1 を回避
  - tenant first / shared second の cross-store 順序
- `upsertRepositoryMembership` / `softDeleteRepositoryMembership` / `initializeMembershipsForInstallation` helpers

### installation repos fetcher (`app/services/github-installation-repos.server.ts`) 新規

- `fetchInstallationRepositories(installationId)`: GitHub API でその installation が見える repository を全ページ取得

### audit log writer (`app/services/github-app-link-events.server.ts`)

- `tryLogGithubAppLinkEvent` best-effort wrapper を追加(呼び出し側の `.catch(() => {})` ノイズを排除)

### auto repair (`app/services/jobs/crawl.server.ts`)

- crawl 冒頭に独立 step `repair-membership:<installation_id>` を追加
- `membership_initialized_at IS NULL` の active link を検出 → `installation_repositories` を再 fetch → membership upsert → `membership_initialized_at = now`
- per-link で独立 step、durably の中断・再開性を維持
- 失敗時は次回 crawl で再試行(idempotent)

### PR webhook (`app/services/github-webhook-pull.server.ts`)

- `owner + repo + installation_id` で repository を引く
- 移行期間中は `github_installation_id IS NULL` の repository も許可(PR 7 で strict 化)

### tests

- **`app/services/github-app-membership.server.test.ts`** 新規 (8 ケース):
  - 1 候補 → reassign + `canonical_reassigned`
  - 0 候補 → null + `canonical_cleared`
  - 2+ 候補 → null + `assignment_required`
  - 未初期化 link 残存 + 候補 0 → `assignment_required` (cleared じゃない)
  - suspended link は除外
  - 未初期化 link は除外
  - soft-deleted membership は除外
  - idempotency: 2 回実行しても結果が同じ
- **`app/services/github-webhook.server.test.ts`** 既存 12 ケース更新:
  - 新 schema (`suspended_at`, `membership_initialized_at`, `github_account_type`, `github_app_link_events`) 対応
  - tenant DB mock を chain proxy に変更

## 満たす受入条件

- **#8**: `installation.suspend/unsuspend` が対象 installation row のみ更新
- **#9**: `installation_repositories` が対象 installation のみ更新
- **#10**: `installation.deleted` が対象 installation row のみ `deleted_at` セット
- **#11**: 最後の active 切断時のみ method=token + private_token 有無で復帰先を分岐
- **#12**: canonical reassignment が候補 1 件で自動、0/複数で `null` + manual reselect
- **#19**: cross-store 更新は tenant first / shared second + audit log
- **#22**: setup callback で `membership_initialized_at` をセット、失敗時は repair に委譲
- **#23**: 未初期化 link 残存時は assignment_required に倒れる

## Stack 位置

\`\`\`text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
  └ [PR 3: webhook/membership] ← this PR
    └ PR 4 (UI)
      └ PR 5 (repo UI)
        └ PR 6 (backfill)
          └ PR 7 (strict)
\`\`\`

## 後続 PR への影響

- **PR 4**: integration settings UI が `getGithubAppLinks()` に切替 + installation selector 追加 + `assertInstallationBelongsToOrg` を loader 境界で呼ぶ
- **PR 5**: repository list/detail で `assignment required` バッジ + 個別再選択 mutation(同 helper を再利用)
- **PR 7**: PR webhook の `OR github_installation_id IS NULL` 削除 + crawl/backfill の移行期間 fallback 削除

## テスト

- [x] \`pnpm validate\` (lint / format / typecheck / build / test 全 339 tests)
- [x] canonical reassignment helper を 8 ケースのユニットテストでカバー (cross-store 整合性 / idempotency / 候補 0/1/2+ / 未初期化ガード / suspended 除外 / soft-deleted 除外)
- [x] webhook integration test (12 ケース)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## リリースノート

* **新機能**
  * GitHub Appインストール時にリポジトリメンバーシップを自動初期化・同期する仕組みを追加
  * メンバーシップ情報に基づくリポジトリの正規割り当て(再割り当て)処理を導入
  * インストールのリポジトリ取得機能を追加

* **バグ修正**
  * インストール/サスペンド/削除イベント処理のスコープと整合性を強化
  * 監査ログ書き込み失敗を影響させない安全措置を追加

* **テスト**
  * 再割り当て挙動の包括的なテストスイートを追加
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
coji added a commit that referenced this pull request Apr 12, 2026
## Summary

Issue #283 の実装 stack **PR 4/7** — settings UI を multi-installation 対応に。Vercel ライクに installation の存在をユーザーに意識させない UX。

設計根拠: [\`docs/rdd/issue-283-multiple-github-accounts.md\`](./docs/rdd/issue-283-multiple-github-accounts.md)
作業計画: [\`docs/rdd/issue-283-work-plan.md\`](./docs/rdd/issue-283-work-plan.md)

依存: #288 (PR 1: schema), #296 (PR 2: query/octokit), #290 (PR 3: webhook/membership)

## 変更内容

### integration page (\`app/routes/$orgSlug/settings/integration/index.tsx\`)

- loader が \`githubAppLinks[]\` 配列で各 installation の状態 (suspended / membership_initialized) を返す
- action を **discriminated union + parseWithZod + match.exhaustive** に refactor (CLAUDE.md 規約準拠、200 行 if-chain → ts-pattern)
- 新 INTENTS: \`disconnectGithubAppLink\`, \`confirmDisconnectGithubAppLink\`
- per-installation disconnect (\`assertInstallationBelongsToOrg\` 検証)

### GitHub App セクション UI

- \`InstallationCard\` で active installation を 1 枚ずつカード表示
- カードごとに fetcher + ConfirmDialog (installationId を保持できる)
- \`Add another GitHub account\` ボタン (1 件以上接続済みのとき)
- personal account / organization で GitHub 設定 URL を分岐 (\`buildInstallationSettingsUrl\`)

### repositories.add page (Vercel 風 merge UI)

- **installation selector を出さない**: 全 active installation の repo を並列 fetch → owner/repo で dedupe → 1 リスト表示
- 各 repo は元 installation を tag (\`TaggedInstallationRepo\`)
- ユーザーが Add 押下時、その repo の \`installationId\` が hidden input で submit される
- action 側で \`assertInstallationBelongsToOrg\` を server-side 検証
- \`addRepository\` mutation に \`githubInstallationId\` 引数追加 + \`upsertRepositoryMembership\` 呼び出し
- \`fetchAllInstallationRepos\` cache key を per-installation に変更

### github-users page

- **installation selector を出さない**: \`search.users\` は global API なので、複数 active link があれば最初を裏で使う
- \`searchGithubUsers\` を installationId 引数なしに簡素化
- toolbar / loader / table から installation 関連 UI 削除

### 共有 helpers

- \`app/libs/github-account.ts\`:
  - \`formatGithubAccountLabel\` (personal は \`@login\`, org はそのまま)
  - \`isPersonalAccount\`
  - \`buildInstallationSettingsUrl\` (User → \`/settings/installations/<id>\`, Organization → \`/organizations/<login>/settings/installations\`)
- \`app/services/github-integration-queries.server.ts\`:
  - \`getActiveInstallationOptions\` 共通 helper

## 満たす受入条件

- **#1**: 同一 org で複数 installation を接続できる (UI で確認可能)
- **#4**: repository 追加時に installationId が記録される (UI 上は merge されているが裏で installationId が紐付く)
- **#5**: github-users で複数 installation 環境でも検索可能 (active link を裏で選択)
- **#13/#14/#15**: integration UI の installation 単位表示 / Add another / personal/org URL 分岐
- **#17**: server-side で client 由来 installationId を検証
- **#21**: 1 件目のみは selector なしでデフォルト動作

## Stack 位置

\`\`\`text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
  └ PR 3 (#290): webhook/membership
    └ [PR 4: UI] ← this PR
      └ PR 5 (repo UI)
        └ PR 6 (backfill)
          └ PR 7 (strict)
\`\`\`

## UX 哲学

最初は installation selector で実装したが、Vercel など先行サービスが「installation を意識させない」UX を取っているのを参考に書き直し。ユーザーは repository を選ぶだけでよく、installation は内部の attribution として透過的に処理される。

## テスト

- [x] \`pnpm validate\` (lint / format / typecheck / build / test 全 339 tests)
- [x] \`get-installation-repos.test.ts\` を \`TaggedInstallationRepo\` 対応に更新

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **新機能**
  * 各GitHub Appインストールを個別カードで表示し、アカウント表示ラベルとインストール設定への直接リンクを追加。

* **改善**
  * インストール単位の接続解除(確認付き)と専用意図を導入。リポジトリ追加は全アクティブインストールを並列取得・重複排除し、各リポジトリにインストールIDを保持。ユーザー検索はアクティブインストールを順次試行するフォールバックを実装。

* **ドキュメント**
  * 統合UXと回復フロー案を更新。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
coji added a commit that referenced this pull request Apr 12, 2026
## Summary

Issue #283 の実装 stack **PR 5/7** — broken state(`github_installation_id IS NULL`)の repository を救済する UI / mutation / batch CLI を追加。

設計根拠: [\`docs/rdd/issue-283-multiple-github-accounts.md\`](./docs/rdd/issue-283-multiple-github-accounts.md)
作業計画: [\`docs/rdd/issue-283-work-plan.md\`](./docs/rdd/issue-283-work-plan.md)

依存: #288 (PR 1: schema), #296 (PR 2: query/octokit), #290 (PR 3: webhook/membership), #291 (PR 4: UI)

## UX 哲学

PR 4 の Vercel 哲学を継承し、**通常時は repository 一覧/詳細に installation 名を出さない**。canonical installation を失った broken state の repository だけを救済導線として可視化する。

## 変更内容

### service helper (\`app/services/github-app-membership.server.ts\`)

- \`fetchEligibleInstallationIds()\` 内部 helper 抽出 — \`reassignCanonicalAfterLinkLoss\` と \`reassignBrokenRepository\` で eligible link 取得ロジックを共有
- \`reassignBrokenRepository(orgId, repoId, source)\` 追加
  - canonical installation を持たない repo に対して membership table から候補を引いて再割当を試みる
  - 結果を discriminated union で返す: \`reassigned | no_candidates | ambiguous | not_broken\`
  - 1 候補 → \`canonical_reassigned\` audit log を書く
  - 0 候補 / 2+ 候補 → 戻り値で表現、audit log は書かない(installationId が無いため)
- \`isRepositoryBroken()\` を \`app/libs/github-account.ts\` に追加(client-safe)

### route mutation (\`app/routes/$orgSlug/settings/repositories._index/\`)

- loader が \`integrationMethod\` を \`'token' | 'github_app' | null\` で返す
- action に \`reassignBroken\` intent 追加(\`source: 'manual_reassign'\`)
- discriminated union を \`match.exhaustive\` でハンドリング

### UI (\`+components/repo-columns.tsx\`)

- \`NeedsReconnectionBadge\` コンポーネント
  - \`isRepositoryBroken(repo, integrationMethod)\` で判定
  - destructive variant Badge + \`AlertTriangleIcon\` + tooltip
  - 1-click "Reassign" ボタン (per-row \`useFetcher\`)
- 結果別 toast:
  - 成功 → "Repository reassigned to an active installation."
  - 候補 0 → "No active installation can see this repository. Reinstall the GitHub App and try again."
  - 候補 2+ → "Disconnect the unwanted installations to resolve."

### batch CLI (\`batch/commands/reassign-broken-repositories.ts\`)

- \`pnpm batch reassign-broken-repositories <orgId> [--repository <id>]\`
- broken repository を全て列挙 → 順次 \`reassignBrokenRepository\` (\`source: 'cli_repair'\`)
- 結果集計と各 repo の状況を consola で出力
- \`match.exhaustive\` で網羅性確保

### tests

- \`github-app-membership.server.test.ts\` に 6 ケース追加:
  - 1 候補 → reassigned + canonical_reassigned event
  - 0 候補 → no_candidates (audit log なし)
  - 2+ 候補 → ambiguous (audit log なし)
  - not_broken(既に canonical あり)
  - suspended link は候補から除外
  - 未初期化 link は候補から除外

## 満たす受入条件

- **#7**: 失われた canonical installation の repository 救済経路
- **#18**: assignment required 状態の表示と再割当 UI

## Stack 位置

\`\`\`text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
  └ PR 3 (#290): webhook/membership
    └ PR 4 (#291): UI
      └ [PR 5: repo UI] ← this PR
        └ PR 6 (backfill)
          └ PR 7 (strict)
\`\`\`

## テスト

- [x] \`pnpm validate\` (lint / format / typecheck / build / test 全 345 tests)
- [x] \`reassignBrokenRepository\` の 6 ケースをユニットテストでカバー

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **新機能**
  * GitHub App連携で「接続切れ(broken)」判定を共通化するチェックを追加
  * リポジトリ一覧に警告バッジを表示し、バッジから再割り当て(再接続)操作を実行可能に(操作中の状態表示・ツールチップ付き)
  * 壊れたリポジトリを一括検出・再割り当てするCLIコマンドを追加
* **テスト**
  * 再割り当ての各結果(再割当て/候補なし/あいまい/初期化待ち/未該当/未発見)を検証する自動テストを追加
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
coji added a commit that referenced this pull request Apr 12, 2026
#283 PR 6/7) (#293)

## Summary

Issue #283 の実装 stack **PR 6/7** — PR 7 (strict lookup 切替) のデプロイ準備を行う one-shot batch CLI。

設計根拠: [\`docs/rdd/issue-283-multiple-github-accounts.md\`](./docs/rdd/issue-283-multiple-github-accounts.md)
作業計画: [\`docs/rdd/issue-283-work-plan.md\`](./docs/rdd/issue-283-work-plan.md)

依存: #288 (PR 1: schema), #296 (PR 2), #290 (PR 3), #291 (PR 4), #292 (PR 5)

## 変更内容

### batch CLI (\`batch/commands/backfill-installation-membership.ts\`)

\`pnpm batch backfill-installation-membership [orgId] [--dry-run]\`

組織ごとに以下の判定:

| 組織の状態 | 動作 | 結果 |
|---|---|---|
| \`integrations.method = 'token'\` | skip | \`skipped_token_method\` |
| github_app + 0 active link | skip + warn | \`skipped_no_active_link\` (要 reinstall) |
| github_app + 1 active link | bulk backfill | \`backfilled_single_link\` |
| github_app + 2+ active links | skip + warn | \`skipped_multi_link_unmapped\` (要 \`reassign-broken-repositories\`) |

### 動作詳細

- **bulk backfill**: \`UPDATE repositories SET github_installation_id = ? WHERE github_installation_id IS NULL\` で一括設定
- **membership seed**: \`fetchInstallationRepositories\` (GitHub API) で installation の見える repo を全取得 → \`initializeMembershipsForInstallation\` で bulk 投入
- **API 失敗時 fallback**: orphan repository に対してのみ \`upsertRepositoryMembership\` を呼ぶ(最低限の membership を確保、本格的な seed は次回 crawl の repair に委譲)
- **冪等性**: \`WHERE github_installation_id IS NULL\` で再実行 no-op
- **\`--dry-run\`**: 書き込みも GitHub API 呼び出しも行わず判定だけ実行

### Runbook (本番デプロイ手順)

1. **PR 1-5 がマージ・本番デプロイ済みであることを確認**
2. **本番 DB のスナップショット**: \`pnpm ops pull-db -- --app upflow\`
3. **dry-run で計画確認**:
   \`\`\`bash
   pnpm batch backfill-installation-membership -- --dry-run
   \`\`\`
   - skip 件数 / backfill 対象件数 / 要注意組織(multi-link / no-link)を確認
4. **実行**:
   \`\`\`bash
   pnpm batch backfill-installation-membership
   \`\`\`
5. **検証**: \`integrations.method = 'github_app'\` の組織に対して以下が 0 件であること:
   \`\`\`sql
   SELECT count(*) FROM repositories WHERE github_installation_id IS NULL;
   \`\`\`
6. **multi-link 組織の対応**: \`pnpm batch reassign-broken-repositories <orgId>\` で各 multi-link 組織を救済
7. **検証 OK 後** PR 7 をマージ

### Stack 位置

\`\`\`text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
  └ PR 3 (#290): webhook/membership
    └ PR 4 (#291): UI
      └ PR 5 (#292): repo UI
        └ [PR 6: backfill] ← this PR
          └ PR 7 (strict)
\`\`\`

## 満たす受入条件

- **#20**: schema → backfill 完了 → strict lookup 切替 (前半)

## テスト

- [x] \`pnpm validate\` (lint / format / typecheck / build / test 全 351 tests)
- [x] CLI コマンドの 6 ケースを vitest でカバー (single / token / multi / dry-run / idempotent / API failure fallback)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## リリースノート

* **新機能**
  * `backfill-installation-membership` コマンドを CLI に追加しました。このコマンドは GitHub App インストール連携時に、リポジトリのインストール ID とメンバーシップ情報を一括更新する機能を提供します。`--dryRun` フラグでプレビュー実行も可能です。

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
coji added a commit that referenced this pull request Apr 12, 2026
…acks (#283 PR 7/7) (#294)

## Summary

Issue #283 stack の **最終 PR (7/7)** — 移行期間 fallback を全削除し、strict installation lookup に切替。これでシリーズ完結。

設計根拠: [\`docs/rdd/issue-283-multiple-github-accounts.md\`](./docs/rdd/issue-283-multiple-github-accounts.md)
作業計画: [\`docs/rdd/issue-283-work-plan.md\`](./docs/rdd/issue-283-work-plan.md)

依存: #288, #296, #290, #291, #292, #293

## ⚠️ デプロイ順序

このマージは **PR 6 (#293) の \`backfill-installation-membership\` を本番実行 + 検証完了後** にのみ実行してください。順序を守らないと既存リポジトリが strict lookup で弾かれてエラーになります。

詳細手順は #293 の Runbook 参照。

## 変更内容

### schema (\`db/shared.sql\` + migration)

- \`integrations.app_suspended_at\` カラム削除(PR 1 で \`github_app_links.suspended_at\` に移行済み)
- Atlas が table-rebuild migration を生成 (\`20260408001949.sql\`)
- \`app/services/type.ts\` 再生成

### Octokit 解決 (\`app/services/github-octokit.server.ts\`)

- \`resolveOctokitForRepository\` 内の transitional fallback を全削除:
  - \`github_app + githubInstallationId IS NULL\` → エラー \"Repository has no canonical installation assigned. Run reassign-broken-repositories or reinstall.\"
  - \"active link 1 件で代用\" のロジック削除
- 削除した legacy export:
  - \`assertOrgGithubAuthResolvable\`
  - \`resolveOctokitFromOrg\`
  - \`OrgGithubAuthInput\`

### query 層 (\`app/services/github-integration-queries.server.ts\`)

- \`getGithubAppLink\` (deprecated) を削除(caller ゼロ)

### PR webhook (\`app/services/github-webhook-pull.server.ts\`)

- 移行期間の \`OR github_installation_id IS NULL\` を削除し strict 化:
  \`\`\`sql
  WHERE owner = ? AND repo = ? AND github_installation_id = ?
  \`\`\`
- broken state webhook を ops が検知できるよう、各 silent return に \`debug()\` ログを追加(\`createDebug('app:github-webhook:pull')\`)

### route (\`app/routes/$orgSlug/settings/repositories/$repository/$pull/\`)

- \`index.tsx\`: \`resolveOctokitFromOrg\` から \`resolveOctokitForRepository\` に migration
- \`queries.server.ts\`: \`getRepository\` の select に \`githubInstallationId\` 追加

### cleanup

- \`appSuspendedAt\` への参照を全削除 (setup callback / webhook handler / mutation / batch query / test fixture)
- \`github-octokit.server.test.ts\` から legacy 関数のテストと transitional fallback ケースを削除

## 満たす受入条件

- **#16**: PR webhook strict lookup
- **#20** (完全達成): schema → backfill → strict lookup 切替

## デプロイ Rollback 戦略

- migration は destructive (column drop)。本番デプロイ前に staging で apply → rollback → re-apply のリハーサル必須
- デプロイ後 24 時間は \`github_app_link_events\` の異常パターン (連続 \`canonical_reassigned\`, \`webhook_dropped\` debug ログの急増) を監視
- 異常検知時:
  1. PR 7 を revert
  2. \`app_suspended_at\` を復活させる down migration を当てる (Atlas で生成済み)
  3. 必要なら \`reassign-broken-repositories\` で個別 repo 救済

## Stack 位置 (完成形)

\`\`\`text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
  └ PR 3 (#290): webhook/membership
    └ PR 4 (#291): UI
      └ PR 5 (#292): repo UI
        └ PR 6 (#293): backfill
          └ [PR 7: strict] ← this PR
\`\`\`

## テスト

- [x] \`pnpm validate\` (lint / format / typecheck / build / test 全 342 tests)
- [x] \`resolveOctokitForRepository\` の strict path テスト (canonical なし → throw)
- [x] migration \`pnpm db:setup\` で再現可能

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **バグ修正**
  * リポジトリのGitHub Appインストール割り当てがない場合に厳格にエラーを返すようにし、エラーメッセージをより具体的に表示するよう改善しました。

* **リファクタリング**
  * 組織スコープの旧ヘルパーを削除し、Webhook処理にデバッグログを追加して追跡性を向上させました。
  * 統合の解決フローを整理しました。

* **雑務**
  * 統合テーブルから未使用フィールドを削除するスキーマ変更と関連マイグレーション、CLI表示文言、テスト修正を行いました。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant