feat(ui): multi-installation aware settings UI (#283 PR 4/7)#291
feat(ui): multi-installation aware settings UI (#283 PR 4/7)#291
Conversation
📝 WalkthroughWalkthroughGitHub Appの単一リンクモデルを複数インストール対応へ移行。インストール単位のメタデータ・表示ラベル・設定URL・切断フロー・リポジトリ取得/追加/検証を分散化して実装した。 Changes
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: 表示更新
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
af71c5b to
be0b946
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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:copyFetcherのuseEffect依存配列について。リポジトリのルールでは
fetcherオブジェクト自体を依存配列に含めることを避けるべきとされていますが、ここではcopyFetcher.data、copyFetcher.reset、copyFetcher.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
📒 Files selected for processing (13)
app/libs/github-account.tsapp/routes/$orgSlug/settings/_index/+forms/integration-settings.tsxapp/routes/$orgSlug/settings/_index/+schema.tsapp/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.tsapp/routes/$orgSlug/settings/integration/index.tsxapp/routes/$orgSlug/settings/repositories.add/+components/repository-item.tsxapp/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.test.tsapp/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.tsapp/routes/$orgSlug/settings/repositories.add/+functions/get-repositories-by-owner-and-keyword.tsapp/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.tsapp/routes/$orgSlug/settings/repositories.add/index.tsxapp/services/github-integration-queries.server.tsdocs/rdd/issue-283-work-plan.md
6768429 to
1c62eb5
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
1c62eb5 to
e40a7c8
Compare
ad26a55 to
9072111
Compare
e40a7c8 to
763a5c9
Compare
5d28a9b to
047cd20
Compare
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>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- 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>
There was a problem hiding this comment.
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.dataとcopyFetcher.resetを依存配列から除外してください。この repo では
useFetcher()の戻り値は render ごとに新しい参照として扱う前提です。現在のコードは[copyFetcher.state, copyFetcher.data, copyFetcher.reset]ですが、copyFetcher.dataとcopyFetcher.resetは毎 render で新しい参照になるため、effect が毎 render 再実行されます。安全な選択肢は 2 つです:
- 依存配列を
[copyFetcher.state]のみにする(state は primitive で安定している)- 依存配列を
[]にして、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)に寄せると、非Errorthrow も一貫して扱えます。♻️ 修正案
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()fromapp/libs/error-message.tsto extract error messages, never useString(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
📒 Files selected for processing (13)
app/libs/github-account.tsapp/routes/$orgSlug/settings/_index/+forms/integration-settings.tsxapp/routes/$orgSlug/settings/_index/+schema.tsapp/routes/$orgSlug/settings/github-users._index/+functions/search-github-users.server.tsapp/routes/$orgSlug/settings/integration/index.tsxapp/routes/$orgSlug/settings/repositories.add/+components/repository-item.tsxapp/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.test.tsapp/routes/$orgSlug/settings/repositories.add/+functions/get-installation-repos.tsapp/routes/$orgSlug/settings/repositories.add/+functions/get-repositories-by-owner-and-keyword.tsapp/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.tsapp/routes/$orgSlug/settings/repositories.add/index.tsxapp/services/github-integration-queries.server.tsdocs/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
| 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') | ||
| } |
There was a problem hiding this comment.
部分失敗時に 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.
| 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.
| try { | ||
| await assertInstallationBelongsToOrg( | ||
| organization.id, | ||
| submission.value.installationId, | ||
| ) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
♻️ 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
📒 Files selected for processing (4)
app/routes/$orgSlug/settings/_index/+forms/integration-settings.tsxapp/routes/$orgSlug/settings/integration/index.tsxapp/routes/$orgSlug/settings/repositories.add/index.tsxapp/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

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`)
GitHub App セクション UI
repositories.add page (Vercel 風 merge UI)
github-users page
共有 helpers
満たす受入条件
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 として透過的に処理される。
テスト
🤖 Generated with Claude Code
Summary by CodeRabbit
リリースノート
新機能
改善