Fix GitHub bot avatar URL (#55297)

## Summary

- Fix GitHub avatar URL generation for bot noreply commit authors
- Fall back to the GitHub commit author API when the CDN email avatar
endpoint cannot resolve bot noreply emails
- Add tests covering bot noreply and regular user noreply author emails

- Before 
<img width="305" height="117" alt="image"
src="https://github.com/user-attachments/assets/01f79e6c-cae6-4c28-a3e3-3ca506898f4f"
/>
- After
<img width="297" height="105" alt="image"
src="https://github.com/user-attachments/assets/3f387b62-e34a-41f5-b377-2afe2e895bf8"
/>


## Test Plan

- `cargo test -p git_hosting_providers --features gpui/test-support`


Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed GitHub avatar lookup for bot noreply commit authors.

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
tsutoringoandChristopher Biscardi authored and GitHub committed 2026-04-30 14:17:32 +00:00
1 parent cfb96a48c0
commit f30074cd1b
1 file changed
+40 -3
@@ -68,8 +68,12 @@ pub struct Github {
base_url: Url,
}
fn normalize_author_email(email: &str) -> &str {
email.trim_start_matches('<').trim_end_matches('>')
}
fn build_cdn_avatar_url(email: &str) -> Result<Url> {
let email = email.trim_start_matches('<').trim_end_matches('>');
let email = normalize_author_email(email);
Url::parse(&format!(
"https://avatars.githubusercontent.com/u/e?email={}&s=128",
encode(email)
@@ -77,6 +81,15 @@ fn build_cdn_avatar_url(email: &str) -> Result<Url> {
.context("failed to construct avatar URL")
}
fn build_cdn_avatar_url_for_author_email(email: &str) -> Result<Option<Url>> {
let email = normalize_author_email(email);
if email.ends_with("[bot]@users.noreply.github.com") {
return Ok(None);
}
build_cdn_avatar_url(email).map(Some)
}
impl Github {
pub fn new(name: impl Into<String>, base_url: Url) -> Self {
Self {
@@ -267,8 +280,10 @@ impl GitHostingProvider for Github {
author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
if let Some(email) = author_email {
return Ok(Some(build_cdn_avatar_url(&email)?));
if let Some(email) = author_email
&& let Some(avatar_url) = build_cdn_avatar_url_for_author_email(&email)?
{
return Ok(Some(avatar_url));
}
let commit = commit.to_string();
@@ -629,4 +644,26 @@ mod tests {
"https://avatars.githubusercontent.com/u/e?email=user%2Btag%40example.com&s=128"
);
}
#[test]
fn test_build_cdn_avatar_url_for_author_email_skips_bot_noreply_emails() {
for email in [
"41898282+github-actions[bot]@users.noreply.github.com",
"<41898282+github-actions[bot]@users.noreply.github.com>",
] {
assert_eq!(build_cdn_avatar_url_for_author_email(email).unwrap(), None);
}
}
#[test]
fn test_build_cdn_avatar_url_for_author_email_uses_user_noreply_emails() {
let url = build_cdn_avatar_url_for_author_email("12345+octocat@users.noreply.github.com")
.unwrap()
.unwrap();
assert_eq!(
url.as_str(),
"https://avatars.githubusercontent.com/u/e?email=12345%2Boctocat%40users.noreply.github.com&s=128"
);
}
}