Skip to content

feat(db): add schema for multiple GitHub App installations (#283 PR 1/7)#288

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

feat(db): add schema for multiple GitHub App installations (#283 PR 1/7)#288
coji merged 1 commit intomainfrom
feat/issue-283-schema

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Apr 7, 2026

Summary

Issue #283 の実装 stack PR 1/7 — schema と型生成のみ。アプリケーションコードからの新カラム参照は PR 2 以降で入れる。

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

変更内容

shared DB (db/shared.sql)

  • github_app_links:
    • PK を (organization_id) から (organization_id, installation_id) に変更 → 1 org に複数 installation を保持可能に
    • UNIQUE (github_account_id) を削除し、non-unique INDEX に変更 → 同一 GitHub account を複数 Upflow org で使えるように
    • suspended_at TEXT NULL 追加 → installation 単位の suspend 状態を持つ (現状 org 単位 1 値の integrations.app_suspended_at から PR 7 で移行予定)
    • github_account_type TEXT NULL 追加 → personal account / organization の UI 分岐用
    • membership_initialized_at TEXT NULL 追加 → canonical reassignment の安全ガード用
  • github_app_link_events 新規追加 → cross-store 更新の監査ログ。writer は PR 2 / PR 3 で追加される

tenant DB (db/tenant.sql)

  • repositories.github_installation_id INTEGER NULL 追加 → repository ごとにどの installation で fetch するかを保持
  • repository_installation_memberships 新規追加 → installation ↔ repository の所属関係を永続化。canonical reassignment 判定の正本として使う

型生成・ごく僅かな追従

  • app/services/type.ts, app/services/tenant-type.ts: kysely-codegen 再生成
  • batch/db/queries.ts: getTenantData() の repositories SELECT に githubInstallationId を追加 (型エラー解消のための最小変更)

満たす受入条件

Stack 位置

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

注意点

  • github_app_links の migration は table 再作成方式 (PK 変更のため)。既存 row は organization_id, installation_id, github_account_id, github_org, app_repository_selection, deleted_at, created_at, updated_at をコピー。新カラムは NULL で初期化される
  • github_app_link_events の writer は PR 2 (disconnectGithubAppLink) / PR 3 (canonical reassignment / repair / webhook) で追加される。本 PR 時点では未使用 table が main に入ることを許容
  • destructive な DROP TABLE が含まれるが、直前の INSERT SELECT 成功前提の Atlas 生成パターン

テスト

  • pnpm validate (lint / format / typecheck / build / test 全 320 tests)
  • pnpm db:setup で seed 含めて再現可能

🤖 Generated with Claude Code

Summary by CodeRabbit

リリースノート

  • 新機能
    • GitHub アプリのインストール管理を強化しました。インストール イベントの追跡とリポジトリ メンバーシップの管理機能を追加しました。
    • GitHub アプリ リンク情報に新しいステータス追跡フィールドを追加しました。

shared DB:
- github_app_links: PK を (organization_id, installation_id) に変更
- UNIQUE (github_account_id) を廃止、INDEX に変更
- suspended_at / github_account_type / membership_initialized_at カラム追加
- github_app_link_events 監査ログテーブル新規追加

tenant DB:
- repositories.github_installation_id カラム追加
- repository_installation_memberships テーブル新規追加

本 PR は schema と型生成のみ。アプリケーションコードからの参照は PR 2 以降で追加する。

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

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

複数のGitHub App インストレーションを組織ごとにサポートするために、データベーススキーマとTypeScript型定義を拡張しました。リポジトリテーブルにgithubInstallationIdフィールドを追加し、新しいRepositoryInstallationMembershipsテーブルとGithubAppLinkEventsイベント記録テーブルを導入しました。

Changes

Cohort / File(s) Summary
TypeScript型定義
app/services/tenant-type.ts, app/services/type.ts
RepositoriesgithubInstallationIdフィールドを追加、RepositoryInstallationMembershipsGithubAppLinkEventsインターフェースを新規追加、GithubAppLinksgithubAccountTypemembershipInitializedAtsuspendedAtフィールドを拡張
バッチクエリ更新
batch/db/queries.ts
テナントレベルリポジトリクエリにgithubInstallationIdカラムを追加
共有スキーマ定義
db/shared.sql
github_app_linksテーブルの主キーを複合キー(organization_id, installation_id)に変更、新しいgithub_app_link_eventsテーブルと関連インデックスを追加
テナントスキーマ定義
db/tenant.sql
repositoriesテーブルにgithub_installation_idカラムを追加、新しいrepository_installation_membershipsテーブルを追加
データベースマイグレーション
db/migrations/shared/20260407084907.sql, db/migrations/tenant/20260407084907.sql
スキーマ定義と同期するマイグレーション、外部キー制約とインデックスを含む

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • docs: add RDD and work plan for issue #283 multi GitHub App installations #287: 複数GitHub Appインストレーション対応の設計とマイグレーション計画(RDD)を具体化するDB スキーマ・型定義・マイグレーション(repositories.github_installation_idrepository_installation_membershipsgithub_app_link_eventsgithub_app_linksフィールド)を実装

Poem

🐰 多くのインストールへの道を開き
スキーマは広がり、テーブルも増えて
イベントは記録され、つながりは深く
組織はしぶき、インストール花咲く
データベースの庭に新しい枝が🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 プルリクエストのタイトルは、複数のGitHub App インストールをサポートするためのスキーマ追加という主要な変更を明確に要約しており、変更セット全体と密接に関連しています。

✏️ 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-schema

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.

@coji coji marked this pull request as ready for review April 7, 2026 09:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/services/type.ts`:
- Around line 52-74: The generated Kysely types in GithubAppLinkEvents and
GithubAppLinks (and corresponding entries in tenant-type.ts around the mentioned
lines) have been manually edited and are out of sync with codegen; revert any
hand changes and regenerate the types by running the codegen command (pnpm
db:generate), replace app/services/type.ts and app/services/tenant-type.ts with
the exact output produced, and commit those regenerated files so the Generated
DB types match CI expectations (look for the interfaces GithubAppLinkEvents and
GithubAppLinks to verify the corrected, codegen-produced definitions).

In `@db/shared.sql`:
- Around line 157-163: 現在のクエリ/サービスは org ごとに単一行を前提にしているため、複数 installation
を許可したスキーマ変更で取りこぼしや全件更新が発生します。対処は二択で揃えてください:1) 永続対応:batch/db/queries.ts の
getGithubAppLinkByOrgId() を installation-aware に改名/拡張して (例:
getGithubAppLinkByOrgAndInstallation(orgId, installationId)) あるいは
getAllGithubAppLinks() を org
毎に複数行返すように修正し、app/services/github-app-mutations.server.ts と
app/services/github-webhook-installation.server.ts の更新・削除ロジックを organizationId
単体ではなく installation_id をキーにして対象行を特定またはループで処理する(更新/削除は installation_id を WHERE
に含める)。2) 一時ガード:リリースまでの暫定対応として insert/update 前に既存行の存在チェックを追加して 1 org = 1 row
を維持する(新規挿入を拒否または既存行を更新する)。該当する識別子:getGithubAppLinkByOrgId(),
getAllGithubAppLinks(), app/services/github-app-mutations.server.ts,
app/services/github-webhook-installation.server.ts を修正対象として扱ってください。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce73a542-af8b-4782-8295-5fe45b2d976e

📥 Commits

Reviewing files that changed from the base of the PR and between eaed0ac and 97dc9e7.

⛔ Files ignored due to path filters (2)
  • db/migrations/shared/atlas.sum is excluded by !**/*.sum
  • db/migrations/tenant/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • app/services/tenant-type.ts
  • app/services/type.ts
  • batch/db/queries.ts
  • db/migrations/shared/20260407084907.sql
  • db/migrations/tenant/20260407084907.sql
  • db/shared.sql
  • db/tenant.sql

Comment on lines +52 to 74
export interface GithubAppLinkEvents {
createdAt: Generated<string>;
detailsJson: string | null;
eventType: string;
id: Generated<number>;
installationId: number;
organizationId: string;
source: string;
status: string;
}

export interface GithubAppLinks {
appRepositorySelection: "all" | "selected";
createdAt: Generated<string>;
deletedAt: string | null;
githubAccountId: number;
githubAccountType: string | null;
githubOrg: string;
installationId: number;
membershipInitializedAt: string | null;
organizationId: string;
suspendedAt: string | null;
updatedAt: Generated<string>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

生成ファイルがまだ codegen と同期していません。

CI の Generated DB types are out of date が出ているので、このコミットの app/services/type.ts / app/services/tenant-type.ts はまだ再生成結果と一致していません。ここは手修正を積むのではなく、pnpm db:generate の出力をそのままコミットし直してください。

Based on learnings: In app/services/type.ts, do not manually modify generated kysely-codegen types. Rely on the codegen output for types and migrations for schema changes; manual edits can cause drift between codegen and database schema.

Also applies to: 173-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/type.ts` around lines 52 - 74, The generated Kysely types in
GithubAppLinkEvents and GithubAppLinks (and corresponding entries in
tenant-type.ts around the mentioned lines) have been manually edited and are out
of sync with codegen; revert any hand changes and regenerate the types by
running the codegen command (pnpm db:generate), replace app/services/type.ts and
app/services/tenant-type.ts with the exact output produced, and commit those
regenerated files so the Generated DB types match CI expectations (look for the
interfaces GithubAppLinkEvents and GithubAppLinks to verify the corrected,
codegen-produced definitions).

Comment on lines +157 to +163
PRIMARY KEY (`organization_id`, `installation_id`),
CONSTRAINT `github_app_links_organization_id_fkey` FOREIGN KEY (`organization_id`) REFERENCES `organizations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
);
-- Create index "github_app_links_installation_id_key" to table: "github_app_links"
CREATE UNIQUE INDEX `github_app_links_installation_id_key` ON `github_app_links` (`installation_id`);
-- Create index "github_app_links_github_account_id_key" to table: "github_app_links"
CREATE UNIQUE INDEX `github_app_links_github_account_id_key` ON `github_app_links` (`github_account_id`);
-- Create index "github_app_links_github_account_id_idx" to table: "github_app_links"
CREATE INDEX `github_app_links_github_account_id_idx` ON `github_app_links` (`github_account_id`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

2件目の installation を許可する前に、既存の singleton 前提を外す対応が必要です。

organization_id ごとに複数行を持てるようにした一方で、現状の batch/db/queries.tsgetGithubAppLinkByOrgId() で 1 件だけ取得し、getAllGithubAppLinks() でも organizationId ごとに 1 件へ潰しています。さらに app/services/github-app-mutations.server.tsapp/services/github-webhook-installation.server.ts の更新系も organizationId だけで githubAppLinks を更新しているので、2件目の installation が入り始めると取りこぼしか全件更新/削除になります。installation-aware な read/write と同じリリース境界に揃えるか、それまでは 1 org = 1 row を保つガードが必要です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/shared.sql` around lines 157 - 163, 現在のクエリ/サービスは org ごとに単一行を前提にしているため、複数
installation を許可したスキーマ変更で取りこぼしや全件更新が発生します。対処は二択で揃えてください:1)
永続対応:batch/db/queries.ts の getGithubAppLinkByOrgId() を installation-aware
に改名/拡張して (例: getGithubAppLinkByOrgAndInstallation(orgId, installationId)) あるいは
getAllGithubAppLinks() を org
毎に複数行返すように修正し、app/services/github-app-mutations.server.ts と
app/services/github-webhook-installation.server.ts の更新・削除ロジックを organizationId
単体ではなく installation_id をキーにして対象行を特定またはループで処理する(更新/削除は installation_id を WHERE
に含める)。2) 一時ガード:リリースまでの暫定対応として insert/update 前に既存行の存在チェックを追加して 1 org = 1 row
を維持する(新規挿入を拒否または既存行を更新する)。該当する識別子:getGithubAppLinkByOrgId(),
getAllGithubAppLinks(), app/services/github-app-mutations.server.ts,
app/services/github-webhook-installation.server.ts を修正対象として扱ってください。

@coji
Copy link
Copy Markdown
Owner Author

coji commented Apr 7, 2026

@coderabbitai 指摘ありがとうございます。両方とも回答です。

1. app/services/type.ts / tenant-type.ts の手動編集について

手動編集はありません。pnpm db:generate (kysely-codegen) の出力をそのままコミットしています。新カラム (githubAccountType, suspendedAt, membershipInitializedAt) と新テーブル (githubAppLinkEvents, repositoryInstallationMemberships) はすべて schema 変更(同 PR 内 db/shared.sql, db/tenant.sql)から自動生成されたものです。

2. クエリ層が "1 org = 1 row" 前提のままになっている件

これは stacked PR の構造による意図的な分離です。本 PR (#288) は schema 追加と型生成のみ をスコープとし、caller の追従は後続 PR で対応しています:

この分離理由は docs/rdd/issue-283-work-plan.md に記載しており、各 PR 単独でビルド・テストが通る形を維持しています。最終的な strict 化 (OR github_installation_id IS NULL 削除など) は #7 で行います。

stack 全体のコンテキストで再レビューしていただくと、"1 org = 1 row" 前提は段階的に解消されているのが見えると思います。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Tip

For best results, initiate chat on the files or code changes.

@coji ご丁寧な説明ありがとうございます。両点とも理解しました。

  1. 型ファイルの自動生成: app/services/type.ts および app/services/tenant-type.tspnpm db:generate (kysely-codegen) による自動生成であることを確認しました。レビューコメントの指摘は不要でした。失礼しました。

  2. "1 org = 1 row" 前提の段階的解消: stacked PR の設計意図が明確です。feat(db): add schema for multiple GitHub App installations (#283 PR 1/7) #288 はスキーマ・型生成のみ、feat(github-app): per-repo Octokit resolution and multi-installation queries (#283 PR 2/7) #289 でクエリ層の配列対応、feat(github-app): webhook handlers, canonical reassignment, and membership initialization (#283 PR 3/7) #290 で webhook 処理の書き換えと audit log 実装、最終的な strict 化は #7 で行うという段階的アプローチを理解しました。docs/rdd/issue-283-work-plan.md に設計方針が文書化されていることも了解です。

次の PR (#289, #290) のレビュー時には今回のコンテキストを踏まえてレビューします。

(∩`-´)⊃━☆゚.*・。゚


✏️ Learnings added
Learnt from: coji
URL: https://github.com/coji/upflow/pull/288

Timestamp: 2026-04-07T13:45:02.165Z
Learning: In coji/upflow, `app/services/type.ts` and `app/services/tenant-type.ts` are auto-generated files produced by `pnpm db:generate` (kysely-codegen) from the SQL schema files (`db/shared.sql`, `db/tenant.sql`). Do not flag these files for manual editing concerns — all changes reflect schema definitions.

Learnt from: coji
URL: https://github.com/coji/upflow/pull/288

Timestamp: 2026-04-07T13:45:02.165Z
Learning: In coji/upflow, issue `#283` (multiple GitHub App installations) is implemented as a 7-PR stack. PR `#288` is schema+type-generation only. PR `#289` adds `getGithubAppLinks()` array return, `disconnectGithubAppLink(orgId, installationId)`, per-repo Octokit resolution. PR `#290` rewrites `github-webhook-installation.server.ts` to per-installation lookup and adds canonical reassignment, audit log, and auto repair. Final strict cleanup (removing `OR github_installation_id IS NULL` etc.) happens in PR 7. Work plan is documented in `docs/rdd/issue-283-work-plan.md`. Do not flag "1 org = 1 row" patterns in PR `#288` as incomplete — they are intentionally deferred.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/upflow PR: 141
File: CLAUDE.md:215-215
Timestamp: 2026-03-09T08:25:14.662Z
Learning: In coji/upflow, `OrganizationScopePlugin` was removed in PR `#108` and the file `app/services/organization-scope-plugin.ts` no longer exists. The canonical org-scoping pattern is now: every UPDATE/DELETE on org-scoped tables must manually include `WHERE organizationId = ?` with a server-derived value, and per-org tenant data is accessed via `getTenantDb(organizationId)`. There is no automatic Kysely plugin for org scoping.

Learnt from: coji
Repo: coji/upflow PR: 198
File: app/services/jobs/crawl.server.ts:159-165
Timestamp: 2026-03-17T07:58:41.063Z
Learning: In coji/upflow, `getTenantDb(organizationId)` returns a per-org isolated SQLite database file (one DB file per organization). Tables within the tenant DB (e.g., `organizationSettings`, `repositories`) are already implicitly scoped to that single organization. Therefore, `WHERE organizationId = ?` is NOT required for UPDATE/DELETE operations on tenant DB tables. The mandatory `WHERE organizationId = ?` rule applies only to the shared/global DB accessed via the top-level `db` instance (app/services/db.server.ts), not to queries made through `getTenantDb()`.

Learnt from: CR
Repo: coji/upflow PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T09:33:01.662Z
Learning: Applies to app/**/*.{ts,tsx} : In Kysely queries on shared database org-scoped tables, manually include `WHERE organizationId = ?` condition; tenant database queries are auto-scoped via `getTenantDb(organizationId)`

Learnt from: coji
Repo: coji/upflow PR: 196
File: batch/db/mutations.ts:229-263
Timestamp: 2026-03-17T06:06:00.348Z
Learning: In coji/upflow (batch/db/mutations.ts), the upsertAnalyzedData function intentionally does NOT wrap upsertCompanyGithubUsers, batchUpsertPullRequests, batchUpsertPullRequestReviews, and upsertPullRequestReviewers in a single transaction. This is by design: all upserts use ON CONFLICT ... doUpdateSet (idempotent), and Durably retries the entire upsert step on failure, so partial writes are safely overwritten on retry. This matches the pattern in batch/usecases/analyze-and-upsert.ts. Do not flag this as a missing transaction in future reviews.

Learnt from: coji
Repo: coji/upflow PR: 269
File: batch/github/fetcher.ts:1208-1217
Timestamp: 2026-03-31T08:36:33.653Z
Learning: In coji/upflow's `batch/github/fetcher.ts`, the `comments()` function intentionally uses a single GraphQL query (`GetPullRequestCommentsQuery`) with two non-standard cursor variables (`commentsCursor` and `reviewThreadsCursor`). Passing `null` for the unused cursor on each side is by design — splitting into two separate queries would double API calls. The `reviewThreads.comments` being capped at `first: 100` without nested pagination is also a known, accepted constraint. Do not flag this pattern as a bug or suggest query splitting.

Learnt from: CR
Repo: coji/upflow PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T09:33:01.662Z
Learning: Applies to **/*.{ts,tsx} : Every UPDATE/DELETE on shared database org-scoped tables must include `WHERE organizationId = ?` with server-derived organization ID

@coji coji merged commit 386b835 into main Apr 8, 2026
7 checks passed
@coji coji deleted the feat/issue-283-schema branch April 8, 2026 10:49
coji added a commit that referenced this pull request Apr 8, 2026
…queries (#283 PR 2/7) (#296)

## Summary

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

設計根拠: [`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)

## 変更内容

### 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`)

- `getGithubAppLinkByOrgId` → `getGithubAppLinksByOrgId`(配列返却)
- `getAllGithubAppLinks` を `Map.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

- `lib` を `ES2024` に bump(`Map.groupBy` を使うため)

### tests

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

## 満たす受入条件

- **#6**: `crawl.server.ts` / `backfill.server.ts` が repository ごとに対応 installation の Octokit を使う

## Stack 位置

```text
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 エラーにする

## テスト

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

🤖 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のリンク接続・切断イベントに対する監査ログ機能を追加しました。

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

* **改善**
  * GitHub統合に関する検証とエラー処理を堅牢化しました。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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 -->
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