From 0cd527c039385e2924ce9778facd1f3db647928a Mon Sep 17 00:00:00 2001 From: hu wang Date: Tue, 19 May 2026 14:47:06 -0400 Subject: [PATCH 1/5] add feature to allow merge request from the same branch to cancel running job that was triggered by the same merge request --- README.md | 11 ++- .../gitlabjenkins/GitLabPushTrigger.java | 11 +++ .../MergeRequestTriggerConfig.java | 9 +++ .../trigger/handler/PendingBuildsHandler.java | 61 ++++++++++++++++ ...MergeRequestHookTriggerHandlerFactory.java | 41 ++++++++++- .../MergeRequestHookTriggerHandlerImpl.java | 63 ++++++++++++++-- .../GitLabPushTrigger/config.jelly | 4 ++ .../help-cancelRunningBuildsOnUpdate.html | 6 ++ .../handler/PendingBuildsHandlerTest.java | 71 +++++++++++++++++++ 9 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/help-cancelRunningBuildsOnUpdate.html diff --git a/README.md b/README.md index 34503c1f5..907260ac7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - [Accept merge request after build](#accept-merge-request) - [Notify specific project by a specific GitLab connection](#notify-specific-project-by-a-specific-gitlab-connection) - [Cancel pending builds on merge request update](#cancel-pending-builds-on-merge-request-update) + - [Cancel running builds on merge request update](#cancel-running-builds-on-merge-request-update) - [Check if a label is applied to a merge request](#check-if-a-label-is-applied-to-a-merge-request) - [Compatibility](#compatibility) - [Contributing to the Plugin](#contributing-to-the-plugin) @@ -304,7 +305,8 @@ properties([ exclude: "" ], pendingBuildName: "jenkins", - cancelPendingBuildsOnUpdate: true + cancelPendingBuildsOnUpdate: true, + cancelRunningBuildsOnUpdate: true ] ]) ]) @@ -469,6 +471,7 @@ triggers { excludeBranchesSpec: "", pendingBuildName: "Jenkins", cancelPendingBuildsOnUpdate: false, + cancelRunningBuildsOnUpdate: false, secretToken: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEF", triggerToBranchDeleteRequest: false, triggerOnlyIfNewCommitsPushed: false, @@ -611,6 +614,12 @@ gitlabCommitStatus( To cancel pending builds of the same merge request when new commits are pushed, check 'Cancel pending merge request builds on update' from the Advanced-section in the trigger configuration. This saves time in projects where builds can stay long time in a build queue and you care only about the status of the newest commit. +### Cancel running builds on merge request update +To abort an in-flight build for the same merge request when new commits are pushed, check 'Cancel running merge request builds on update' from the Advanced-section in the trigger configuration. +Only builds whose `GitLabWebHookCause` matches the same source project ID **and** source branch are aborted — builds for other merge requests, branches, or jobs are left untouched. +This is independent of `cancelPendingBuildsOnUpdate`; enable both to abort the running build *and* drain the queue when an update arrives. +Aborted builds are interrupted with cause `Superseded by merge request update on source branch '...'`, which is visible in the build log. + ### Check if a label is applied to a merge request To handle conditional logic in your pipelines based on merge request labels, use: ```groovy diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java index 43aa26000..d7b53088c 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java @@ -108,6 +108,7 @@ public class GitLabPushTrigger extends Trigger> implements MergeReques private volatile Secret secretToken; private String pendingBuildName; private boolean cancelPendingBuildsOnUpdate; + private boolean cancelRunningBuildsOnUpdate; private transient BranchFilter branchFilter; private transient PushHookTriggerHandler pushHookTriggerHandler; @@ -347,6 +348,11 @@ public boolean getCancelPendingBuildsOnUpdate() { return this.cancelPendingBuildsOnUpdate; } + @Override + public boolean getCancelRunningBuildsOnUpdate() { + return this.cancelRunningBuildsOnUpdate; + } + @DataBoundSetter public void setTriggerOnPush(boolean triggerOnPush) { this.triggerOnPush = triggerOnPush; @@ -516,6 +522,11 @@ public void setCancelPendingBuildsOnUpdate(boolean cancelPendingBuildsOnUpdate) this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate; } + @DataBoundSetter + public void setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) { + this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate; + } + // executes when the Trigger receives a push request public void onPost(final PushHook hook) { if (branchFilter == null) { diff --git a/src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java b/src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java index 5ced3f46e..eca771699 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java @@ -20,4 +20,13 @@ public interface MergeRequestTriggerConfig { String getLabelsThatForcesBuildIfAdded(); boolean getCancelPendingBuildsOnUpdate(); + + /** + * Whether running builds for the same merge request source branch should be aborted when the + * merge request is updated. Independent of {@link #getCancelPendingBuildsOnUpdate()} — either + * can be enabled on its own. + */ + default boolean getCancelRunningBuildsOnUpdate() { + return false; + } } diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java index 39c522456..f6138ad00 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java @@ -10,8 +10,12 @@ import com.dabsquared.gitlabjenkins.util.LoggerUtil; import hudson.model.AbstractProject; import hudson.model.Cause; +import hudson.model.Executor; import hudson.model.Job; import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import jenkins.model.CauseOfInterruption; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,6 +49,63 @@ public void cancelPendingBuilds(Job job, Integer projectId, String branch) } } + /** + * Aborts in-flight builds of {@code job} whose {@link GitLabWebHookCause} matches the given + * source project id and source branch. Intended to be invoked when a merge request is updated + * and the user has opted in via {@code cancelRunningBuildsOnUpdate}. + */ + public void cancelRunningBuilds(Job job, Integer projectId, String sourceBranch) { + for (Run build : job.getBuilds()) { + if (!build.isBuilding()) { + continue; + } + GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class); + if (cause == null) { + continue; + } + CauseData causeData = cause.getData(); + if (!projectId.equals(causeData.getSourceProjectId())) { + continue; + } + if (!sourceBranch.equals(causeData.getSourceBranch())) { + continue; + } + stopBuild(build, job.getName(), sourceBranch); + setCommitStatusCancelledIfNecessary(causeData, job); + } + } + + private void stopBuild(Run build, String jobName, String sourceBranch) { + Executor executor = build.getExecutor(); + if (executor == null) { + return; + } + try { + LOGGER.log( + Level.INFO, + "Stopping build {0} of job {1} superseded by merge request update on branch {2}", + LoggerUtil.toArray(build.getDisplayName(), jobName, sourceBranch)); + executor.interrupt(Result.ABORTED, new SupersededByMergeRequestUpdate(sourceBranch)); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error stopping running build", e); + } + } + + /** Surfaced in the build log so users understand why the build was killed. */ + public static final class SupersededByMergeRequestUpdate extends CauseOfInterruption { + private static final long serialVersionUID = 1L; + private final String sourceBranch; + + public SupersededByMergeRequestUpdate(String sourceBranch) { + this.sourceBranch = sourceBranch; + } + + @Override + public String getShortDescription() { + return "Superseded by merge request update on source branch '" + sourceBranch + "'"; + } + } + private GitLabWebHookCause getGitLabWebHookCauseData(Queue.Item item) { for (Cause cause : item.getCauses()) { if (cause instanceof GitLabWebHookCause hookCause) { diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java index e7196bf62..69af69d20 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java @@ -29,6 +29,30 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler( String labelsThatForcesBuildIfAdded, boolean triggerOnApprovedMergeRequest, boolean cancelPendingBuildsOnUpdate) { + return newMergeRequestHookTriggerHandler( + triggerOnMergeRequest, + triggerOnlyWithNewCommitsPushed, + triggerOnAcceptedMergeRequest, + triggerOnClosedMergeRequest, + triggerOpenMergeRequest, + skipWorkInProgressMergeRequest, + labelsThatForcesBuildIfAdded, + triggerOnApprovedMergeRequest, + cancelPendingBuildsOnUpdate, + false); + } + + public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler( + boolean triggerOnMergeRequest, + boolean triggerOnlyWithNewCommitsPushed, + boolean triggerOnAcceptedMergeRequest, + boolean triggerOnClosedMergeRequest, + TriggerOpenMergeRequest triggerOpenMergeRequest, + boolean skipWorkInProgressMergeRequest, + String labelsThatForcesBuildIfAdded, + boolean triggerOnApprovedMergeRequest, + boolean cancelPendingBuildsOnUpdate, + boolean cancelRunningBuildsOnUpdate) { TriggerConfigChain chain = new TriggerConfigChain(); chain.rejectUnless( @@ -54,7 +78,8 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler( triggerOnlyWithNewCommitsPushed, skipWorkInProgressMergeRequest, labelsThatForcesBuildIfAddedSet, - cancelPendingBuildsOnUpdate); + cancelPendingBuildsOnUpdate, + cancelRunningBuildsOnUpdate); } public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(MergeRequestTriggerConfig config) { @@ -67,7 +92,8 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(M config.isSkipWorkInProgressMergeRequest(), config.getLabelsThatForcesBuildIfAdded(), config.isTriggerOnApprovedMergeRequest(), - config.getCancelPendingBuildsOnUpdate()); + config.getCancelPendingBuildsOnUpdate(), + config.getCancelRunningBuildsOnUpdate()); } public static Config withConfig() { @@ -84,6 +110,7 @@ public static class Config implements MergeRequestTriggerConfig { private String labelsThatForcesBuildIfAdded; private boolean triggerOnApprovedMergeRequest = false; private boolean cancelPendingBuildsOnUpdate = false; + private boolean cancelRunningBuildsOnUpdate = false; @Override public boolean getTriggerOnMergeRequest() { @@ -130,6 +157,11 @@ public boolean getCancelPendingBuildsOnUpdate() { return cancelPendingBuildsOnUpdate; } + @Override + public boolean getCancelRunningBuildsOnUpdate() { + return cancelRunningBuildsOnUpdate; + } + public Config setTriggerOnMergeRequest(boolean triggerOnMergeRequest) { this.triggerOnMergeRequest = triggerOnMergeRequest; return this; @@ -175,6 +207,11 @@ public Config setCancelPendingBuildsOnUpdate(boolean cancelPendingBuildsOnUpdate return this; } + public Config setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) { + this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate; + return this; + } + public MergeRequestHookTriggerHandler build() { return newMergeRequestHookTriggerHandler(this); } diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java index f808c1e39..1f959f080 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java @@ -47,17 +47,27 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler skipBuiltYetCheckActions = EnumSet.of(Action.open, Action.approved, Action.merge); private final EnumSet skipAllowedStateForActions = EnumSet.of(Action.approved); private final boolean cancelPendingBuildsOnUpdate; + private final boolean cancelRunningBuildsOnUpdate; MergeRequestHookTriggerHandlerImpl( Collection allowedStates, boolean skipWorkInProgressMergeRequest, boolean cancelPendingBuildsOnUpdate) { + this(allowedStates, skipWorkInProgressMergeRequest, cancelPendingBuildsOnUpdate, false); + } + + MergeRequestHookTriggerHandlerImpl( + Collection allowedStates, + boolean skipWorkInProgressMergeRequest, + boolean cancelPendingBuildsOnUpdate, + boolean cancelRunningBuildsOnUpdate) { this( allowedStates, EnumSet.noneOf(Action.class), false, skipWorkInProgressMergeRequest, - cancelPendingBuildsOnUpdate); + cancelPendingBuildsOnUpdate, + cancelRunningBuildsOnUpdate); } // this retains internal API, however, the plugin code no longer instantiates the handler this way. @@ -69,12 +79,30 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler allowedStates, + Collection allowedActions, + boolean onlyIfNewCommitsPushed, + boolean skipWorkInProgressMergeRequest, + boolean cancelPendingBuildsOnUpdate, + boolean cancelRunningBuildsOnUpdate) { this( new TriggerConfigChain().add(allowedStates, null).add(null, allowedActions), onlyIfNewCommitsPushed, skipWorkInProgressMergeRequest, emptySet(), - cancelPendingBuildsOnUpdate); + cancelPendingBuildsOnUpdate, + cancelRunningBuildsOnUpdate); } MergeRequestHookTriggerHandlerImpl( @@ -83,11 +111,28 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler labelsThatForcesBuildIfAdded, boolean cancelPendingBuildsOnUpdate) { + this( + triggerConfig, + onlyIfNewCommitsPushed, + skipWorkInProgressMergeRequest, + labelsThatForcesBuildIfAdded, + cancelPendingBuildsOnUpdate, + false); + } + + MergeRequestHookTriggerHandlerImpl( + Predicate triggerConfig, + boolean onlyIfNewCommitsPushed, + boolean skipWorkInProgressMergeRequest, + Set labelsThatForcesBuildIfAdded, + boolean cancelPendingBuildsOnUpdate, + boolean cancelRunningBuildsOnUpdate) { this.triggerConfig = triggerConfig; this.onlyIfNewCommitsPushed = onlyIfNewCommitsPushed; this.skipWorkInProgressMergeRequest = skipWorkInProgressMergeRequest; this.labelsThatForcesBuildIfAdded = labelsThatForcesBuildIfAdded; this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate; + this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate; } @Override @@ -158,16 +203,20 @@ protected boolean isCiSkip(MergeRequestHook hook) { @Override protected void cancelPendingBuildsIfNecessary(Job job, MergeRequestHook hook) { - if (!this.cancelPendingBuildsOnUpdate) { + if (!this.cancelPendingBuildsOnUpdate && !this.cancelRunningBuildsOnUpdate) { return; } if (!hook.getObjectAttributes().getAction().equals(Action.update)) { return; } - this.pendingBuildsHandler.cancelPendingBuilds( - job, - hook.getObjectAttributes().getSourceProjectId(), - hook.getObjectAttributes().getSourceBranch()); + Integer sourceProjectId = hook.getObjectAttributes().getSourceProjectId(); + String sourceBranch = hook.getObjectAttributes().getSourceBranch(); + if (this.cancelPendingBuildsOnUpdate) { + this.pendingBuildsHandler.cancelPendingBuilds(job, sourceProjectId, sourceBranch); + } + if (this.cancelRunningBuildsOnUpdate) { + this.pendingBuildsHandler.cancelRunningBuilds(job, sourceProjectId, sourceBranch); + } } @Override diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly index b1a2f3907..c61313742 100644 --- a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly +++ b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly @@ -59,6 +59,10 @@ + + +
diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/help-cancelRunningBuildsOnUpdate.html b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/help-cancelRunningBuildsOnUpdate.html new file mode 100644 index 000000000..0ee4b83a1 --- /dev/null +++ b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/help-cancelRunningBuildsOnUpdate.html @@ -0,0 +1,6 @@ +
+ If enabled, then running builds for the same merge request source branch will be aborted + when the merge request is updated in the GitLab repository. Builds for other merge requests + or branches are left untouched. This is independent of cancelPendingBuildsOnUpdate + — enable both to cancel the entire queue plus the running build. +
diff --git a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java index 49f8afc50..5d06eee06 100644 --- a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java +++ b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java @@ -5,14 +5,19 @@ import static com.dabsquared.gitlabjenkins.gitlab.hook.model.builder.generated.UserBuilder.user; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import com.dabsquared.gitlabjenkins.GitLabPushTrigger; +import com.dabsquared.gitlabjenkins.cause.CauseData; +import com.dabsquared.gitlabjenkins.cause.GitLabWebHookCause; import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty; import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClient; import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState; @@ -30,10 +35,15 @@ import com.dabsquared.gitlabjenkins.gitlab.hook.model.builder.generated.UserBuilder; import com.dabsquared.gitlabjenkins.publisher.GitLabCommitStatusPublisher; import com.dabsquared.gitlabjenkins.trigger.filter.BranchFilterType; +import hudson.model.Executor; import hudson.model.FreeStyleProject; import hudson.model.ItemGroup; +import hudson.model.Job; import hudson.model.Project; import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.util.RunList; import java.io.File; import java.util.Arrays; import org.jenkinsci.plugins.workflow.job.WorkflowJob; @@ -158,6 +168,67 @@ void queuedMergeRequestBuildsCanBeCancelledOnMergeRequestUpdate() throws Excepti assertThat(jenkins.getInstance().getQueue().getItems().length, is(3)); } + @Test + void cancelRunningBuildsAbortsOnlyBuildsMatchingSourceProjectIdAndSourceBranch() { + Job job = mock(Job.class); + when(job.getName()).thenReturn("job1"); + when(job.getFullName()).thenReturn("job1"); + + // Matching: same source project, same source branch — should be aborted. + Executor matchExec1 = mock(Executor.class); + Run match1 = runningBuildWithCause(matchExec1, 1, "feature-branch", "commit-a"); + + Executor matchExec2 = mock(Executor.class); + Run match2 = runningBuildWithCause(matchExec2, 1, "feature-branch", "commit-b"); + + // Non-matching: same project, different branch. + Executor otherBranchExec = mock(Executor.class); + Run otherBranch = runningBuildWithCause(otherBranchExec, 1, "another-branch", "commit-c"); + + // Non-matching: different project, same branch. + Executor otherProjectExec = mock(Executor.class); + Run otherProject = runningBuildWithCause(otherProjectExec, 2, "feature-branch", "commit-d"); + + // Non-matching: already finished. + Executor finishedExec = mock(Executor.class); + Run finished = mock(Run.class); + when(finished.isBuilding()).thenReturn(false); + + // Non-matching: building but has no GitLab cause (e.g. triggered manually). + Run manualBuild = mock(Run.class); + when(manualBuild.isBuilding()).thenReturn(true); + when(manualBuild.getCause(GitLabWebHookCause.class)).thenReturn(null); + + RunList> runs = + RunList.fromRuns(Arrays.asList(match1, match2, otherBranch, otherProject, finished, manualBuild)); + doReturn(runs).when(job).getBuilds(); + + new PendingBuildsHandler().cancelRunningBuilds(job, 1, "feature-branch"); + + verify(matchExec1).interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + verify(matchExec2).interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + verify(otherBranchExec, never()).interrupt(any(), any()); + verify(otherProjectExec, never()).interrupt(any(), any()); + verify(finishedExec, never()).interrupt(any(), any()); + } + + private static Run runningBuildWithCause( + Executor executor, int sourceProjectId, String sourceBranch, String commit) { + Run run = mock(Run.class); + when(run.isBuilding()).thenReturn(true); + when(run.getExecutor()).thenReturn(executor); + + CauseData causeData = mock(CauseData.class); + when(causeData.getSourceProjectId()).thenReturn(sourceProjectId); + when(causeData.getSourceBranch()).thenReturn(sourceBranch); + when(causeData.getLastCommit()).thenReturn(commit); + + GitLabWebHookCause cause = mock(GitLabWebHookCause.class); + when(cause.getData()).thenReturn(causeData); + when(run.getCause(GitLabWebHookCause.class)).thenReturn(cause); + return run; + } + private GitLabPushTrigger gitLabPushTrigger(Project project) throws Exception { GitLabPushTrigger gitLabPushTrigger = gitLabPushTrigger(); project.addTrigger(gitLabPushTrigger); From a5d8f0c30cb262cbc0c38a0871597f2af47ccd16 Mon Sep 17 00:00:00 2001 From: Manjunath M Date: Thu, 28 May 2026 21:54:55 +0530 Subject: [PATCH 2/5] fix(test): Simplify PendingBuildsHandlerTest --- .../trigger/handler/PendingBuildsHandler.java | 2 +- .../handler/PendingBuildsHandlerTest.java | 31 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java index f6138ad00..14e3ce26a 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java @@ -15,10 +15,10 @@ import hudson.model.Queue; import hudson.model.Result; import hudson.model.Run; -import jenkins.model.CauseOfInterruption; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.CauseOfInterruption; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; diff --git a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java index 5d06eee06..5680fd8f3 100644 --- a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java +++ b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java @@ -9,6 +9,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.contains; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -78,7 +79,7 @@ static void setUp(JenkinsRule rule) { @BeforeEach void setUp() { - when(gitLabConnectionProperty.getClient()).thenReturn(gitLabClient); + lenient().when(gitLabConnectionProperty.getClient()).thenReturn(gitLabClient); } @AfterEach @@ -172,25 +173,23 @@ void queuedMergeRequestBuildsCanBeCancelledOnMergeRequestUpdate() throws Excepti void cancelRunningBuildsAbortsOnlyBuildsMatchingSourceProjectIdAndSourceBranch() { Job job = mock(Job.class); when(job.getName()).thenReturn("job1"); - when(job.getFullName()).thenReturn("job1"); // Matching: same source project, same source branch — should be aborted. Executor matchExec1 = mock(Executor.class); - Run match1 = runningBuildWithCause(matchExec1, 1, "feature-branch", "commit-a"); + Run match1 = runningBuildWithCause(matchExec1, 1, "feature-branch"); Executor matchExec2 = mock(Executor.class); - Run match2 = runningBuildWithCause(matchExec2, 1, "feature-branch", "commit-b"); + Run match2 = runningBuildWithCause(matchExec2, 1, "feature-branch"); // Non-matching: same project, different branch. Executor otherBranchExec = mock(Executor.class); - Run otherBranch = runningBuildWithCause(otherBranchExec, 1, "another-branch", "commit-c"); + Run otherBranch = runningBuildWithCause(null, 1, "another-branch"); // Non-matching: different project, same branch. Executor otherProjectExec = mock(Executor.class); - Run otherProject = runningBuildWithCause(otherProjectExec, 2, "feature-branch", "commit-d"); + Run otherProject = runningBuildWithCause(null, 2, "feature-branch"); // Non-matching: already finished. - Executor finishedExec = mock(Executor.class); Run finished = mock(Run.class); when(finished.isBuilding()).thenReturn(false); @@ -205,23 +204,25 @@ void cancelRunningBuildsAbortsOnlyBuildsMatchingSourceProjectIdAndSourceBranch() new PendingBuildsHandler().cancelRunningBuilds(job, 1, "feature-branch"); - verify(matchExec1).interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); - verify(matchExec2).interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + verify(matchExec1) + .interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + verify(matchExec2) + .interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); verify(otherBranchExec, never()).interrupt(any(), any()); verify(otherProjectExec, never()).interrupt(any(), any()); - verify(finishedExec, never()).interrupt(any(), any()); } - private static Run runningBuildWithCause( - Executor executor, int sourceProjectId, String sourceBranch, String commit) { + private static Run runningBuildWithCause(Executor executor, int sourceProjectId, String sourceBranch) { Run run = mock(Run.class); when(run.isBuilding()).thenReturn(true); - when(run.getExecutor()).thenReturn(executor); + if (executor != null) { + when(run.getExecutor()).thenReturn(executor); + } CauseData causeData = mock(CauseData.class); when(causeData.getSourceProjectId()).thenReturn(sourceProjectId); - when(causeData.getSourceBranch()).thenReturn(sourceBranch); - when(causeData.getLastCommit()).thenReturn(commit); + // Branch is only read after project id matches (see PendingBuildsHandler.cancelRunningBuilds). + lenient().when(causeData.getSourceBranch()).thenReturn(sourceBranch); GitLabWebHookCause cause = mock(GitLabWebHookCause.class); when(cause.getData()).thenReturn(causeData); From 5c802147e4df0e8e74e73e251b4ee18898eba87c Mon Sep 17 00:00:00 2001 From: Manjunath M Date: Fri, 29 May 2026 20:27:10 +0530 Subject: [PATCH 3/5] update help link for 'Cancel running merge request builds on update' in config.jelly --- .../com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly index c61313742..21285c294 100644 --- a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly +++ b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly @@ -60,7 +60,6 @@ From 30a582091db6f6473cef183eba49ff99da42b8b3 Mon Sep 17 00:00:00 2001 From: Manjunath M Date: Mon, 1 Jun 2026 11:23:57 +0530 Subject: [PATCH 4/5] fix: resolve help file loading for cancel running builds option --- .../com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly index 21285c294..6e95ca129 100644 --- a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly +++ b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly @@ -59,7 +59,7 @@ - From 06d6bec33bd1b7723c0595fc971234f30ebfb2fe Mon Sep 17 00:00:00 2001 From: Manjunath M Date: Tue, 2 Jun 2026 01:28:06 +0530 Subject: [PATCH 5/5] refactor: rename PendingBuildsHandler to MergeRequestBuildHandler The handler cancels both queued and running MR builds, so the old name was misleading. Also remove the redundant nine-parameter factory overload that always defaulted cancelRunningBuildsOnUpdate to false. --- .../AbstractWebHookTriggerHandler.java | 4 ++-- ...ler.java => MergeRequestBuildHandler.java} | 4 ++-- ...MergeRequestHookTriggerHandlerFactory.java | 23 ------------------- .../MergeRequestHookTriggerHandlerImpl.java | 4 ++-- ...penMergeRequestPushHookTriggerHandler.java | 4 ++-- ...java => MergeRequestBuildHandlerTest.java} | 10 ++++---- 6 files changed, 13 insertions(+), 36 deletions(-) rename src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/{PendingBuildsHandler.java => MergeRequestBuildHandler.java} (97%) rename src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/{PendingBuildsHandlerTest.java => MergeRequestBuildHandlerTest.java} (96%) diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/AbstractWebHookTriggerHandler.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/AbstractWebHookTriggerHandler.java index 489fcf313..83afee96a 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/AbstractWebHookTriggerHandler.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/AbstractWebHookTriggerHandler.java @@ -36,7 +36,7 @@ public abstract class AbstractWebHookTriggerHandler implements WebHookTriggerHandler { private static final Logger LOGGER = Logger.getLogger(AbstractWebHookTriggerHandler.class.getName()); - protected PendingBuildsHandler pendingBuildsHandler = new PendingBuildsHandler(); + protected MergeRequestBuildHandler mergeRequestBuildHandler = new MergeRequestBuildHandler(); @Override public void handle( @@ -70,7 +70,7 @@ public void handle( private void setCommitStatusPendingIfNecessary(Job job, H hook) { try { - String buildName = PendingBuildsHandler.resolvePendingBuildName(job); + String buildName = MergeRequestBuildHandler.resolvePendingBuildName(job); if (StringUtils.isNotBlank(buildName)) { GitLabConnectionProperty connectionProperty = job.getProperty(GitLabConnectionProperty.class); if (connectionProperty != null) { diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java similarity index 97% rename from src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java rename to src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java index 14e3ce26a..b78b3cd1a 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java @@ -24,9 +24,9 @@ import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; import org.jenkinsci.plugins.workflow.job.WorkflowJob; -public class PendingBuildsHandler { +public class MergeRequestBuildHandler { - private static final Logger LOGGER = Logger.getLogger(PendingBuildsHandler.class.getName()); + private static final Logger LOGGER = Logger.getLogger(MergeRequestBuildHandler.class.getName()); public void cancelPendingBuilds(Job job, Integer projectId, String branch) { Queue queue = Objects.requireNonNull(Jenkins.getInstance()).getQueue(); diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java index 69af69d20..db723af65 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java @@ -19,29 +19,6 @@ public final class MergeRequestHookTriggerHandlerFactory { private MergeRequestHookTriggerHandlerFactory() {} - public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler( - boolean triggerOnMergeRequest, - boolean triggerOnlyWithNewCommitsPushed, - boolean triggerOnAcceptedMergeRequest, - boolean triggerOnClosedMergeRequest, - TriggerOpenMergeRequest triggerOpenMergeRequest, - boolean skipWorkInProgressMergeRequest, - String labelsThatForcesBuildIfAdded, - boolean triggerOnApprovedMergeRequest, - boolean cancelPendingBuildsOnUpdate) { - return newMergeRequestHookTriggerHandler( - triggerOnMergeRequest, - triggerOnlyWithNewCommitsPushed, - triggerOnAcceptedMergeRequest, - triggerOnClosedMergeRequest, - triggerOpenMergeRequest, - skipWorkInProgressMergeRequest, - labelsThatForcesBuildIfAdded, - triggerOnApprovedMergeRequest, - cancelPendingBuildsOnUpdate, - false); - } - public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler( boolean triggerOnMergeRequest, boolean triggerOnlyWithNewCommitsPushed, diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java index 1f959f080..bcaee1bfb 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java @@ -212,10 +212,10 @@ protected void cancelPendingBuildsIfNecessary(Job job, MergeRequestHook ho Integer sourceProjectId = hook.getObjectAttributes().getSourceProjectId(); String sourceBranch = hook.getObjectAttributes().getSourceBranch(); if (this.cancelPendingBuildsOnUpdate) { - this.pendingBuildsHandler.cancelPendingBuilds(job, sourceProjectId, sourceBranch); + this.mergeRequestBuildHandler.cancelPendingBuilds(job, sourceProjectId, sourceBranch); } if (this.cancelRunningBuildsOnUpdate) { - this.pendingBuildsHandler.cancelRunningBuilds(job, sourceProjectId, sourceBranch); + this.mergeRequestBuildHandler.cancelRunningBuilds(job, sourceProjectId, sourceBranch); } } diff --git a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java index bfe06018a..36df1b94c 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java @@ -16,7 +16,7 @@ import com.dabsquared.gitlabjenkins.gitlab.hook.model.State; import com.dabsquared.gitlabjenkins.trigger.filter.BranchFilter; import com.dabsquared.gitlabjenkins.trigger.filter.MergeRequestLabelFilter; -import com.dabsquared.gitlabjenkins.trigger.handler.PendingBuildsHandler; +import com.dabsquared.gitlabjenkins.trigger.handler.MergeRequestBuildHandler; import com.dabsquared.gitlabjenkins.util.LoggerUtil; import hudson.model.Action; import hudson.model.CauseAction; @@ -181,7 +181,7 @@ private CauseData retrieveCauseData(PushHook hook, Project project, MergeRequest } private void setCommitStatusPendingIfNecessary(Job job, Integer projectId, String commit, String ref) { - String buildName = PendingBuildsHandler.resolvePendingBuildName(job); + String buildName = MergeRequestBuildHandler.resolvePendingBuildName(job); if (StringUtils.isNotBlank(buildName)) { GitLabClient client = job.getProperty(GitLabConnectionProperty.class).getClient(); diff --git a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandlerTest.java similarity index 96% rename from src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java rename to src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandlerTest.java index 5680fd8f3..e7244f65d 100644 --- a/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandlerTest.java +++ b/src/test/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandlerTest.java @@ -60,7 +60,7 @@ @WithJenkins @ExtendWith(MockitoExtension.class) -class PendingBuildsHandlerTest { +class MergeRequestBuildHandlerTest { private static final String GITLAB_BUILD_NAME = "Jenkins"; @@ -202,12 +202,12 @@ void cancelRunningBuildsAbortsOnlyBuildsMatchingSourceProjectIdAndSourceBranch() RunList.fromRuns(Arrays.asList(match1, match2, otherBranch, otherProject, finished, manualBuild)); doReturn(runs).when(job).getBuilds(); - new PendingBuildsHandler().cancelRunningBuilds(job, 1, "feature-branch"); + new MergeRequestBuildHandler().cancelRunningBuilds(job, 1, "feature-branch"); verify(matchExec1) - .interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + .interrupt(eq(Result.ABORTED), any(MergeRequestBuildHandler.SupersededByMergeRequestUpdate.class)); verify(matchExec2) - .interrupt(eq(Result.ABORTED), any(PendingBuildsHandler.SupersededByMergeRequestUpdate.class)); + .interrupt(eq(Result.ABORTED), any(MergeRequestBuildHandler.SupersededByMergeRequestUpdate.class)); verify(otherBranchExec, never()).interrupt(any(), any()); verify(otherProjectExec, never()).interrupt(any(), any()); } @@ -221,7 +221,7 @@ void cancelRunningBuildsAbortsOnlyBuildsMatchingSourceProjectIdAndSourceBranch() CauseData causeData = mock(CauseData.class); when(causeData.getSourceProjectId()).thenReturn(sourceProjectId); - // Branch is only read after project id matches (see PendingBuildsHandler.cancelRunningBuilds). + // Branch is only read after project id matches (see MergeRequestBuildHandler.cancelRunningBuilds). lenient().when(causeData.getSourceBranch()).thenReturn(sourceBranch); GitLabWebHookCause cause = mock(GitLabWebHookCause.class);