From 4d519841428578b5daa1d62ab5a9a000622da4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isidro=20Montero=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 09:12:46 +0100 Subject: [PATCH 1/3] Fix: Handle bot users gracefully when metadata is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repository scans were failing with FileNotFoundException when PRs were authored by bot users (e.g., GitHub Copilot) whose metadata is not accessible via the GitHub API. Problem: - Scan aborts with FATAL error when user metadata fetch returns 404 - This prevents orphanedItemStrategy from executing - Results in accumulation of orphaned PR branches Solution: - Implement tolerant error handling for user metadata fetching - Use default values (login, login@users.noreply.github.com) when metadata is unavailable - Log warnings instead of throwing exceptions - Allow scan to complete successfully This ensures bot-authored PRs don't block repository indexing and orphaned item cleanup can proceed as configured. Fixes scan failures with bot users like GitHub Copilot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../github_branch_source/GitHubSCMSource.java | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index ac1c480fc..7f32da7b6 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -2581,28 +2581,64 @@ public void observe(GHPullRequest pr) { // looked up this user already user = users.get(login); } + // Try to get user metadata, but be tolerant to API failures + String userName = login; + String userEmail = login + "@users.noreply.github.com"; + try { + userName = user.getName(); + if (userName == null || userName.isEmpty()) { + userName = login; + } + userEmail = user.getEmail(); + if (userEmail == null || userEmail.isEmpty()) { + userEmail = login + "@users.noreply.github.com"; + } + } catch (FileNotFoundException e) { + // User metadata not accessible (e.g., bot users like Copilot) + // Log warning but continue with default values + request.listener() + .getLogger() + .format( + "%n Could not find user metadata for %s (PR #%d). Using default values.%n", + login, number); + } catch (IOException e) { + // Other IO errors, use default values but log + request.listener() + .getLogger() + .format( + "%n IO error fetching user metadata for %s (PR #%d): %s. Using default values.%n", + login, number, e.getMessage()); + } ContributorMetadataAction contributor = - new ContributorMetadataAction(login, user.getName(), user.getEmail()); + new ContributorMetadataAction(login, userName, userEmail); // store the populated user record now that we have it pullRequestContributorCache.put(number, contributor); users.put(login, user); } + // Store PR metadata + pullRequestMetadataCache.put( + number, + new ObjectMetadataAction( + pr.getTitle(), pr.getBody(), pr.getHtmlUrl().toExternalForm())); + pullRequestMetadataKeys.add(number); + } catch (FileNotFoundException e) { + // PR user not found at all - log and skip this PR request.listener() .getLogger() .format( - "%n Could not find user %s for pull request %d.%n", - user == null ? "null" : user.getLogin(), number); - throw new WrappedException(e); + "%n Could not find user for pull request #%d. Skipping PR.%n", + number); + // Don't throw exception - just skip this PR and continue scanning } catch (IOException e) { - throw new WrappedException(e); + // Other IO errors when getting PR user - log and skip this PR + request.listener() + .getLogger() + .format( + "%n IO error for pull request #%d: %s. Skipping PR.%n", + number, e.getMessage()); + // Don't throw exception - just skip this PR and continue scanning } - - pullRequestMetadataCache.put( - number, - new ObjectMetadataAction( - pr.getTitle(), pr.getBody(), pr.getHtmlUrl().toExternalForm())); - pullRequestMetadataKeys.add(number); } @Override From 890ea047a65768a2be223ef9994c328179d6a24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isidro=20Montero=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 09:27:33 +0100 Subject: [PATCH 2/3] Fix code formatting with Spotless --- .../plugins/github_branch_source/GitHubSCMSource.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index 7f32da7b6..2944cf869 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -2626,17 +2626,13 @@ public void observe(GHPullRequest pr) { // PR user not found at all - log and skip this PR request.listener() .getLogger() - .format( - "%n Could not find user for pull request #%d. Skipping PR.%n", - number); + .format("%n Could not find user for pull request #%d. Skipping PR.%n", number); // Don't throw exception - just skip this PR and continue scanning } catch (IOException e) { // Other IO errors when getting PR user - log and skip this PR request.listener() .getLogger() - .format( - "%n IO error for pull request #%d: %s. Skipping PR.%n", - number, e.getMessage()); + .format("%n IO error for pull request #%d: %s. Skipping PR.%n", number, e.getMessage()); // Don't throw exception - just skip this PR and continue scanning } } From 7a4c869fa9331e605e3ea242013f32d343a36b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isidro=20Montero=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 10:48:34 +0100 Subject: [PATCH 3/3] Update tests to reflect graceful handling of user metadata errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix for handling bot users whose metadata is unavailable changed the behavior from throwing exceptions to gracefully processing PRs with default values. Updated tests to reflect this new, correct behavior: - fetchSmokes_badUser: PR-2 is now included in results with default values - testOpenSinglePRThrowsFileNotFoundOnObserve: Successfully processes PR instead of throwing - testOpenSinglePRThrowsIOOnObserve: Successfully processes PR instead of throwing This ensures PRs from bots (like GitHub Copilot) are processed instead of causing repository scan failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../GitHubSCMSourceTest.java | 6 ++--- .../GithubSCMSourcePRsTest.java | 24 +++++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceTest.java index 77666a076..81e2d97c2 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceTest.java @@ -411,10 +411,10 @@ public boolean isHead(@NonNull Probe probe, @NonNull TaskListener listener) thro byName.put(h.getKey().getName(), h.getKey()); revByName.put(h.getKey().getName(), h.getValue()); } - assertThat(byName.keySet(), containsInAnyOrder("PR-3", "PR-4", "master", "stephenc-patch-1")); + assertThat(byName.keySet(), containsInAnyOrder("PR-2", "PR-3", "PR-4", "master", "stephenc-patch-1")); - // PR-2 fails to find user and throws file not found for user. - // Caught and handled by removing PR-2 but scan continues. + // PR-2 fails to find user metadata but is handled gracefully. + // The PR is included with default user values instead of being skipped. assertThat(byName.get("PR-3"), instanceOf(PullRequestSCMHead.class)); assertThat(((SCMHeadOrigin.Fork) byName.get("PR-3").getOrigin()).getName(), is("stephenc")); diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourcePRsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourcePRsTest.java index d1c1031ad..e801c8ec3 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourcePRsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourcePRsTest.java @@ -153,15 +153,11 @@ public void testOpenSinglePRThrowsFileNotFoundOnObserve() throws Exception { .new LazyPullRequests(request, repoSpy) .iterator(); - // Expected: In the iterator will have one item in it but when getting that item you receive an - // FileNotFound exception + // Expected: In the iterator will have one item and it will be successfully retrieved + // with default user values since user metadata is not available assertTrue(pullRequestIterator.hasNext()); - try { - pullRequestIterator.next(); - fail(); - } catch (Exception e) { - assertEquals("java.io.FileNotFoundException: User not found", e.getMessage()); - } + GHPullRequest pr = pullRequestIterator.next(); + assertNotNull(pr); } @Test @@ -197,15 +193,11 @@ public void testOpenSinglePRThrowsIOOnObserve() throws Exception { .new LazyPullRequests(request, repoSpy) .iterator(); - // Expected: In the iterator will have one item in it but when getting that item you receive an - // IO exception + // Expected: In the iterator will have one item and it will be successfully retrieved + // with default user values since user metadata retrieval fails assertTrue(pullRequestIterator.hasNext()); - try { - pullRequestIterator.next(); - fail(); - } catch (Exception e) { - assertEquals("java.io.IOException: Failed to get user", e.getMessage()); - } + GHPullRequest pr = pullRequestIterator.next(); + assertNotNull(pr); } // Multiple PRs