From ff8eb45684f12e9af681ce8a20fb30a46921a680 Mon Sep 17 00:00:00 2001 From: Josh Ferrell Date: Tue, 7 Oct 2025 11:51:05 -0400 Subject: [PATCH] Add the ability to ignore push events from certain users --- .../cloudbees/jenkins/GitHubPushTrigger.java | 47 +++++++++++++ .../DefaultPushGHEventSubscriber.java | 19 +++-- .../jenkins/GitHubPushTrigger/config.groovy | 8 +++ .../GitHubPushTrigger/help-ignoredUsers.html | 11 +++ .../jenkins/GitHubPushTriggerTest.java | 37 ++++++++++ .../DefaultPushGHEventListenerTest.java | 69 +++++++++++++++++++ 6 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-ignoredUsers.html diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 4cae5f049..ad5a52af6 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -35,6 +35,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.Stapler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,10 +68,56 @@ */ public class GitHubPushTrigger extends Trigger> implements GitHubTrigger { + private String ignoredUsers; + @DataBoundConstructor public GitHubPushTrigger() { } + /** + * Gets the newline-separated list of usernames to ignore. + * + * @return the ignored users list, or null if not configured + * @since FIXME + */ + public String getIgnoredUsers() { + return ignoredUsers; + } + + /** + * Sets the newline-separated list of usernames whose push events should be ignored. + * The list will be trimmed of leading/trailing whitespace. + * + * @param ignoredUsers the newline-separated list of usernames to ignore + * @since FIXME + */ + @DataBoundSetter + public void setIgnoredUsers(String ignoredUsers) { + this.ignoredUsers = Util.fixEmptyAndTrim(ignoredUsers); + } + + /** + * Checks if the given username should be ignored. + * Username comparison is case-insensitive. + * + * @param username the username to check + * @return true if the user should be ignored, false otherwise + * @since FIXME + */ + public boolean isUserIgnored(String username) { + if (isEmpty(ignoredUsers) || isEmpty(username)) { + return false; + } + String[] ignoredUserArray = ignoredUsers.split("[\\r\\n]+"); + for (String ignoredUser : ignoredUserArray) { + String trimmedUser = ignoredUser.trim(); + if (!trimmedUser.isEmpty() && trimmedUser.equalsIgnoreCase(username)) { + return true; + } + } + return false; + } + /** * Called when a POST is made. */ diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java index 95180fddb..af06073aa 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java @@ -92,13 +92,18 @@ public void run() { LOGGER.debug("Considering to poke {}", fullDisplayName); if (GitHubRepositoryNameContributor.parseAssociatedNames(job) .contains(changedRepository)) { - LOGGER.info("Poked {}", fullDisplayName); - trigger.onPost(GitHubTriggerEvent.create() - .withTimestamp(event.getTimestamp()) - .withOrigin(event.getOrigin()) - .withTriggeredByUser(pusherName) - .build() - ); + if (trigger.isUserIgnored(pusherName)) { + LOGGER.debug("Skipped {} because pusher '{}' is in the ignored users list", + fullDisplayName, pusherName); + } else { + LOGGER.info("Poked {}", fullDisplayName); + trigger.onPost(GitHubTriggerEvent.create() + .withTimestamp(event.getTimestamp()) + .withOrigin(event.getOrigin()) + .withTriggeredByUser(pusherName) + .build() + ); + } } else { LOGGER.debug("Skipped {} because it doesn't have a matching repository.", fullDisplayName); diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy index 768800958..3bb684193 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy @@ -2,6 +2,14 @@ package com.cloudbees.jenkins.GitHubPushTrigger import com.cloudbees.jenkins.GitHubPushTrigger +def f = namespace(lib.FormTagLib) + +f.advanced() { + f.entry(title: _('Ignored Users'), field: 'ignoredUsers') { + f.textarea() + } +} + tr { td(colspan: 4) { def url = descriptor.getCheckMethod('hookRegistered').toCheckUrl() diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-ignoredUsers.html b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-ignoredUsers.html new file mode 100644 index 000000000..3688ab4ba --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-ignoredUsers.html @@ -0,0 +1,11 @@ +
+ A newline-separated list of (case-insensitive) GitHub usernames whose push events should be ignored. +
+ When a push event is received from a user in this list, the trigger will not poll for changes. +
+
+ Example: +
renovate-bot
+dependabot[bot]
+some-automation-user
+
diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java index cf301eb75..7f4cf847d 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java @@ -26,6 +26,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventListenerTest.TRIGGERED_BY_USER_FROM_RESOURCE; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author lanwen (Merkushev Kirill) @@ -97,4 +99,39 @@ void shouldReturnOkOnNoAnyProblem() throws Exception { FormValidation validation = descriptor.doCheckHookRegistered(job); assertThat("all ok", validation.kind, is(FormValidation.Kind.OK)); } + + @Test + public void shouldIgnoreSingleUser() { + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.setIgnoredUsers("ignored-user"); + + assertTrue("user should be ignored", trigger.isUserIgnored("ignored-user")); + assertTrue("user should be ignored", trigger.isUserIgnored("IGNORED-user")); + assertFalse("user should not be ignored", trigger.isUserIgnored("another-user")); + assertFalse("user should not be ignored", trigger.isUserIgnored("")); + assertFalse("user should not be ignored", trigger.isUserIgnored(null)); + } + + @Test + public void shouldIgnoreMultipleUsers() { + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.setIgnoredUsers(" user1 \nUsEr2\nuser3"); + + assertTrue("user should be ignored", trigger.isUserIgnored("user1")); + assertTrue("user should be ignored", trigger.isUserIgnored("user2")); + assertTrue("user should be ignored", trigger.isUserIgnored("USER3")); + assertFalse("user should not be ignored", trigger.isUserIgnored("user4")); + assertFalse("user should not be ignored", trigger.isUserIgnored("")); + assertFalse("user should not be ignored", trigger.isUserIgnored(null)); + } + + @Test + public void shouldHandleEmptyIgnoredUsers() { + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.setIgnoredUsers(""); + + assertFalse("user should not be ignored", trigger.isUserIgnored("user4")); + assertFalse("user should not be ignored", trigger.isUserIgnored("")); + assertFalse("user should not be ignored", trigger.isUserIgnored(null)); + } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java index 0a20c01a5..fc228154a 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java @@ -140,4 +140,73 @@ void shouldNotReceivePushHookOnWorkflowWithNoBuilds() throws Exception { verify(trigger, never()).onPost(Mockito.any(GitHubTriggerEvent.class)); } + + @Test + @WithoutJenkins + public void shouldNotTriggerWhenUserIsIgnored() { + GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); + when(trigger.isUserIgnored(eq(TRIGGERED_BY_USER_FROM_RESOURCE))).thenReturn(true); + + FreeStyleProject prj = mock(FreeStyleProject.class); + when(prj.getTriggers()).thenReturn( + Collections.singletonMap(new GitHubPushTrigger.DescriptorImpl(), trigger)); + when(prj.getSCMs()).thenAnswer(unused -> Collections.singletonList(GIT_SCM_FROM_RESOURCE)); + when(prj.getFullDisplayName()).thenReturn("test-job"); + + GHSubscriberEvent subscriberEvent = + new GHSubscriberEvent("shouldNotTriggerWhenUserIsIgnored", GHEvent.PUSH, classpath("payloads/push.json")); + + Jenkins jenkins = mock(Jenkins.class); + when(jenkins.getAllItems(Item.class)).thenReturn(Collections.singletonList(prj)); + + ExtensionList extensionList = mock(ExtensionList.class); + List gitHubRepositoryNameContributorList = + Collections.singletonList(new GitHubRepositoryNameContributor.FromSCM()); + when(extensionList.iterator()).thenReturn(gitHubRepositoryNameContributorList.iterator()); + when(jenkins.getExtensionList(GitHubRepositoryNameContributor.class)).thenReturn(extensionList); + + try (MockedStatic mockedJenkins = mockStatic(Jenkins.class)) { + mockedJenkins.when(Jenkins::getInstance).thenReturn(jenkins); + new DefaultPushGHEventSubscriber().onEvent(subscriberEvent); + } + + verify(trigger, never()).onPost(Mockito.any(GitHubTriggerEvent.class)); + } + + @Test + @WithoutJenkins + public void shouldTriggerWhenUserIsNotIgnored() { + GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); + when(trigger.isUserIgnored(eq(TRIGGERED_BY_USER_FROM_RESOURCE))).thenReturn(false); + + FreeStyleProject prj = mock(FreeStyleProject.class); + when(prj.getTriggers()).thenReturn( + Collections.singletonMap(new GitHubPushTrigger.DescriptorImpl(), trigger)); + when(prj.getSCMs()).thenAnswer(unused -> Collections.singletonList(GIT_SCM_FROM_RESOURCE)); + when(prj.getFullDisplayName()).thenReturn("test-job"); + + GHSubscriberEvent subscriberEvent = + new GHSubscriberEvent("shouldTriggerWhenUserIsNotIgnored", GHEvent.PUSH, classpath("payloads/push.json")); + + Jenkins jenkins = mock(Jenkins.class); + when(jenkins.getAllItems(Item.class)).thenReturn(Collections.singletonList(prj)); + + ExtensionList extensionList = mock(ExtensionList.class); + List gitHubRepositoryNameContributorList = + Collections.singletonList(new GitHubRepositoryNameContributor.FromSCM()); + when(extensionList.iterator()).thenReturn(gitHubRepositoryNameContributorList.iterator()); + when(jenkins.getExtensionList(GitHubRepositoryNameContributor.class)).thenReturn(extensionList); + + try (MockedStatic mockedJenkins = mockStatic(Jenkins.class)) { + mockedJenkins.when(Jenkins::getInstance).thenReturn(jenkins); + new DefaultPushGHEventSubscriber().onEvent(subscriberEvent); + } + + verify(trigger).onPost(eq(GitHubTriggerEvent.create() + .withTimestamp(subscriberEvent.getTimestamp()) + .withOrigin("shouldTriggerWhenUserIsNotIgnored") + .withTriggeredByUser(TRIGGERED_BY_USER_FROM_RESOURCE) + .build() + )); + } }