feat(batch): backfill-installation-membership CLI for PR 7 deploy prep (#283 PR 6/7)#293
feat(batch): backfill-installation-membership CLI for PR 7 deploy prep (#283 PR 6/7)#293coji wants to merge 1 commit intofeat/issue-283-repo-uifrom
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughウォークスルーGitHub インストール ID とリポジトリメンバーシップ関係を一括でバックフィルするための新しい CLI コマンドを追加しました。このコマンドは、GitHub API をクエリして各組織のアクティブなインストールリンクを特定し、対応するリポジトリレコードと 変更
シーケンスダイアグラムsequenceDiagram
participant CLI
participant Cmd as backfillInstallationMembershipCommand
participant SharedDB as Shared DB
participant TenantDB as Tenant DB
participant GitHubAPI as GitHub API
CLI->>Cmd: organizationId, dryRun
Cmd->>SharedDB: Query organizations + integrations
SharedDB-->>Cmd: org details, integration method
alt integration_method != github_app
Cmd->>Cmd: Record skip (non-GitHub-App)
else github_app integration
Cmd->>SharedDB: Query active installation links
alt No active links
Cmd->>Cmd: Record skip (no link)
else Multiple active links
Cmd->>Cmd: Record skip (multi-link)
else Single active link
Cmd->>TenantDB: Query orphan repos<br/>(githubInstallationId IS NULL)
TenantDB-->>Cmd: orphan repositories
alt orphans exist and not dryRun
Cmd->>TenantDB: Update repos: set githubInstallationId
Cmd->>GitHubAPI: fetchInstallationRepositories()
alt API succeeds
GitHubAPI-->>Cmd: repo list
Cmd->>TenantDB: Upsert memberships (all repos)
else API fails
GitHubAPI-->>Cmd: error
Cmd->>TenantDB: Upsert memberships (orphans only)
end
Cmd->>Cmd: Record success
else No orphans or dryRun
Cmd->>Cmd: Record already backfilled
end
end
end
Cmd->>Cmd: Log summary box
Cmd-->>CLI: completion
推定コードレビュー時間🎯 4 (複雑) | ⏱️ ~45 分 ウサギの詩
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
5d2e610 to
8d629f8
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 `@batch/commands/backfill-installation-membership.ts`:
- Around line 169-171: The catch block currently formats the exception using a
manual fallback (String(e)) in the consola.warn call; replace this with the
shared helper getErrorMessage from app/libs/error-message.ts to consistently
extract the message (e.g., call getErrorMessage(e) when building the log string
in the catch for the backfill membership seeding), ensuring
consola.warn(`[${org.organizationName}] failed to seed membership table from
API: ${getErrorMessage(e)}`) is used instead of String(e).
- Around line 133-181: The current early "continue" when orphans.length === 0
skips membership seeding for orgs that already have githubInstallationId set,
causing missing repository_installation_memberships and making retries unable to
recover; remove that early continue and split the operations so
tenantDb.updateTable('repositories') (in the non-dryRun path) only handles
setting githubInstallationId, and then always attempt membership seeding for the
installation by calling fetchInstallationRepositories(installationId) followed
by initializeMembershipsForInstallation({ organizationId: orgId, installationId,
repositories: apiRepos }) inside the try/catch; on API failure fall back to
upserting memberships via upsertRepositoryMembership for either the original
orphans list or the repos you just assigned, and add a test that covers the case
"githubInstallationId is populated but repository_installation_memberships is
empty" to ensure the seed still runs.
🪄 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: 4a07e50e-687e-4a81-b37f-109b3aa06e02
📒 Files selected for processing (3)
batch/cli.tsbatch/commands/backfill-installation-membership.test.tsbatch/commands/backfill-installation-membership.ts
| if (orphans.length === 0) { | ||
| summaries.push({ | ||
| organizationId: org.organizationId, | ||
| organizationName: org.organizationName, | ||
| status: 'backfilled_single_link', | ||
| repositoryCount: 0, | ||
| installationId, | ||
| notes: 'Already up-to-date.', | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| consola.info( | ||
| `[${org.organizationName}] backfilling ${orphans.length} repositories to installation ${installationId}...`, | ||
| ) | ||
|
|
||
| if (!props.dryRun) { | ||
| await tenantDb | ||
| .updateTable('repositories') | ||
| .set({ githubInstallationId: installationId }) | ||
| .where('githubInstallationId', 'is', null) | ||
| .execute() | ||
|
|
||
| // Prefer the GitHub API as the membership seed source — it knows | ||
| // every repo the installation can see, not just the ones we have | ||
| // already tracked. If the API call fails, fall back to upserting | ||
| // memberships for the orphans we just assigned (so canonical | ||
| // reassignment after a future link loss has at least *something* | ||
| // to find). | ||
| try { | ||
| const apiRepos = await fetchInstallationRepositories(installationId) | ||
| await initializeMembershipsForInstallation({ | ||
| organizationId: orgId, | ||
| installationId, | ||
| repositories: apiRepos, | ||
| }) | ||
| } catch (e) { | ||
| consola.warn( | ||
| `[${org.organizationName}] failed to seed membership table from API: ${e instanceof Error ? e.message : String(e)}`, | ||
| ) | ||
| for (const repo of orphans) { | ||
| await upsertRepositoryMembership({ | ||
| organizationId: orgId, | ||
| installationId, | ||
| repositoryId: repo.id, | ||
| }) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
repository_installation_memberships の初期化を orphans.length に依存させないでください。
Line 133 の早期 continue のせいで、githubInstallationId は既に埋まっているのに membership だけ未初期化な org を Already up-to-date. として素通りします。さらに Line 150 の UPDATE 後に seed 側で失敗したケースも、再実行時は orphan が 0 件になって回収できません。repo への backfill と membership seed は分離して、single-link org では orphan が 0 件でも seed を継続できる形にしておきたいです。あわせて「githubInstallationId は埋まっているが membership が空」の回帰ケースをテストに追加すると安全です。
💡 修正イメージ
- if (orphans.length === 0) {
+ const needsRepoBackfill = orphans.length > 0
+ if (!needsRepoBackfill && props.dryRun) {
summaries.push({
organizationId: org.organizationId,
organizationName: org.organizationName,
status: 'backfilled_single_link',
repositoryCount: 0,
installationId,
notes: 'Already up-to-date.',
})
continue
}
- if (!props.dryRun) {
+ if (!props.dryRun && needsRepoBackfill) {
await tenantDb
.updateTable('repositories')
.set({ githubInstallationId: installationId })
.where('githubInstallationId', 'is', null)
.execute()
+ }
+ if (!props.dryRun) {
try {
const apiRepos = await fetchInstallationRepositories(installationId)
await initializeMembershipsForInstallation({
organizationId: orgId,
installationId,
repositories: apiRepos,
})
} catch (e) {
// orphan-only fallback can stay scoped to `orphans`
}
}📝 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.
| if (orphans.length === 0) { | |
| summaries.push({ | |
| organizationId: org.organizationId, | |
| organizationName: org.organizationName, | |
| status: 'backfilled_single_link', | |
| repositoryCount: 0, | |
| installationId, | |
| notes: 'Already up-to-date.', | |
| }) | |
| continue | |
| } | |
| consola.info( | |
| `[${org.organizationName}] backfilling ${orphans.length} repositories to installation ${installationId}...`, | |
| ) | |
| if (!props.dryRun) { | |
| await tenantDb | |
| .updateTable('repositories') | |
| .set({ githubInstallationId: installationId }) | |
| .where('githubInstallationId', 'is', null) | |
| .execute() | |
| // Prefer the GitHub API as the membership seed source — it knows | |
| // every repo the installation can see, not just the ones we have | |
| // already tracked. If the API call fails, fall back to upserting | |
| // memberships for the orphans we just assigned (so canonical | |
| // reassignment after a future link loss has at least *something* | |
| // to find). | |
| try { | |
| const apiRepos = await fetchInstallationRepositories(installationId) | |
| await initializeMembershipsForInstallation({ | |
| organizationId: orgId, | |
| installationId, | |
| repositories: apiRepos, | |
| }) | |
| } catch (e) { | |
| consola.warn( | |
| `[${org.organizationName}] failed to seed membership table from API: ${e instanceof Error ? e.message : String(e)}`, | |
| ) | |
| for (const repo of orphans) { | |
| await upsertRepositoryMembership({ | |
| organizationId: orgId, | |
| installationId, | |
| repositoryId: repo.id, | |
| }) | |
| } | |
| } | |
| } | |
| const needsRepoBackfill = orphans.length > 0 | |
| if (!needsRepoBackfill && props.dryRun) { | |
| summaries.push({ | |
| organizationId: org.organizationId, | |
| organizationName: org.organizationName, | |
| status: 'backfilled_single_link', | |
| repositoryCount: 0, | |
| installationId, | |
| notes: 'Already up-to-date.', | |
| }) | |
| continue | |
| } | |
| consola.info( | |
| `[${org.organizationName}] backfilling ${orphans.length} repositories to installation ${installationId}...`, | |
| ) | |
| if (!props.dryRun && needsRepoBackfill) { | |
| await tenantDb | |
| .updateTable('repositories') | |
| .set({ githubInstallationId: installationId }) | |
| .where('githubInstallationId', 'is', null) | |
| .execute() | |
| } | |
| if (!props.dryRun) { | |
| // Prefer the GitHub API as the membership seed source — it knows | |
| // every repo the installation can see, not just the ones we have | |
| // already tracked. If the API call fails, fall back to upserting | |
| // memberships for the orphans we just assigned (so canonical | |
| // reassignment after a future link loss has at least *something* | |
| // to find). | |
| try { | |
| const apiRepos = await fetchInstallationRepositories(installationId) | |
| await initializeMembershipsForInstallation({ | |
| organizationId: orgId, | |
| installationId, | |
| repositories: apiRepos, | |
| }) | |
| } catch (e) { | |
| consola.warn( | |
| `[${org.organizationName}] failed to seed membership table from API: ${e instanceof Error ? e.message : String(e)}`, | |
| ) | |
| for (const repo of orphans) { | |
| await upsertRepositoryMembership({ | |
| organizationId: orgId, | |
| installationId, | |
| repositoryId: repo.id, | |
| }) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@batch/commands/backfill-installation-membership.ts` around lines 133 - 181,
The current early "continue" when orphans.length === 0 skips membership seeding
for orgs that already have githubInstallationId set, causing missing
repository_installation_memberships and making retries unable to recover; remove
that early continue and split the operations so
tenantDb.updateTable('repositories') (in the non-dryRun path) only handles
setting githubInstallationId, and then always attempt membership seeding for the
installation by calling fetchInstallationRepositories(installationId) followed
by initializeMembershipsForInstallation({ organizationId: orgId, installationId,
repositories: apiRepos }) inside the try/catch; on API failure fall back to
upserting memberships via upsertRepositoryMembership for either the original
orphans list or the repos you just assigned, and add a test that covers the case
"githubInstallationId is populated but repository_installation_memberships is
empty" to ensure the seed still runs.
| } catch (e) { | ||
| consola.warn( | ||
| `[${org.organizationName}] failed to seed membership table from API: ${e instanceof Error ? e.message : String(e)}`, |
There was a problem hiding this comment.
例外メッセージ抽出は getErrorMessage() に統一してください。
ここだけ独自の String(e) フォールバックになっていて、throw された値によってはログが崩れます。既存ヘルパーに寄せた方が安全です。
🔧 修正例
+import { getErrorMessage } from '~/app/libs/error-message'
import { db } from '~/app/services/db.server'
...
consola.warn(
- `[${org.organizationName}] failed to seed membership table from API: ${e instanceof Error ? e.message : String(e)}`,
+ `[${org.organizationName}] failed to seed membership table from API: ${getErrorMessage(e)}`,
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@batch/commands/backfill-installation-membership.ts` around lines 169 - 171,
The catch block currently formats the exception using a manual fallback
(String(e)) in the consola.warn call; replace this with the shared helper
getErrorMessage from app/libs/error-message.ts to consistently extract the
message (e.g., call getErrorMessage(e) when building the log string in the catch
for the backfill membership seeding), ensuring
consola.warn(`[${org.organizationName}] failed to seed membership table from
API: ${getErrorMessage(e)}`) is used instead of String(e).
8d629f8 to
95f0e0c
Compare
19f78bb to
4e18e33
Compare
44a5cda to
aa36656
Compare
d0f0617 to
26f67f3
Compare
aa36656 to
3c83597
Compare
26f67f3 to
f573893
Compare
3c83597 to
ea800f0
Compare
f573893 to
b9ba6fb
Compare
ea800f0 to
18a9df2
Compare
b9ba6fb to
ddaae6e
Compare
18a9df2 to
9ae43b8
Compare
ddaae6e to
577b1d8
Compare
9ae43b8 to
1e0c6e8
Compare
577b1d8 to
042e55c
Compare
1e0c6e8 to
d0da8d7
Compare
#283 PR 6/7) One-shot data migration that runs after PR 1's schema deploy and before PR 7 flips repository lookups to strict mode. Assigns github_installation_id and seeds repository_installation_memberships for organizations whose GitHub App mode has exactly one active installation. batch/commands/backfill-installation-membership.ts: - Decision rules per org: - integrations.method = 'token' → skip (PAT mode never uses installation ids) - 0 active GitHub App links → skip + warn (org needs reinstall before PR 7) - exactly 1 active link → assign every NULL repo to that installation - 2+ active links → skip and list (operator must use reassign-broken-repositories) - --dry-run flag for safe inspection - Best-effort GitHub API call to seed memberships from installation_repositories; failures fall back to crawl-time membership repair - Cross-store rule (tenant first / shared second) maintained - Idempotent: WHERE github_installation_id IS NULL filter batch/cli.ts: - registered backfill-installation-membership command tests (backfill-installation-membership.test.ts): - single active link → backfills repos and memberships - token method → skipped, no writes - multi active link → not backfilled - dry-run does not write - idempotent: re-running on already-backfilled rows is a no-op Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d0da8d7 to
e6a4166
Compare
042e55c to
bc5e2e9
Compare

Summary
Issue #283 の実装 stack PR 6/7 — PR 7 (strict lookup 切替) のデプロイ準備を行う one-shot batch CLI。
設計根拠: `docs/rdd/issue-283-multiple-github-accounts.md`
作業計画: `docs/rdd/issue-283-work-plan.md`
依存: #288 (PR 1: schema), #296 (PR 2), #290 (PR 3), #291 (PR 4), #292 (PR 5)
変更内容
batch CLI (`batch/commands/backfill-installation-membership.ts`)
`pnpm batch backfill-installation-membership [orgId] [--dry-run]`
組織ごとに以下の判定:
動作詳細
Runbook (本番デプロイ手順)
```bash
pnpm batch backfill-installation-membership -- --dry-run
```
```bash
pnpm batch backfill-installation-membership
```
```sql
SELECT count(*) FROM repositories WHERE github_installation_id IS NULL;
```
Stack 位置
```text
PR 1 (#288): schema
└ PR 2 (#296): query/octokit
└ PR 3 (#290): webhook/membership
└ PR 4 (#291): UI
└ PR 5 (#292): repo UI
└ [PR 6: backfill] ← this PR
└ PR 7 (strict)
```
満たす受入条件
テスト
🤖 Generated with Claude Code
Summary by CodeRabbit
リリースノート
backfill-installation-membershipコマンドを CLI に追加しました。このコマンドは GitHub App インストール連携時に、リポジトリのインストール ID とメンバーシップ情報を一括更新する機能を提供します。--dryRunフラグでプレビュー実行も可能です。