Skip to content

feat(ui): multi-installation aware settings UI (#283 PR 4/7)#291

Open
coji wants to merge 2 commits intomainfrom
feat/issue-283-ui
Open

feat(ui): multi-installation aware settings UI (#283 PR 4/7)#291
coji wants to merge 2 commits intomainfrom
feat/issue-283-ui

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Apr 7, 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-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/`, Organization → `/organizations//settings/installations`)
  • `app/services/github-integration-queries.server.ts`:
    • `getActiveInstallationOptions` 共通 helper

満たす受入条件

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 として透過的に処理される。

テスト

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

🤖 Generated with Claude Code

Summary by CodeRabbit

リリースノート

  • 新機能

    • 複数のGitHub Appインストールを個別に表示するインストールカードを追加。アカウントごとの状態(サスペンド・初期化中)表示、別アカウントの追加が可能になりました。
  • 改善

    • インストール単位での接続解除(確認ダイアログ付き)を導入。リポジトリ追加は全アクティブインストールを横断して検索・重複排除し、取得失敗時は警告を表示します。リポジトリにインストール情報を保持。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

GitHub Appの単一リンクモデルを複数インストール対応へ移行。インストール単位のメタデータ・表示ラベル・設定URL・切断フロー・リポジトリ取得/追加/検証を分散化して実装した。

Changes

Cohort / File(s) Summary
GitHub アカウント共通ユーティリティ
app/libs/github-account.ts
新規追加。GithubAccountLike型、isPersonalAccountformatGithubAccountLabelbuildInstallationSettingsUrlを導入。アカウント種別判定と設定ページURL/ラベル生成を集中。
統合設定フォーム(UI)
app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx
githubAppLinkgithubAppLinks[]へ変更。各インストールを個別カードで描画、インストール単位の切断確認(ConfirmDialog)と設定リンクを追加。フォーム型を厳密化。
アクションスキーマとINTENTS
app/routes/$orgSlug/settings/_index/+schema.ts
INTENTSdisconnectGithubAppLink/confirmDisconnectGithubAppLinkを追加。Zodのdiscriminated unionでintegrationActionSchemaを導入(installationId検証含む)。
統合ルート(loader/action)
app/routes/$orgSlug/settings/integration/index.tsx
loaderで全インストールを返すgithubAppLinksを導入。actionをts-patternで分岐、org所有確認とインストール単位切断フロー(assertInstallationBelongsToOrg)を追加。インストール用の生成URLロジックを整理。
アクティブインストール照会
app/services/github-integration-queries.server.ts
ActiveInstallationOption型とgetActiveInstallationOptionsを追加。サスペンド済みを除外してUI向け選択肢を構築。assertInstallationBelongsToOrgAppErrorを投げるよう変更。
リポジトリ追加フロー(loader/action/UI)
app/routes/$orgSlug/settings/repositories.add/index.tsx, .../+components/repository-item.tsx
複数インストールの並列取得(Promise.allSettled)、デデュープ、installationIdを各Repositoryに付与。フォームにinstallationIdを受け付け、サーバー側で所有検証後にaddRepositoryへ渡す。部分取得失敗の警告追加。
リポジトリ取得ユーティリティとテスト
app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts, .../get-installation-repos.test.ts, .../get-repositories-by-owner-and-keyword.ts, .../mutations.server.ts
TaggedInstallationRepo型を追加。owners抽出/フィルタ/マッピングをtagged構造へ適用。RepositoryinstallationIdを追加。addRepositorygithubInstallationIdを受け保存し、membership upsert失敗時はフラグを返す新型AddRepositoryResultを導入。
GitHubユーザー検索
app/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.ts
getActiveInstallationOptionsから最初のアクティブインストールを選び、インストール向けOctokitを構築する分岐を追加。トークン欠如やintegration未設定時は早期に空配列を返す。
ドキュメント
docs/rdd/issue-283-work-plan.md
UI方針を更新(インストール非表示の統合ビュー、並列取得・デデュープ、インストール単位検証、個別切断フローなど)を反映。

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as "Repository\nAdd Form"
    participant Action as "Route\nAction"
    participant InstallOpts as "getActiveInstallation\nOptions"
    participant RepoFetch as "fetchAllInstallation\nRepos (parallel)"
    participant Assert as "assertInstallation\nBelongsToOrg"
    participant Repo as "addRepository\nMutation"
    participant Membership as "upsertRepository\nMembership"
    participant DB as "Organization/DB"

    User->>UI: リポジトリ追加フォーム送信(hidden: installationId)
    UI->>Action: POST (owner,name,installationId)
    Action->>InstallOpts: getActiveInstallationOptions(orgId)
    InstallOpts-->>Action: ActiveInstallationOption[]
    Action->>RepoFetch: 並列で各installationのreposを取得
    RepoFetch-->>Action: TaggedInstallationRepo[] / 失敗情報
    Action->>Assert: assertInstallationBelongsToOrg(orgId, installationId)
    Assert->>DB: 所有権確認クエリ
    DB-->>Assert: 所有確認結果
    Assert-->>Action: OK / エラー
    Action->>Repo: addRepository(orgId, {owner,repo,githubInstallationId})
    Repo->>DB: repositories upsert (githubInstallationId 保存)
    alt githubInstallationId != null
        Repo->>Membership: upsertRepositoryMembership(orgId, installationId, repositoryId)
        Membership-->>Repo: 成功 or エラー
    end
    Repo-->>Action: AddRepositoryResult { id, membershipUpsertFailed }
    Action-->>UI: 成功 / エラーレスポンス
    UI-->>User: 表示更新
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 ぴょんと飛んだインストールたち、ひとつずつ名札つけたよ
ラベル揃え、設定リンクもぽんと置いたよ
並列で拾って重複はきれいにまとめて
切断は個別に、確認はそっと優しく
さあ春の掃除だ、リポジトリもすっきり 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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 プルリクエストのタイトルは、複数インストレーション対応の設定UIという主要な変更を明確かつ簡潔に要約しており、変更内容と関連している。

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

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 force-pushed the feat/issue-283-ui branch 2 times, most recently from af71c5b to be0b946 Compare April 7, 2026 15:14
@coji coji marked this pull request as ready for review April 7, 2026 15:14
@coji
Copy link
Copy Markdown
Owner Author

coji commented Apr 7, 2026

@coderabbitai review

@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: 1

🧹 Nitpick comments (2)
app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts (1)

15-24: 型定義の配置順序を整理することを推奨します。

TaggedInstallationRepo 型が extractOwners 関数(15行目)で使用された後に定義されています(21-24行目)。TypeScript ではホイスティングにより動作しますが、可読性のため型定義を使用箇所より前に移動することをお勧めします。

♻️ 提案する修正
 type InstallationRepo = Awaited<ReturnType<typeof fetchAllInstallationRepos>>[0]

+export type TaggedInstallationRepo = {
+  installationId: number
+  repo: InstallationRepo
+}
+
 /** Extract unique owners from pre-fetched installation repos. */
 export function extractOwners(tagged: TaggedInstallationRepo[]): string[] {
   return [...new Set(tagged.map((t) => t.repo.owner.login))].sort((a, b) =>
     a.localeCompare(b, undefined, { sensitivity: 'base' }),
   )
 }
-
-export type TaggedInstallationRepo = {
-  installationId: number
-  repo: InstallationRepo
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/routes/`$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts
around lines 15 - 24, extractOwners が TaggedInstallationRepo
型を参照する前に型定義が後置されているので、可読性向上のため TaggedInstallationRepo の定義を extractOwners
より上に移動してください。具体的にはファイル内で TaggedInstallationRepo 型宣言を extractOwners
関数の前に置き、型名(TaggedInstallationRepo)と関数名(extractOwners)をそのまま使って参照が先に来るように並べ替えてください。
app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx (1)

153-170: copyFetcheruseEffect 依存配列について。

リポジトリのルールでは fetcher オブジェクト自体を依存配列に含めることを避けるべきとされていますが、ここでは copyFetcher.datacopyFetcher.resetcopyFetcher.state という個別のプロパティを使用しています。Biome がこれをフラグしない限り問題ありませんが、copyFetcher.reset が不安定な参照である可能性があることに注意してください。

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

In `@app/routes/`$orgSlug/settings/_index/+forms/integration-settings.tsx around
lines 153 - 170, The useEffect watching copyFetcher properties may be relying on
an unstable reference (copyFetcher.reset); to fix, either depend on the whole
fetcher object instead of individual props (replace the dependency array with
[copyFetcher]) or stabilize the reset callback (create a stable wrapper via
useCallback that calls copyFetcher.reset and use that in the deps). Locate the
useEffect that references copyFetcher.data, copyFetcher.state, and
copyFetcher.reset and apply one of these two fixes so the effect won't retrigger
due to an unstable reset reference.
🤖 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/routes/`$orgSlug/settings/integration/index.tsx:
- Around line 70-82: Move the org membership/auth check to run before parsing
form data: call requireOrgMember (or requireOrgAdmin as appropriate) at the
start of the exported action function using the same context/org info, and only
after that call await request.formData() and parseWithZod(formData, { schema:
integrationActionSchema }); ensure you keep the existing use of
context.get(orgContext) or use the returned org from requireOrgMember and
preserve the same error handling/intent response flow if authentication fails.

---

Nitpick comments:
In `@app/routes/`$orgSlug/settings/_index/+forms/integration-settings.tsx:
- Around line 153-170: The useEffect watching copyFetcher properties may be
relying on an unstable reference (copyFetcher.reset); to fix, either depend on
the whole fetcher object instead of individual props (replace the dependency
array with [copyFetcher]) or stabilize the reset callback (create a stable
wrapper via useCallback that calls copyFetcher.reset and use that in the deps).
Locate the useEffect that references copyFetcher.data, copyFetcher.state, and
copyFetcher.reset and apply one of these two fixes so the effect won't retrigger
due to an unstable reset reference.

In
`@app/routes/`$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts:
- Around line 15-24: extractOwners が TaggedInstallationRepo
型を参照する前に型定義が後置されているので、可読性向上のため TaggedInstallationRepo の定義を extractOwners
より上に移動してください。具体的にはファイル内で TaggedInstallationRepo 型宣言を extractOwners
関数の前に置き、型名(TaggedInstallationRepo)と関数名(extractOwners)をそのまま使って参照が先に来るように並べ替えてください。
🪄 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: 8155a088-29a6-4de5-accc-73a0d974fda6

📥 Commits

Reviewing files that changed from the base of the PR and between 6768429 and be0b946.

📒 Files selected for processing (13)
  • app/libs/github-account.ts
  • app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx
  • app/routes/$orgSlug/settings/_index/+schema.ts
  • app/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.ts
  • app/routes/$orgSlug/settings/integration/index.tsx
  • app/routes/$orgSlug/settings/repositories.add/+components/repository-item.tsx
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.test.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-repositories-by-owner-and-keyword.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/repositories.add/index.tsx
  • app/services/github-integration-queries.server.ts
  • docs/rdd/issue-283-work-plan.md

@coji
Copy link
Copy Markdown
Owner Author

coji commented Apr 8, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 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.

@coji coji force-pushed the feat/issue-283-webhook-membership branch from 1c62eb5 to e40a7c8 Compare April 8, 2026 11:04
@coji coji force-pushed the feat/issue-283-ui branch 2 times, most recently from ad26a55 to 9072111 Compare April 8, 2026 13:21
@coji coji force-pushed the feat/issue-283-webhook-membership branch from e40a7c8 to 763a5c9 Compare April 8, 2026 13:21
@coji coji force-pushed the feat/issue-283-ui branch 5 times, most recently from 5d28a9b to 047cd20 Compare April 8, 2026 15:32
@coji coji changed the base branch from feat/issue-283-webhook-membership to graphite-base/291 April 8, 2026 15:41
@coji coji force-pushed the feat/issue-283-ui branch from 047cd20 to 5ab3838 Compare April 8, 2026 15:42
@coji coji force-pushed the graphite-base/291 branch from 38c4046 to fe8ba1a Compare April 8, 2026 15:42
@graphite-app graphite-app bot changed the base branch from graphite-base/291 to main April 8, 2026 15:42
integration page (settings/integration):
- loader returns githubAppLinks[] array with installation status
- action refactored to discriminated union + parseWithZod + match (CLAUDE.md 規約)
- new INTENTS: disconnectGithubAppLink, confirmDisconnectGithubAppLink
- per-installation disconnect with assertInstallationBelongsToOrg validation

GitHub App section UI:
- InstallationCard renders one card per active installation
- per-installation fetcher + ConfirmDialog (preserves installationId)
- "Add another GitHub account" button when at least one link exists
- buildInstallationSettingsUrl branches by github_account_type (User → /settings/installations/<id>, Organization → /organizations/<login>/settings/installations)

repositories.add page:
- installation selector via shared <InstallationSelect> when 2+ active links
- per-installation fetchAllInstallationRepos cache key
- loader/action share resolveInstallationIdForApp helper for installation resolution
- assertInstallationBelongsToOrg server-side validation
- addRepository mutation accepts githubInstallationId + upsertRepositoryMembership

github-users page:
- loader returns installationOptions via shared getActiveInstallationOptions
- searchGithubUsers accepts installationId, validates via assertInstallationBelongsToOrg
- toolbar uses shared <InstallationSelect>; selection persisted in URL searchParams
- Add button disabled when 2+ installations and none selected (UX feedback)

shared helpers added:
- app/libs/github-account.ts: formatGithubAccountLabel, isPersonalAccount, buildInstallationSettingsUrl
- app/components/installation-select.tsx: <InstallationSelect> reusable component
- app/services/github-integration-queries.server.ts: getActiveInstallationOptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coji coji force-pushed the feat/issue-283-ui branch from 5ab3838 to 073e9fe Compare April 8, 2026 15:43
@coji
Copy link
Copy Markdown
Owner Author

coji commented Apr 8, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 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.

- useEffect on copyFetcher: depend on the stable primitive fields
  (state, data, reset) instead of the whole fetcher object, which
  is a fresh reference each render and caused the effect to wake
  up on unrelated parent re-renders.
- Extract the duplicated "GitHub App is not configured" error path
  from installGithubApp / copyInstallUrl into a shared helper.
- repositories.add action: use getErrorMessage(e) instead of the
  hand-rolled `e instanceof Error ? ... : 'Invalid installation id'`
  so the codebase-wide error extraction rule (CLAUDE.md) is honored.
- assertInstallationBelongsToOrg: throw AppError (user-safe) instead
  of plain Error so getErrorMessage surfaces the real reason to the
  caller instead of the generic fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx (1)

153-170: ⚠️ Potential issue | 🟡 Minor

copyFetcher.datacopyFetcher.reset を依存配列から除外してください。

この repo では useFetcher() の戻り値は render ごとに新しい参照として扱う前提です。現在のコードは [copyFetcher.state, copyFetcher.data, copyFetcher.reset] ですが、copyFetcher.datacopyFetcher.reset は毎 render で新しい参照になるため、effect が毎 render 再実行されます。

安全な選択肢は 2 つです:

  1. 依存配列を [copyFetcher.state] のみにする(state は primitive で安定している)
  2. 依存配列を [] にして、biome-ignore lint/correctness/useExhaustiveDependencies で意図を明示する
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/_index/+forms/integration-settings.tsx around
lines 153 - 170, The effect currently depends on unstable object refs
(copyFetcher.data and copyFetcher.reset) causing it to run every render; change
the dependency array to use only the stable primitive copyFetcher.state (i.e.
useEffect(..., [copyFetcher.state])) or, if you intend a one-time effect, use an
empty array with a biome-ignore comment; keep the effect body as-is (matching
INTENTS.copyInstallUrl, calling navigator.clipboard.writeText and toast.success)
and still call copyFetcher.reset inside the effect body but do not include
copyFetcher.reset in the dependency list.
🧹 Nitpick comments (1)
app/routes/$orgSlug/settings/repositories.add/index.tsx (1)

282-287: エラーメッセージ抽出は getErrorMessage() に揃えてください。

ここだけ手書きの instanceof Error 分岐になっていて、route 側のエラーハンドリング規約から外れています。getErrorMessage(e) に寄せると、非 Error throw も一貫して扱えます。

♻️ 修正案
 import { requireOrgOwner } from '~/app/libs/auth.server'
+import { getErrorMessage } from '~/app/libs/error-message'
 import { captureExceptionToSentry } from '~/app/libs/sentry-node.server'
-          message: e instanceof Error ? e.message : 'Invalid installation id',
+          message: getErrorMessage(e),

As per coding guidelines, "Use getErrorMessage() from app/libs/error-message.ts to extract error messages, never use String(e) which returns [object Error] for Error instances."

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

In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx around lines 282 -
287, The catch block currently extracts the error message with an instanceof
Error check and a hardcoded fallback; replace that manual extraction by calling
getErrorMessage(e) and passing its result into dataWithError so non-Error throws
are handled consistently; update the catch in the function that calls
dataWithError (the same block where you return dataWithError({}, { message: ...
})) to use getErrorMessage and ensure getErrorMessage is imported from
app/libs/error-message.ts.
🤖 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/routes/`$orgSlug/settings/repositories.add/index.tsx:
- Around line 192-198: The current validation throws "invalid owner" when the
query param owner is not found in the rebuilt owners list (computed from
apiOwners and registeredOwners), which causes the whole page to error when some
installations failed; change the check in the block that computes owners (the
variables apiOwners, registeredOwners and owner) to only enforce the strict
owners.includes(owner) throw when there are no failed installations (i.e., when
failedInstallationIds is empty/undefined), otherwise skip or log a warning and
allow the request to proceed so partial failures don't crash the page.
- Around line 277-281: The current check only validates the installation belongs
to the org (assertInstallationBelongsToOrg) but does not verify that the
submitted owner/name is actually accessible via that installation; add a
server-side reconciliation step that, given submission.value.installationId and
submission.value.owner/name, uses the installation-scoped GitHub API token (or
an equivalent helper, e.g., verifyInstallationCanAccessRepo or
fetchRepoWithInstallationToken) to fetch the repository or confirm it appears in
the installation's repo list and throw/return a validation error if not; apply
this extra verification at both places where assertInstallationBelongsToOrg is
called (the try block around submission handling and the similar block
referenced at the later lines 293-296).

---

Outside diff comments:
In `@app/routes/`$orgSlug/settings/_index/+forms/integration-settings.tsx:
- Around line 153-170: The effect currently depends on unstable object refs
(copyFetcher.data and copyFetcher.reset) causing it to run every render; change
the dependency array to use only the stable primitive copyFetcher.state (i.e.
useEffect(..., [copyFetcher.state])) or, if you intend a one-time effect, use an
empty array with a biome-ignore comment; keep the effect body as-is (matching
INTENTS.copyInstallUrl, calling navigator.clipboard.writeText and toast.success)
and still call copyFetcher.reset inside the effect body but do not include
copyFetcher.reset in the dependency list.

---

Nitpick comments:
In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx:
- Around line 282-287: The catch block currently extracts the error message with
an instanceof Error check and a hardcoded fallback; replace that manual
extraction by calling getErrorMessage(e) and passing its result into
dataWithError so non-Error throws are handled consistently; update the catch in
the function that calls dataWithError (the same block where you return
dataWithError({}, { message: ... })) to use getErrorMessage and ensure
getErrorMessage is imported from app/libs/error-message.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: 89c4ea51-2d4d-4687-86c4-88997f5b9019

📥 Commits

Reviewing files that changed from the base of the PR and between be0b946 and 073e9fe.

📒 Files selected for processing (13)
  • app/libs/github-account.ts
  • app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx
  • app/routes/$orgSlug/settings/_index/+schema.ts
  • app/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.ts
  • app/routes/$orgSlug/settings/integration/index.tsx
  • app/routes/$orgSlug/settings/repositories.add/+components/repository-item.tsx
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.test.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-repositories-by-owner-and-keyword.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/repositories.add/index.tsx
  • app/services/github-integration-queries.server.ts
  • docs/rdd/issue-283-work-plan.md
✅ Files skipped from review due to trivial changes (2)
  • app/libs/github-account.ts
  • app/services/github-integration-queries.server.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/routes/$orgSlug/settings/repositories.add/+components/repository-item.tsx
  • app/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.test.ts
  • app/routes/$orgSlug/settings/_index/+schema.ts
  • app/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.ts
  • docs/rdd/issue-283-work-plan.md
  • app/routes/$orgSlug/settings/integration/index.tsx

Comment on lines +192 to 198
const apiOwners = extractOwners(allTagged)
const owners = [...new Set([...apiOwners, ...registeredOwners])].sort(
(a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }),
)
if (owner && !owners.includes(owner)) {
throw new Error('invalid owner')
}
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

部分失敗時に invalid owner でページ全体が落ちます。

owners は成功した installation だけから再構築されるので、失敗した installation にしか存在しない ?owner= が付いたリクエストはここで例外になります。Promise.allSettled() で劣化運転する意図と逆なので、少なくとも failedInstallationIds がある間はこのチェックを緩めた方が安全です。

💡 最小修正例
-  if (owner && !owners.includes(owner)) {
+  if (owner && !owners.includes(owner) && failedInstallationIds.length === 0) {
     throw new Error('invalid owner')
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const apiOwners = extractOwners(allTagged)
const owners = [...new Set([...apiOwners, ...registeredOwners])].sort(
(a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }),
)
if (owner && !owners.includes(owner)) {
throw new Error('invalid owner')
}
const apiOwners = extractOwners(allTagged)
const owners = [...new Set([...apiOwners, ...registeredOwners])].sort(
(a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }),
)
if (owner && !owners.includes(owner) && failedInstallationIds.length === 0) {
throw new Error('invalid owner')
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx around lines 192 -
198, The current validation throws "invalid owner" when the query param owner is
not found in the rebuilt owners list (computed from apiOwners and
registeredOwners), which causes the whole page to error when some installations
failed; change the check in the block that computes owners (the variables
apiOwners, registeredOwners and owner) to only enforce the strict
owners.includes(owner) throw when there are no failed installations (i.e., when
failedInstallationIds is empty/undefined), otherwise skip or log a warning and
allow the request to proceed so partial failures don't crash the page.

Comment on lines +277 to +281
try {
await assertInstallationBelongsToOrg(
organization.id,
submission.value.installationId,
)
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

installationId の所属確認だけでは、追加対象リポジトリを検証しきれていません。

ここで確認しているのは「その installation がこの org に属していて有効か」だけで、owner/name がその installation から実際に見えているかは未検証です。改ざんされた POST で別 installation の owner/name を組み合わせると、そのまま誤った githubInstallationId / membership が保存され、後続の crawl や canonical reassignment が壊れます。追加前に、少なくとも installationId + owner + name をサーバー側で再照合してください。

Also applies to: 293-296

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

In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx around lines 277 -
281, The current check only validates the installation belongs to the org
(assertInstallationBelongsToOrg) but does not verify that the submitted
owner/name is actually accessible via that installation; add a server-side
reconciliation step that, given submission.value.installationId and
submission.value.owner/name, uses the installation-scoped GitHub API token (or
an equivalent helper, e.g., verifyInstallationCanAccessRepo or
fetchRepoWithInstallationToken) to fetch the repository or confirm it appears in
the installation's repo list and throw/return a validation error if not; apply
this extra verification at both places where assertInstallationBelongsToOrg is
called (the try block around submission handling and the similar block
referenced at the later lines 293-296).

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.

♻️ Duplicate comments (2)
app/routes/$orgSlug/settings/repositories.add/index.tsx (2)

270-297: ⚠️ Potential issue | 🟠 Major

installationId だけでなく owner/name との対応も追加前に再検証してください。

ここで保証しているのは「その installation がこの org のもの」という点だけです。hidden input を改ざんされると、別 installation の owner/name を組み合わせたまま githubInstallationId を保存でき、membershipUpsertFailed は insert 後の検知にしかなりません。addRepository() の前に、その installation から当該 repo が実際に見えているかをサーバー側で再照合した方が安全です。

💡 最小修正例
   if (integration.method === 'github_app') {
     if (submission.value.installationId === undefined) {
       return dataWithError(
         {},
         { message: 'Installation id is required for GitHub App mode' },
       )
     }
     try {
       await assertInstallationBelongsToOrg(
         organization.id,
         submission.value.installationId,
       )
+      const octokit = resolveOctokitForInstallation(
+        submission.value.installationId,
+      )
+      const visibleRepos = await getOrgCachedData(
+        organization.id,
+        `app-installation-all-repos:${submission.value.installationId}`,
+        () => fetchAllInstallationRepos(octokit),
+        300000,
+      )
+      const canAccessRepo = visibleRepos.some(
+        (repo) =>
+          repo.owner.login === submission.value.owner &&
+          repo.name === submission.value.name,
+      )
+      if (!canAccessRepo) {
+        return dataWithError(
+          {},
+          { message: 'Selected installation cannot access this repository' },
+        )
+      }
     } catch (e) {
       return dataWithError(
         {},
         {
           message: getErrorMessage(e),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx around lines 270 -
297, Ensure the repo (owner/name) is actually visible to the GitHub App
installation before calling addRepository: after setting installationId (and
after assertInstallationBelongsToOrg) call a verification that uses the
installation's API (e.g., implement/ call
verifyInstallationCanAccessRepo(installationId, submission.value.owner,
submission.value.name) or fetch the repo via the installation's GitHub client)
and return dataWithError if the installation cannot see the repo; only then
proceed to call addRepository(organization.id, {...}) to prevent tampered hidden
inputs from pairing a repo with a wrong installation.

193-199: ⚠️ Potential issue | 🟠 Major

部分失敗時に invalid owner で loader を落とさないでください。

owners は成功した installation だけから再構築されるので、失敗した installation にしか存在しない ?owner= が付いているとここでページ全体が落ちます。Promise.allSettled() で劣化運転する意図と逆なので、少なくとも failedInstallationIds.length > 0 の間はこの検証を緩めた方が安全です。

💡 最小修正例
-  if (owner && !owners.includes(owner)) {
+  if (owner && !owners.includes(owner) && failedInstallationIds.length === 0) {
     throw new Error('invalid owner')
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx around lines 193 -
199, The current strict check that throws new Error('invalid owner') when owner
is not in the computed owners array (built from apiOwners and registeredOwners)
should be relaxed when some installations failed; change the validation around
the owner variable to consult failedInstallationIds (or the settled results) and
only throw when there are no failed installations (failedInstallationIds.length
=== 0); when failures exist, avoid throwing—either log a warning and ignore the
owner parameter or allow it through for degraded operation so the loader does
not crash. Ensure you update the code that references apiOwners, owners,
registeredOwners and owner to use this conditional logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/routes/`$orgSlug/settings/repositories.add/index.tsx:
- Around line 270-297: Ensure the repo (owner/name) is actually visible to the
GitHub App installation before calling addRepository: after setting
installationId (and after assertInstallationBelongsToOrg) call a verification
that uses the installation's API (e.g., implement/ call
verifyInstallationCanAccessRepo(installationId, submission.value.owner,
submission.value.name) or fetch the repo via the installation's GitHub client)
and return dataWithError if the installation cannot see the repo; only then
proceed to call addRepository(organization.id, {...}) to prevent tampered hidden
inputs from pairing a repo with a wrong installation.
- Around line 193-199: The current strict check that throws new Error('invalid
owner') when owner is not in the computed owners array (built from apiOwners and
registeredOwners) should be relaxed when some installations failed; change the
validation around the owner variable to consult failedInstallationIds (or the
settled results) and only throw when there are no failed installations
(failedInstallationIds.length === 0); when failures exist, avoid throwing—either
log a warning and ignore the owner parameter or allow it through for degraded
operation so the loader does not crash. Ensure you update the code that
references apiOwners, owners, registeredOwners and owner to use this conditional
logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd99d600-b282-428c-8bc2-f8f895ee47f9

📥 Commits

Reviewing files that changed from the base of the PR and between 073e9fe and bdef24d.

📒 Files selected for processing (4)
  • app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx
  • app/routes/$orgSlug/settings/integration/index.tsx
  • app/routes/$orgSlug/settings/repositories.add/index.tsx
  • app/services/github-integration-queries.server.ts
✅ Files skipped from review due to trivial changes (2)
  • app/routes/$orgSlug/settings/integration/index.tsx
  • app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsx

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