Skip to content

Respect preferred GitHub account across authentication scopes - #8940

Merged
Alex Ross (alexr00) merged 6 commits into
mainfrom
copilot/fix-multiple-accounts-issue
Sep 10, 2026
Merged

Respect preferred GitHub account across authentication scopes#8940
Alex Ross (alexr00) merged 6 commits into
mainfrom
copilot/fix-multiple-accounts-issue

Conversation

Copilot AI commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Account switching could retain credentials from another GitHub identity when accounts had different scope-specific sessions, exposing stale private PRs and causing unauthorized actions under the wrong account.

  • Session selection
    • Resolve VS Code’s preferred account across all scopes.
    • Select the richest available session constrained to that account.
    • Preserve legacy fallback when no preference is available.
const preferred = await getSession(provider, [], { silent: true });
await getSession(provider, scopes, {
  silent: true,
  account: preferred.account,
});
  • Regression coverage
    • Cover accounts with different default and additional scope sets.
    • Verify broader scopes never override the preferred account.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix issue with multiple GitHub accounts not working Respect preferred GitHub account across authentication scopes Sep 8, 2026

@alexr00 Alex Ross (alexr00) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copilot Can you explain why this happens? As far as I can see we're always asking for a specific auth provider id already. Are you saying that getSession can return a session with a different auth provider ID than we ask for?

Copilot AI commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Copilot Can you explain why this happens? As far as I can see we're always asking for a specific auth provider id already. Are you saying that getSession can return a session with a different auth ...

No—the provider ID remains github. That provider can have sessions for multiple accounts, and each account can have different exact scope sets. VS Code filters by provider and requested scopes before applying the extension’s account preference. If preferred Account B has no session for those scopes but Account A is the only matching allowed session, silent getSession('github', scopes, ...) returns A. We then incorrectly treated A as the preferred account. The empty-scope lookup includes all scope-specific sessions so VS Code can select B first; subsequent lookups pass account: B. The EMU error in the screenshot is consistent with using the wrong account/token within the same github provider, not a different provider ID.

@alexr00
Alex Ross (alexr00) marked this pull request as ready for review September 10, 2026 09:29
Copilot AI lite review requested due to automatic review settings September 10, 2026 09:29
@alexr00
Alex Ross (alexr00) enabled auto-merge (squash) September 10, 2026 09:29
@alexr00
Alex Ross (alexr00) marked this pull request as draft September 10, 2026 09:30
auto-merge was automatically disabled September 10, 2026 09:30

Pull request was converted to draft

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved account-switch races and stale cache paths can expose or retain data from the previous account.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates GitHub authentication to honor the preferred account and refresh account-specific state during account switching.

Changes:

  • Constrains session selection to the preferred account.
  • Clears repository, review, issue, notification, and UI state.
  • Adds regression coverage for authentication and cache cleanup.
File summaries
File Description
src/view/webviewViewCoordinator.ts Resets active PR webview state
src/view/reviewsManager.ts Clears and refreshes review state
src/view/reviewManager.ts Clears account-specific review state
src/view/prsTreeModel.ts Supports silent cache clearing
src/view/prsTreeDataProvider.ts Clears PR tree state
src/view/createPullRequestHelper.ts Resets PR creation state
src/test/view/reviewManager.test.ts Tests review-state cleanup
src/test/view/prsTree.test.ts Tests tree cleanup
src/test/notifications/notificationsManager.test.ts Tests notification cleanup
src/test/issues/stateManager.test.ts Tests issue cleanup
src/test/github/pullRequestOverview.test.ts Tests panel cleanup
src/test/github/folderRepositoryManager.test.ts Tests repository cache cleanup
src/test/github/credentials.test.ts Tests preferred-account selection
src/notifications/notificationsManager.ts Clears notification state
src/issues/stateManager.ts Clears and refreshes issue state
src/github/repositoriesManager.ts Propagates repository cleanup
src/github/issueOverview.ts Clears issue panels
src/github/folderRepositoryManager.ts Clears repository and user caches
src/github/credentials.ts Handles preferred-account sessions
src/extension.ts Coordinates account-change cleanup
Review details

Suppressed comments (6)

src/github/credentials.ts:66

  • These account-constrained probes only cover findExistingSession. The richer-scope upgrade path in getHubEnsureAdditionalScopes calls initialize(..., requireScopes=true), which bypasses findExistingSession and requests SCOPES_WITH_ADDITIONAL without an account; if the preferred account only has the default scopes while another account has the richer session, that upgrade can still silently switch identities. Thread the preferred account through the forced-scope path as well.
		const scopesInPreferenceOrder = [SCOPES_WITH_ADDITIONAL, SCOPES_OLD, SCOPES_OLDEST];
		for (const scopes of scopesInPreferenceOrder) {
			const session = await getSession(authProviderId, scopes, { silent: true, account: preferredSession.account });

src/github/folderRepositoryManager.ts:497

  • Clearing this promise breaks the serialization guard in updateRepositories. If a repository refresh is already in flight when the account changes, the auth handler immediately starts another refresh; the old doUpdateRepositories can then finish later and replace _githubRepositories or related caches with results from the previous auth generation. Keep the in-flight operation serialized, or discard results from an older auth generation instead of resetting this reference.
		this._updatingRepositories = undefined;

src/github/folderRepositoryManager.ts:700

  • Once a cache kind is added to _invalidatedUserCaches, this check always returns undefined for that manager because the set is never cleared. getOrgProjects has no in-memory cache and therefore performs a fresh API fetch on every lookup after an account change instead of reusing the newly fetched data. Clear the marker after a successful account-scoped refresh or retain the result in memory.
		if (this._invalidatedUserCaches.has(userKind)) {
			return undefined;

src/github/folderRepositoryManager.ts:490

  • totalFetchedPages is another per-query pagination cache (declared later in this class), but this auth reset only clears _repositoryPageInformation. After an account switch, fetchPagedData can reuse the previous account's page count and stop early or request the wrong pages for the new account. Clear totalFetchedPages here as part of the reset.
	clearForAuthChange(): void {
		this._sessionIgnoredRemoteNames.clear();
		this._inaccessibleRepos.clear();
		this._repositoryPageInformation.clear();
		this._gitBlameCache = {};

src/issues/stateManager.ts:218

  • The auth-change cleanup clears query and user caches but leaves state.currentIssue intact. Consequently currentIssues(), the status bar, and issue commands can continue exposing or operating on the old account's IssueModel after credentials switch. Reset and dispose the current issue as part of this account-change path, and notify the current-issue listeners.
		for (const state of this._singleRepoStates.values()) {
			if (state) {
				state.issueCollection.clear();
				state.userMap = undefined;
			}

src/view/prsTreeDataProvider.ts:372

  • On the account-change path this only clears the query cache. PrsTreeModel.forceClearCache(true) leaves _queriedPullRequests and copilotStateModel populated with old-account models/statuses, which are still read by tree item tooltips, badges, and Copilot state queries after the tree is rebuilt. Use an auth-specific model reset that clears those account-bound stores and their notifications as well.
	clear() {
		this.prsTreeModel.forceClearCache(true);
		this._children.forEach(child => child.dispose());
		this._children = [];
		this._onDidChangeTreeData.fire();
  • Files reviewed: 21/21 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/github/folderRepositoryManager.ts
Comment thread src/issues/stateManager.ts Outdated
Comment thread src/notifications/notificationsManager.ts
Comment thread src/github/folderRepositoryManager.ts Outdated
@alexr00
Alex Ross (alexr00) marked this pull request as ready for review September 10, 2026 15:00
Copilot AI review requested due to automatic review settings September 10, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved critical and moderate findings affect account-switch races and stale account-scoped state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (9)

Previously missed (2) — in code that hasn't changed since the last review.

src/issues/stateManager.ts:218

  • This clears issue query/user caches but leaves each state.currentIssue pointing at an IssueModel from the previous account. currentIssues() and the issue status-bar actions will therefore continue to expose and operate on that stale issue after an account switch; clear or re-resolve the current issues and publish the corresponding current-issue/status updates here.
    src/view/reviewManager.ts:1532
  • Resetting these fields does not invalidate _validateStatusInProgress. A validation started before the account event can continue after this method returns, restore _lastCommitSha or the active PR from the old identity, and the later updateState(true) is queued behind it. Add a generation guard/cancellation or await and discard the old validation before publishing state.

src/github/credentials.ts:164

  • Environment-token authentication stores _accountId as undefined (line 226). If that active token is removed, newSession is also undefined, so hasAccountChanged(undefined, undefined) is false and the extension skips its account-change clearing path, leaving the token's PR/issue/panel caches in place after sign-out. Treat removal of an active session as an account-state change or track the environment token's identity separately.
				accountChanged = hasAccountChanged(this._accountId, newSession.session);

src/github/folderRepositoryManager.ts:489

  • totalFetchedPages is also account-dependent pagination state, but this reset only clears _repositoryPageInformation. After switching accounts, fetchPagedData still compares the new result against the previous account's page count, so it can stop after an earlier repository/page and omit PRs visible only to the new account. Clear totalFetchedPages here as well.
		this._repositoryPageInformation.clear();

src/notifications/notificationsManager.ts:320

  • Replacing the map protects getNotifications, but markAsRead, markAsDone, and markPullRequests still await provider calls and then mutate this._notifications without checking the cache generation. A completion from the old cache can delete or fire against a replacement-account entry, and markPullRequests can act through the new provider. Capture the cache/account generation and ignore stale completions after clear().
		this._notifications = new Map();

src/view/prsTreeDataProvider.ts:372

  • The auth-change clear only drops _cachedPRs and _allCachedPRs; PrsTreeModel still retains _queriedPullRequests status entries and copilotStateModel's account-bound PR/notification models. After switching accounts, those entries can keep old PR decorations or Copilot badge data even though the tree children were reset. Add an auth-specific model reset, including the necessary decoration and badge invalidation events, instead of only disposing the tree nodes.
	clear() {
		this.prsTreeModel.forceClearCache(true);
		this._children.forEach(child => child.dispose());
		this._children = [];
		this._onDidChangeTreeData.fire();

src/view/prsTreeDataProvider.ts:372

  • Disposing _children does not invalidate an in-flight getChildren(undefined). After its awaits in getChildren, that call can still assign its old result to _children after this method has emptied it, reintroducing pre-switch nodes. Add a generation/token check before publishing the result.
	clear() {
		this.prsTreeModel.forceClearCache(true);
		this._children.forEach(child => child.dispose());
		this._children = [];
		this._onDidChangeTreeData.fire();

src/view/prsTreeDataProvider.ts:370

  • forceClearCache(true) only removes the current maps; an already running getPullRequestsForQuery/getAllPullRequests can still set them after this clear. Because the query lock serializes the next load behind that completion, the new-account refresh can then reuse the old-account result instead of fetching again. Add a generation check before caching and returning async results.
		this.prsTreeModel.forceClearCache(true);
		this._children.forEach(child => child.dispose());

src/view/prsTreeModel.ts:204

  • This cache reset does not clear copilotStateModel, including its cached PR models and notification set. After an account switch, Copilot counts and notification badges can therefore continue to reference the previous account until the watcher performs a later poll. Reset that account-scoped state as part of auth cleanup.
	public forceClearCache(silent: boolean = false) {
		this._cachedPRs.clear();
		this._allCachedPRs.clear();
		if (!silent) {
			this._onDidChangeData.fire();
		}
  • Files reviewed: 21/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/github/folderRepositoryManager.ts
Comment thread src/view/prsTreeModel.ts
@alexr00
Alex Ross (alexr00) merged commit fab197b into main Sep 10, 2026
7 checks passed
@alexr00
Alex Ross (alexr00) deleted the copilot/fix-multiple-accounts-issue branch September 10, 2026 15:21
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.

Multiple accounts doesn't work for any but the first account added

4 participants