Skip to content

fix(gh cli): migrate legacy gh cli auth token into keychain#1724

Merged
arnestrickmann merged 4 commits intomainfrom
fix/pr-sidebar-links-broken
Apr 14, 2026
Merged

fix(gh cli): migrate legacy gh cli auth token into keychain#1724
arnestrickmann merged 4 commits intomainfrom
fix/pr-sidebar-links-broken

Conversation

@janburzinski
Copy link
Copy Markdown
Collaborator

@janburzinski janburzinski commented Apr 14, 2026

Summary

after i added the gh cli removal stuff so that people dont get ratelimited we now had the issue that prs in the sidebar didnt work for users

this should fix that since we now migrate the gh auth token into the keychain if the keychain is empty and that should make it work again

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • This change requires a documentation update

Mandatory Tasks

  • I have self-reviewed the code

Checklist

  • I have read the contributing guide
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my PR needs changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Summary by CodeRabbit

  • New Features

    • Automatic migration of GitHub CLI tokens when no local token exists.
  • Improvements

    • More robust token storage and session coordination to reduce auth races and failures.
    • Logout now blocks automatic re-migration to avoid unintended token restoration.
  • Tests

    • Expanded tests covering migration, logout/migration races, and prevention of post-logout migration.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Adds one-shot keychain migration from the gh CLI into GitHubService with migration-block sentinel and auth-state locking; refactors IPC to use an imported githubService singleton; tests updated with an in-memory keychain mock and migration/race scenarios. (50 words)

Changes

Cohort / File(s) Summary
GitHub Token Migration Logic
src/main/services/GitHubService.ts
Adds migration guard state (migrationAttempted, migrationInFlight, MIGRATION_BLOCK_ACCOUNT), withAuthStateLock, migrateTokenFromGHCLI(), updates getStoredToken() to coordinate one-shot migration, extends storeToken(token, source) and updates logout() to write migration sentinel and coordinate persistence.
IPC Singleton Refactor
src/main/ipc/githubIpc.ts
Replaces local new GitHubService() instantiation with imported githubService singleton; IPC handlers now use the shared instance.
Tests: in-memory keychain & migration scenarios
src/test/main/GitHubService.test.ts
Replaces fixed keytar mocks with a Map-backed in-memory keychain, resets state in beforeEach, adds tests for gh auth token migration, logout-vs-migration race (sentinel written), prevention of auto-migration after logout, and updates logout assertions.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant GS as GitHubService
    participant Keytar as Keytar
    participant GHCLI as GH CLI

    Caller->>GS: getStoredToken()
    GS->>Keytar: getPassword('emdash-github','github-token')
    Keytar-->>GS: token or null

    alt token exists
        GS-->>Caller: return token
    else no token
        GS->>Keytar: getPassword('emdash-github','github-migration-blocked')
        Keytar-->>GS: blocked? ('1' or null)
        alt blocked
            GS-->>Caller: return null
        else not blocked
            GS->>GS: withAuthStateLock -> migrateTokenFromGHCLI()
            GS->>GHCLI: run `gh auth token`
            GHCLI-->>GS: stdout token or empty
            alt token received
                GS->>Keytar: setPassword('emdash-github','github-token', token) (source='migration')
                Keytar-->>GS: stored
                GS-->>Caller: return migrated token
            else no token
                GS-->>Caller: return null
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped from CLI to keychain bright,
A sentinel blinked to guard the night,
One singleton now watches the door,
I migrated once and then no more,
A tiny token snug and right 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: migrating legacy gh CLI auth tokens into the keychain to restore PR sidebar functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 fix/pr-sidebar-links-broken

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.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Apr 14, 2026 3:11pm

Request Review

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

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

Inline comments:
In `@src/main/services/GitHubService.ts`:
- Around line 108-110: The current legacy-token migration guarded by the boolean
migrationAttempted allows racing: multiple callers of getStoredToken() can see
null while one migration is in-flight and logout() can race with migration; fix
by introducing a shared in-flight Promise or mutex (e.g., migrationInFlight:
Promise<string|null> | null) to serialize the migration logic in
getStoredToken()/migrateLegacyToken(), have callers await that promise instead
of returning null, and ensure the actual write/persist of the migrated token is
performed only while holding the serialized section (the mutex/promise) and
after re-checking logout/blocked state so logout() cannot race to block
persistence—update references in getStoredToken(), migrateLegacyToken(), and
logout() to cooperate with the new migrationInFlight synchronization.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd27c5c9-f98b-459d-9ce5-01b7322472f0

📥 Commits

Reviewing files that changed from the base of the PR and between 67ab3a8 and 11568fd.

📒 Files selected for processing (3)
  • src/main/ipc/githubIpc.ts
  • src/main/services/GitHubService.ts
  • src/test/main/GitHubService.test.ts

Comment thread src/main/services/GitHubService.ts
@janburzinski
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

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

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

Inline comments:
In `@src/test/main/GitHubService.test.ts`:
- Around line 203-210: Before releasing the blocked token store, assert that the
logout process has not proceeded past the lock by checking the mock and
sentinel: after calling service.logout() but before calling releaseTokenStore(),
add assertions that deletePasswordMock (or whatever mock wraps key deletion) has
not been called and that keychain.get('emdash-github:github-migration-blocked')
is still null/undefined; then releaseTokenStore() and continue the existing
awaits and final assertions. This ensures the test verifies the serialization in
GitHubService.logout() rather than a late-running delete.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a2215b8-9e45-4ea0-b80d-7c376e81e291

📥 Commits

Reviewing files that changed from the base of the PR and between 11568fd and f947346.

📒 Files selected for processing (2)
  • src/main/services/GitHubService.ts
  • src/test/main/GitHubService.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/services/GitHubService.ts

Comment thread src/test/main/GitHubService.test.ts
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.

🧹 Nitpick comments (1)
src/test/main/GitHubService.test.ts (1)

10-12: De-duplicate the keytar mock behavior.

The blocking/store logic now lives in both the module-scope mock setup and the beforeEach reset path. Extracting one installer/helper would keep the race harness from drifting over time.

♻️ Suggested cleanup
 const keychain = new Map<string, string>();
 
 const keyFor = (serviceName: string, accountName: string) => `${serviceName}:${accountName}`;
 
-const setPasswordMock = vi.fn(
-  async (serviceName: string, accountName: string, password: string) => {
-    if (accountName === 'github-token') {
-      notifyTokenStoreStarted?.();
-      if (blockTokenStore) {
-        await new Promise<void>((resolve) => {
-          releaseTokenStore = resolve;
-        });
-      }
-    }
-
-    keychain.set(keyFor(serviceName, accountName), password);
-  }
-);
-const getPasswordMock = vi.fn(async (serviceName: string, accountName: string) => {
-  return keychain.get(keyFor(serviceName, accountName)) ?? null;
-});
-const deletePasswordMock = vi.fn(async (serviceName: string, accountName: string) => {
-  keychain.delete(keyFor(serviceName, accountName));
-});
+const setPasswordMock = vi.fn();
+const getPasswordMock = vi.fn();
+const deletePasswordMock = vi.fn();
+
+const installKeytarMockBehavior = () => {
+  setPasswordMock.mockImplementation(
+    async (serviceName: string, accountName: string, password: string) => {
+      if (accountName === 'github-token') {
+        notifyTokenStoreStarted?.();
+        if (blockTokenStore) {
+          await new Promise<void>((resolve) => {
+            releaseTokenStore = resolve;
+          });
+        }
+      }
+
+      keychain.set(keyFor(serviceName, accountName), password);
+    }
+  );
+  getPasswordMock.mockImplementation(async (serviceName: string, accountName: string) => {
+    return keychain.get(keyFor(serviceName, accountName)) ?? null;
+  });
+  deletePasswordMock.mockImplementation(async (serviceName: string, accountName: string) => {
+    keychain.delete(keyFor(serviceName, accountName));
+  });
+};
+
+installKeytarMockBehavior();
-    setPasswordMock.mockImplementation(
-      async (serviceName: string, accountName: string, password: string) => {
-        if (accountName === 'github-token') {
-          notifyTokenStoreStarted?.();
-          if (blockTokenStore) {
-            await new Promise<void>((resolve) => {
-              releaseTokenStore = resolve;
-            });
-          }
-        }
-
-        keychain.set(keyFor(serviceName, accountName), password);
-      }
-    );
-    getPasswordMock.mockImplementation(async (serviceName: string, accountName: string) => {
-      return keychain.get(keyFor(serviceName, accountName)) ?? null;
-    });
-    deletePasswordMock.mockImplementation(async (serviceName: string, accountName: string) => {
-      keychain.delete(keyFor(serviceName, accountName));
-    });
+    installKeytarMockBehavior();

Also applies to: 76-99, 125-151

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

In `@src/test/main/GitHubService.test.ts` around lines 10 - 12, The keytar mock
blocking/store logic is duplicated between the module-scope setup and the
beforeEach reset (symbols blockTokenStore, releaseTokenStore,
notifyTokenStoreStarted); extract that logic into a single helper (e.g.,
installKeytarMock or resetKeytarMock) and replace both the module-level
initialization and the beforeEach reset with calls to that helper so the
race-harness behavior is defined in one place and reused consistently across the
tests (update all places that reference
blockTokenStore/releaseTokenStore/notifyTokenStoreStarted to use the helper’s
API).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/test/main/GitHubService.test.ts`:
- Around line 10-12: The keytar mock blocking/store logic is duplicated between
the module-scope setup and the beforeEach reset (symbols blockTokenStore,
releaseTokenStore, notifyTokenStoreStarted); extract that logic into a single
helper (e.g., installKeytarMock or resetKeytarMock) and replace both the
module-level initialization and the beforeEach reset with calls to that helper
so the race-harness behavior is defined in one place and reused consistently
across the tests (update all places that reference
blockTokenStore/releaseTokenStore/notifyTokenStoreStarted to use the helper’s
API).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f5e1baa-89cb-4013-aeb3-267f1788c31a

📥 Commits

Reviewing files that changed from the base of the PR and between f947346 and 18ba446.

📒 Files selected for processing (1)
  • src/test/main/GitHubService.test.ts

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.

🧹 Nitpick comments (1)
src/main/services/GitHubService.ts (1)

1354-1363: Consider: Narrow remaining race window between migrationAttempted check and promise assignment.

There's a small race between lines 1348 and 1354-1355: two callers passing the migrationAttempted check can each create a promise. The lock in migrateTokenFromGHCLI ensures only one actually migrates, but the "loser" promise returns null. A third caller arriving in this window might await the loser promise and receive null despite successful migration.

In practice this is very minor—the token IS stored, so the next call succeeds—but if you want to eliminate it entirely:

♻️ Optional: Atomic check-and-assign
-    if (this.migrationAttempted) return null;
-  } catch (error) {
-    console.error('Failed to retrieve token:', error);
-    return null;
-  }
-
-  const inFlight = this.migrateTokenFromGHCLI();
-  this.migrationInFlight = inFlight;
+    if (this.migrationAttempted) return null;
+
+    // Atomically create and store the migration promise
+    const inFlight = this.migrateTokenFromGHCLI();
+    this.migrationInFlight = inFlight;
+  } catch (error) {
+    console.error('Failed to retrieve token:', error);
+    return null;
+  }

Moving the promise creation inside the try block doesn't fully fix the race—a proper fix would require a synchronous guard variable set immediately, then the async work. Given the minimal impact, the current implementation is acceptable.

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

In `@src/main/services/GitHubService.ts` around lines 1354 - 1363, There’s a tiny
race where two callers pass migrationAttempted and each creates a promise so a
third caller could await the “loser” promise and get null; fix by making the
check-and-assign atomic: ensure you synchronously set a guard/promise before
starting async work (e.g., assign this.migrationInFlight immediately to a
pending promise or set a boolean migrationStarted synchronously) and then run
migrateTokenFromGHCLI inside that promise so subsequent callers see the same
in-flight promise; keep the existing finally that clears this.migrationInFlight
only if it still equals the promise you set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/services/GitHubService.ts`:
- Around line 1354-1363: There’s a tiny race where two callers pass
migrationAttempted and each creates a promise so a third caller could await the
“loser” promise and get null; fix by making the check-and-assign atomic: ensure
you synchronously set a guard/promise before starting async work (e.g., assign
this.migrationInFlight immediately to a pending promise or set a boolean
migrationStarted synchronously) and then run migrateTokenFromGHCLI inside that
promise so subsequent callers see the same in-flight promise; keep the existing
finally that clears this.migrationInFlight only if it still equals the promise
you set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41f36331-f81a-44a3-a066-9ab4fd7d3971

📥 Commits

Reviewing files that changed from the base of the PR and between 18ba446 and 13bb3ea.

📒 Files selected for processing (1)
  • src/main/services/GitHubService.ts

@arnestrickmann
Copy link
Copy Markdown
Contributor

Thanks! @janburzinski

@arnestrickmann arnestrickmann merged commit 4f84815 into main Apr 14, 2026
5 checks passed
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