Skip to content

Conversation

@shuhuiluo
Copy link
Collaborator

@shuhuiluo shuhuiluo commented Nov 25, 2025

…access fails

When a user has OAuth linked but the GitHub App isn't installed on a private repo, show a targeted install URL instead of generic error.

  • Add getOwnerIdFromUsername() to fetch owner ID from public GitHub API
  • Add generateInstallUrl() and sendInstallPrompt() to installation-service
  • Refactor parseRepo() to return tuple [owner, repo]

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive GitHub App installation prompts when access is lacking, with direct install links.
  • Improvements

    • Public API now uniformly accepts full "owner/repo" strings and resolves owner IDs for installation flows.
    • Listing endpoints gain pagination, creator/author and merged-state filtering; clearer validation for malformed repo names.
  • Bug Fixes

    • Issue listings now exclude pull requests.
  • Tests

    • Updated tests to expect validation-style errors for malformed repo names.

✏️ Tip: You can customize this high-level summary in your review settings.

…access fails

When a user has OAuth linked but the GitHub App isn't installed on a
private repo, show a targeted install URL instead of generic error.

- Add getOwnerIdFromUsername() to fetch owner ID from public GitHub API
- Add generateInstallUrl() and sendInstallPrompt() to installation-service
- Refactor parseRepo() to return tuple [owner, repo]

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

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

This PR standardizes repo handling by switching public API functions to accept repoFullName (owner/repo) and introducing parseRepo that returns [owner, repo]. It moves getOwnerIdFromUsername into the GitHub client (now token-less), adds installation helpers generateInstallUrl and sendInstallPrompt, and replaces several local install-URL helpers with the shared generator. Handlers for issues and PRs now validate repo access and may trigger an installation prompt when OAuth access is missing. listPullRequests and listIssues gained pagination and filtering; tests updated to expect validation errors for malformed repo names.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Areas requiring extra attention:

  • Correct destructuring/use of the new parseRepo tuple across all call sites.
  • listPullRequests and listIssues pagination, filtering, and early-termination logic (including merged-state handling).
  • Relocation and signature change of getOwnerIdFromUsername (removed token param) and its call sites.
  • Integration of generateInstallUrl / sendInstallPrompt into handlers and SubscriptionService (ensure no duplicated URL logic remains).
  • Updated tests that now validate repo-format errors instead of mocking API failures.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: showing GitHub App install links for private repos where access is denied, which is the core objective of refactoring the error handling flow.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/install-prompt-error-message

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de1f316 and 915b466.

📒 Files selected for processing (5)
  • src/api/github-client.ts (5 hunks)
  • src/handlers/gh-issue-handler.ts (3 hunks)
  • src/handlers/gh-pr-handler.ts (3 hunks)
  • tests/unit/handlers/gh-issue-handler.test.ts (1 hunks)
  • tests/unit/handlers/gh-pr-handler.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/handlers/gh-issue-handler.ts
  • src/handlers/gh-pr-handler.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Store context externally - maintain stateless bot architecture with no message history, thread context, or conversation memory
Use <@{userId}> for mentions in messages AND add mentions in sendMessage options - do not use @username format
Implement event handlers for onMessage, onSlashCommand, onReaction, onTip, and onInteractionResponse to respond to Towns Protocol events
Define slash commands in src/commands.ts as a const array with name and description properties, then register handlers using bot.onSlashCommand()
Set ID in interaction requests and match ID in responses to correlate form submissions, button clicks, and transaction/signature responses
Use readContract for reading smart contract state, writeContract for SimpleAccount operations, and execute() for external contract interactions
Fund bot.appAddress (Smart Account) for on-chain operations, not bot.botId (Gas Wallet/EOA)
Use bot.* handler methods directly (outside event handlers) for unprompted messages via webhooks, timers, or tasks - requires channelId, spaceId, or other context stored externally
Always check permissions using handler.hasAdminPermission() before performing admin operations like ban, redact, or pin
User IDs are hex addresses in format 0x..., not usernames - use them consistently throughout event handling and message sending
Slash commands do not trigger onMessage - register slash command handlers using bot.onSlashCommand() instead
Use getSmartAccountFromUserId() to retrieve a user's wallet address from their userId
Include required environment variables: APP_PRIVATE_DATA (bot credentials) and JWT_SECRET (webhook security token)

Files:

  • tests/unit/handlers/gh-issue-handler.test.ts
  • src/api/github-client.ts
  • tests/unit/handlers/gh-pr-handler.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Provide alt text for image attachments and use appropriate MIME types for chunked attachments (videos, screenshots)

Files:

  • tests/unit/handlers/gh-issue-handler.test.ts
  • src/api/github-client.ts
  • tests/unit/handlers/gh-pr-handler.test.ts
🔇 Additional comments (10)
tests/unit/handlers/gh-issue-handler.test.ts (1)

141-151: LGTM!

The test correctly reflects the validation-first flow where parseRepo throws before any API call is made. Removing the getIssue mock is appropriate since the handler now validates repository format upfront.

src/api/github-client.ts (8)

39-47: LGTM!

Clean implementation with proper validation. The destructuring correctly handles edge cases where owner or repo would be empty strings (falsy values).


49-61: LGTM!

The validation correctly leverages parseRepo and handles both format errors and API errors uniformly by returning false.


63-76: LGTM!

The function correctly propagates parseRepo errors to callers, enabling handlers to display appropriate error messages.


78-91: LGTM!

Consistent with the updated API pattern.


93-154: LGTM!

Good implementation with pagination guards (maxPages) to prevent excessive API calls. The post-filtering for merged state and author handles GitHub API limitations appropriately.


156-205: LGTM!

Proper filtering of pull requests from the issues endpoint and efficient use of API-level creator filtering.


218-247: LGTM!

Consistent update with proper ETag handling preserved.


248-270: LGTM!

Good fallback strategy trying organization lookup first (most private repos are in orgs) before user lookup. The graceful undefined return allows callers to handle missing owner IDs appropriately.

tests/unit/handlers/gh-pr-handler.test.ts (1)

140-150: LGTM!

The test correctly reflects the validation-first flow, consistent with the parallel change in gh-issue-handler.test.ts.


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

Copy link

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

🧹 Nitpick comments (4)
src/handlers/gh-issue-handler.ts (2)

85-88: Consider more granular error handling for the install prompt.

The catch block now calls sendInstallPrompt which itself can throw (e.g., if getOwnerIdFromUsername fails or handler.sendMessage fails). If sendInstallPrompt throws, the error is silently swallowed, leaving the user with no feedback.

Consider wrapping sendInstallPrompt in a try-catch with a fallback message:

        } catch {
-          await sendInstallPrompt(handler, channelId, repo);
+          try {
+            await sendInstallPrompt(handler, channelId, repo);
+          } catch {
+            await handler.sendMessage(
+              channelId,
+              "❌ Cannot access this repository. Please check your permissions or install the GitHub App."
+            );
+          }
          return;
        }

167-170: Same error handling consideration applies here.

For consistency and robustness, consider adding the same fallback error handling suggested for handleShowIssue (line 86).

src/github-app/installation-service.ts (1)

27-41: Consider validating repo format before parsing.

If repoFullName is malformed (e.g., missing "/"), parseRepo will return [undefined, undefined], and getOwnerIdFromUsername(undefined) will likely fail or produce unexpected behavior.

 export async function sendInstallPrompt(
   handler: BotHandler,
   channelId: string,
   repoFullName: string
 ): Promise<void> {
   const [owner] = parseRepo(repoFullName);
+  if (!owner) {
+    await handler.sendMessage(
+      channelId,
+      `❌ Invalid repository format: ${repoFullName}`
+    );
+    return;
+  }
   const ownerId = await getOwnerIdFromUsername(owner);
src/api/github-client.ts (1)

39-42: Consider adding input validation to parseRepo.

The function doesn't validate that the input contains a "/" or that both owner and repo are non-empty. This could cause downstream issues when the returned values are used in API calls.

 export function parseRepo(repoFullName: string): [owner: string, repo: string] {
   const [owner, repo] = repoFullName.split("/");
+  if (!owner || !repo) {
+    throw new Error(`Invalid repository format: "${repoFullName}". Expected "owner/repo".`);
+  }
   return [owner, repo];
 }

Alternatively, if you prefer to keep this as a utility that doesn't throw, consider returning a union type or documenting the behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa0f8c and de1f316.

📒 Files selected for processing (6)
  • src/api/github-client.ts (5 hunks)
  • src/api/user-oauth-client.ts (0 hunks)
  • src/github-app/installation-service.ts (2 hunks)
  • src/handlers/gh-issue-handler.ts (3 hunks)
  • src/handlers/gh-pr-handler.ts (3 hunks)
  • src/services/subscription-service.ts (6 hunks)
💤 Files with no reviewable changes (1)
  • src/api/user-oauth-client.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Store context externally - maintain stateless bot architecture with no message history, thread context, or conversation memory
Use <@{userId}> for mentions in messages AND add mentions in sendMessage options - do not use @username format
Implement event handlers for onMessage, onSlashCommand, onReaction, onTip, and onInteractionResponse to respond to Towns Protocol events
Define slash commands in src/commands.ts as a const array with name and description properties, then register handlers using bot.onSlashCommand()
Set ID in interaction requests and match ID in responses to correlate form submissions, button clicks, and transaction/signature responses
Use readContract for reading smart contract state, writeContract for SimpleAccount operations, and execute() for external contract interactions
Fund bot.appAddress (Smart Account) for on-chain operations, not bot.botId (Gas Wallet/EOA)
Use bot.* handler methods directly (outside event handlers) for unprompted messages via webhooks, timers, or tasks - requires channelId, spaceId, or other context stored externally
Always check permissions using handler.hasAdminPermission() before performing admin operations like ban, redact, or pin
User IDs are hex addresses in format 0x..., not usernames - use them consistently throughout event handling and message sending
Slash commands do not trigger onMessage - register slash command handlers using bot.onSlashCommand() instead
Use getSmartAccountFromUserId() to retrieve a user's wallet address from their userId
Include required environment variables: APP_PRIVATE_DATA (bot credentials) and JWT_SECRET (webhook security token)

Files:

  • src/handlers/gh-pr-handler.ts
  • src/api/github-client.ts
  • src/handlers/gh-issue-handler.ts
  • src/github-app/installation-service.ts
  • src/services/subscription-service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Provide alt text for image attachments and use appropriate MIME types for chunked attachments (videos, screenshots)

Files:

  • src/handlers/gh-pr-handler.ts
  • src/api/github-client.ts
  • src/handlers/gh-issue-handler.ts
  • src/github-app/installation-service.ts
  • src/services/subscription-service.ts
🧠 Learnings (1)
📚 Learning: 2025-11-25T03:24:12.451Z
Learnt from: CR
Repo: HereNotThere/bot-github PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:24:12.451Z
Learning: Applies to **/*.ts : Use bot.* handler methods directly (outside event handlers) for unprompted messages via webhooks, timers, or tasks - requires channelId, spaceId, or other context stored externally

Applied to files:

  • src/github-app/installation-service.ts
🧬 Code graph analysis (4)
src/handlers/gh-pr-handler.ts (1)
src/github-app/installation-service.ts (1)
  • sendInstallPrompt (27-41)
src/handlers/gh-issue-handler.ts (1)
src/github-app/installation-service.ts (1)
  • sendInstallPrompt (27-41)
src/github-app/installation-service.ts (1)
src/api/github-client.ts (2)
  • parseRepo (39-42)
  • getOwnerIdFromUsername (248-265)
src/services/subscription-service.ts (2)
src/api/github-client.ts (1)
  • getOwnerIdFromUsername (248-265)
src/github-app/installation-service.ts (1)
  • generateInstallUrl (18-22)
🔇 Additional comments (8)
src/handlers/gh-pr-handler.ts (1)

79-82: LGTM! Consistent pattern with issue handler.

The changes mirror the issue handler implementation, maintaining consistency across the codebase. The same optional error handling improvement suggested for gh-issue-handler.ts would apply here as well.

Also applies to: 156-159

src/github-app/installation-service.ts (1)

18-22: LGTM! Clean URL generation with sensible defaults.

The function handles the optional targetId gracefully, falling back to a generic install URL when the owner ID cannot be determined. The environment variable fallback is appropriate.

src/services/subscription-service.ts (2)

3-4: LGTM! Clean refactoring to centralized utilities.

The imports are well-organized, bringing in getOwnerIdFromUsername from the API layer and generateInstallUrl from the installation service. This eliminates code duplication and ensures consistent URL generation across the codebase.

Also applies to: 19-22


159-179: LGTM! Proper handling of owner ID lookup for installation URL.

The flow correctly fetches the owner ID from the public GitHub API and generates a targeted installation URL. When getOwnerIdFromUsername returns undefined, generateInstallUrl gracefully falls back to a generic URL without target_id.

src/api/github-client.ts (4)

44-86: LGTM! Consistent refactoring of API functions.

The validateRepo, getIssue, and getPullRequest functions now consistently use parseRepo to extract owner/repo from the full name. The logic remains unchanged otherwise.


88-149: LGTM! Well-implemented pagination with proper safeguards.

The pagination logic includes sensible safeguards:

  • maxPages limit prevents excessive API calls
  • Early termination when enough results are collected
  • Proper handling of "merged" state by filtering merged_at
  • Case-insensitive author filtering

151-200: LGTM! Correctly filters out pull requests from issues endpoint.

The implementation properly handles the GitHub API behavior where the issues endpoint returns both issues and PRs, filtering them at line 192.


248-265: LGTM! Robust owner ID lookup with graceful fallback.

The implementation correctly:

  • Tries organization first (common for private repos)
  • Falls back to user lookup
  • Returns undefined on failure, allowing callers to handle gracefully
  • Logs a warning for debugging

shuhuiluo and others added 2 commits November 25, 2025 04:59
When fetching a specific PR/issue fails after OAuth retry, check repo
access with validateRepo() before showing install prompt. If user has
access, the 404 means the item doesn't exist.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Prevents confusing downstream errors when repo string is missing "/".
The error propagates to handler's catch block showing a clear message.

Updates existing tests to expect the new validation error.

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

Co-Authored-By: Claude <noreply@anthropic.com>
@shuhuiluo shuhuiluo merged commit 643c41f into main Nov 25, 2025
2 checks passed
@shuhuiluo shuhuiluo deleted the fix/install-prompt-error-message branch November 25, 2025 10:27
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.

2 participants