Skip to content

Fix stale GitHub repo avatars after owner changes#6507

Open
ParkerRex wants to merge 1 commit into
stablyai:mainfrom
ParkerRex:fix/github-avatar-live-refresh
Open

Fix stale GitHub repo avatars after owner changes#6507
ParkerRex wants to merge 1 commit into
stablyai:mainfrom
ParkerRex:fix/github-avatar-live-refresh

Conversation

@ParkerRex

@ParkerRex ParkerRex commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Author's Note

Hey it's Parker - super random edge case I ran into. If you change repo location from Org -> Personal or vice-versa, the image goes stale.

This keeps the fix lazy instead of checking every repo on startup. Orca only refreshes the GitHub avatar identity when the repo icon settings are opened for a repo already using the GitHub avatar, or when the user explicitly hits Reset / GitHub Avatar.

Decision / Tradeoff

We looked at a couple of broader repair options and kept this intentionally narrow:

  • Startup-wide refresh: rejected because every launch would have to inspect repo remotes, and remote/SSH repos can turn that into network round trips. For forks, resolving the parent may also spend GitHub CLI/API budget. That is too much baseline cost for a rare repo-transfer edge case.
  • Background sweep: still touches unrelated repos and adds scheduling/cache invalidation complexity for an issue most users will never hit.
  • Lazy settings refresh: chosen because it has zero repo-list/startup cost, only runs for repos already using a GitHub avatar, and uses the existing repo-owner runtime routing plus existing gh.repoUpstream / gh.repoSlug calls.

The tradeoff is that the stale image self-heals when the user opens that repo's icon settings or explicitly uses Reset / GitHub Avatar, not immediately when .git/config changes behind Orca's back. That seemed like the right balance for this edge case.

Summary

  • Add a forced-live GitHub avatar resolver that ignores stale persisted upstream metadata when settings needs to repair a repo icon.
  • Refresh GitHub-avatar repo icons lazily from live upstream first, then live origin, so org-to-personal and personal-to-org repo moves can correct the stored avatar.
  • Preserve custom uploaded images, emoji, and lucide icons; passive refresh does not clear an existing avatar when GitHub identity cannot be resolved.
  • Add focused resolver and component coverage for the stale org-avatar-to-personal-avatar path.

Screenshots

No visual change.

Testing

  • pnpm exec oxlint src/renderer/src/components/settings/repository-icon-github.ts src/renderer/src/components/settings/RepositoryIconPicker.tsx src/renderer/src/components/settings/repository-icon-github.test.ts src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx
  • pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/settings/repository-icon-github.test.ts src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx
  • pnpm run typecheck:web
  • git diff --check
  • Added focused tests for stale GitHub avatar repair and passive refresh behavior.

Local note: this shell is on Node v25.9.0 while package.json declares Node 24, so pnpm printed engine warnings. The commands above completed successfully.

AI Review Report

Codex reviewed the patch while implementing it, with focus on avoiding startup-wide repo scanning, preserving custom icon choices, keeping SSH/runtime routing on the existing repo-owner target path, and preventing passive refresh from clearing a working avatar during transient GitHub/gh failures.

No separate external AI review command was run.

Security Audit

No new IPC channels, command execution, auth scopes, secrets, dependencies, or path handling were added. The change reuses existing gh.repoUpstream, gh.repoSlug, and repos:update paths, and only writes sanitized RepoIcon / upstream metadata already accepted by the repo settings persistence layer.

Notes

  • Performance impact is limited to opening repo icon settings for GitHub-avatar repos or explicit avatar/reset actions.
  • SSH and remote-runtime repos continue through the existing runtime target routing used by this settings pane.
  • GitLab and other provider behavior is unchanged.

@ParkerRex ParkerRex marked this pull request as ready for review June 27, 2026 17:22
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

repository-icon-github.ts gains two new exported types (ResolveRepositoryGitHubAvatarOptions, RepositoryGitHubAvatarResolution), a resolveRepositorySlugLive helper, a unified resolveRepositoryGitHubAvatar resolver that handles both live upstream lookup and slug fallback, and a buildRepositoryGitHubAvatarUpdate function that computes a Partial<Repo> diff. RepositoryIconPicker.tsx is updated to use these new APIs in handleUseGitHubAvatar, handleResetToDefault, and a rewritten useEffect that performs a one-time identity refresh per repo. Tests are added for both the resolver logic and the picker's refresh effect.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is well structured, but the AI Review Report does not explicitly confirm cross-platform review for macOS, Linux, and Windows. Add a cross-platform compatibility review section covering macOS, Linux, and Windows, including shortcuts, labels, paths, shell behavior, and Electron-specific differences.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: fixing stale GitHub repo avatars after owner changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/renderer/src/components/settings/repository-icon-github.ts (1)

104-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the clearMissingIcon rationale inline.

This guard carries the PR’s “passive refresh must not clear an existing GitHub avatar” rule, but that policy is only implicit here. A short Why: comment would make the opt-in clear behavior much easier to preserve.

As per coding guidelines, "When writing or modifying code driven by a design doc or non-obvious constraint, add a comment explaining why the code behaves the way it does."

Suggested comment
   if (
+    // Why: passive refresh preserves the current GitHub avatar when live
+    // identity cannot be resolved; explicit reset opts into clearing it.
     (resolution.repoIcon || options.clearMissingIcon) &&
     !sameRepoIcon(repo.repoIcon, resolution.repoIcon)
   ) {

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6db9e2ea-a0ac-4b8d-b47b-92103bcc409f

📥 Commits

Reviewing files that changed from the base of the PR and between 867c57b and c45a044.

📒 Files selected for processing (4)
  • src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx
  • src/renderer/src/components/settings/RepositoryIconPicker.tsx
  • src/renderer/src/components/settings/repository-icon-github.test.ts
  • src/renderer/src/components/settings/repository-icon-github.ts

Comment on lines 48 to +56
const upstream =
repo.upstream !== undefined
!options.forceLive && repo.upstream !== undefined
? repo.upstream
: await resolveRepositoryUpstreamLive(runtimeTarget, repo).catch(() => null)
if (upstream) {
return githubAvatarIcon(upstream)
}
const slug =
runtimeTarget.kind === 'environment'
? await callRuntimeRpc<{ owner: string; repo: string } | null>(
runtimeTarget,
'github.repoSlug',
{ repo: repo.id },
{ timeoutMs: 30_000 }
)
: await window.api.gh.repoSlug({ repoPath: repo.path, repoId: repo.id })
return slug ? githubAvatarIcon(slug) : null
return { repoIcon: githubAvatarIcon(upstream), upstream }
}
const slug = await resolveRepositorySlugLive(runtimeTarget, repo)
return { repoIcon: slug ? githubAvatarIcon(slug) : null, upstream: null }

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't collapse upstream lookup errors into null.

Line 51 swallows every resolveRepositoryUpstreamLive() failure and then Lines 55-56 fall back to repoSlug. If repoSlug succeeds, buildRepositoryGitHubAvatarUpdate() can persist upstream: null, which rewrites a still-forked repo as “not a fork” after a transient RPC/IPC failure. Let the error abort the refresh (or preserve the existing upstream) and only slug-fallback on an actual null upstream result.

Proposed fix
 export async function resolveRepositoryGitHubAvatar(
   runtimeTarget: RuntimeTarget,
   repo: Repo,
   options: ResolveRepositoryGitHubAvatarOptions = {}
 ): Promise<RepositoryGitHubAvatarResolution> {
   const upstream =
     !options.forceLive && repo.upstream !== undefined
       ? repo.upstream
-      : await resolveRepositoryUpstreamLive(runtimeTarget, repo).catch(() => null)
+      : await resolveRepositoryUpstreamLive(runtimeTarget, repo)
   if (upstream) {
     return { repoIcon: githubAvatarIcon(upstream), upstream }
   }
   const slug = await resolveRepositorySlugLive(runtimeTarget, repo)
   return { repoIcon: slug ? githubAvatarIcon(slug) : null, upstream: null }
 }
📝 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.

Suggested change
const upstream =
repo.upstream !== undefined
!options.forceLive && repo.upstream !== undefined
? repo.upstream
: await resolveRepositoryUpstreamLive(runtimeTarget, repo).catch(() => null)
if (upstream) {
return githubAvatarIcon(upstream)
}
const slug =
runtimeTarget.kind === 'environment'
? await callRuntimeRpc<{ owner: string; repo: string } | null>(
runtimeTarget,
'github.repoSlug',
{ repo: repo.id },
{ timeoutMs: 30_000 }
)
: await window.api.gh.repoSlug({ repoPath: repo.path, repoId: repo.id })
return slug ? githubAvatarIcon(slug) : null
return { repoIcon: githubAvatarIcon(upstream), upstream }
}
const slug = await resolveRepositorySlugLive(runtimeTarget, repo)
return { repoIcon: slug ? githubAvatarIcon(slug) : null, upstream: null }
export async function resolveRepositoryGitHubAvatar(
runtimeTarget: RuntimeTarget,
repo: Repo,
options: ResolveRepositoryGitHubAvatarOptions = {}
): Promise<RepositoryGitHubAvatarResolution> {
const upstream =
!options.forceLive && repo.upstream !== undefined
? repo.upstream
: await resolveRepositoryUpstreamLive(runtimeTarget, repo)
if (upstream) {
return { repoIcon: githubAvatarIcon(upstream), upstream }
}
const slug = await resolveRepositorySlugLive(runtimeTarget, repo)
return { repoIcon: slug ? githubAvatarIcon(slug) : null, upstream: null }
}

Comment on lines +30 to +31
// @ts-expect-error test window mock
globalThis.window = { api: { gh: apiMocks } }

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve happy-dom’s real window object.

Lines 30-31 replace globalThis.window with a plain object, which strips the real happy-dom window of document, timers, constructors, and other DOM state. That makes this test run against a broken browser global and can hide future regressions. Patch window.api instead of replacing window.

Proposed fix
-// `@ts-expect-error` test window mock
-globalThis.window = { api: { gh: apiMocks } }
+Object.defineProperty(window, 'api', {
+  value: { gh: apiMocks },
+  configurable: true
+})
📝 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.

Suggested change
// @ts-expect-error test window mock
globalThis.window = { api: { gh: apiMocks } }
Object.defineProperty(window, 'api', {
value: { gh: apiMocks },
configurable: true
})

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.

1 participant