Skip to content

Commit 7cd63d1

Browse files
manjukionhuwang-dematicManjunath Mkrisstern
authored
Allow merge request updates to cancel running builds from the same source branch (#1901)
* add feature to allow merge request from the same branch to cancel running job that was triggered by the same merge request * fix(test): Simplify PendingBuildsHandlerTest * update help link for 'Cancel running merge request builds on update' in config.jelly * fix: resolve help file loading for cancel running builds option * 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. --------- Co-authored-by: hu wang <hu.wang@dematic.com> Co-authored-by: Manjunath M <A0096308@kiongroup.com> Co-authored-by: Kris Stern <88480540+krisstern@users.noreply.github.com>
1 parent 8a65d33 commit 7cd63d1

11 files changed

Lines changed: 253 additions & 19 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- [Accept merge request after build](#accept-merge-request)
3333
- [Notify specific project by a specific GitLab connection](#notify-specific-project-by-a-specific-gitlab-connection)
3434
- [Cancel pending builds on merge request update](#cancel-pending-builds-on-merge-request-update)
35+
- [Cancel running builds on merge request update](#cancel-running-builds-on-merge-request-update)
3536
- [Check if a label is applied to a merge request](#check-if-a-label-is-applied-to-a-merge-request)
3637
- [Compatibility](#compatibility)
3738
- [Contributing to the Plugin](#contributing-to-the-plugin)
@@ -304,7 +305,8 @@ properties([
304305
exclude: ""
305306
],
306307
pendingBuildName: "jenkins",
307-
cancelPendingBuildsOnUpdate: true
308+
cancelPendingBuildsOnUpdate: true,
309+
cancelRunningBuildsOnUpdate: true
308310
]
309311
])
310312
])
@@ -469,6 +471,7 @@ triggers {
469471
excludeBranchesSpec: "",
470472
pendingBuildName: "Jenkins",
471473
cancelPendingBuildsOnUpdate: false,
474+
cancelRunningBuildsOnUpdate: false,
472475
secretToken: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEF",
473476
triggerToBranchDeleteRequest: false,
474477
triggerOnlyIfNewCommitsPushed: false,
@@ -611,6 +614,12 @@ gitlabCommitStatus(
611614
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.
612615
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.
613616

617+
### Cancel running builds on merge request update
618+
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.
619+
Only builds whose `GitLabWebHookCause` matches the same source project ID **and** source branch are aborted &mdash; builds for other merge requests, branches, or jobs are left untouched.
620+
This is independent of `cancelPendingBuildsOnUpdate`; enable both to abort the running build *and* drain the queue when an update arrives.
621+
Aborted builds are interrupted with cause `Superseded by merge request update on source branch '...'`, which is visible in the build log.
622+
614623
### Check if a label is applied to a merge request
615624
To handle conditional logic in your pipelines based on merge request labels, use:
616625
```groovy

src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public class GitLabPushTrigger extends Trigger<Job<?, ?>> implements MergeReques
108108
private volatile Secret secretToken;
109109
private String pendingBuildName;
110110
private boolean cancelPendingBuildsOnUpdate;
111+
private boolean cancelRunningBuildsOnUpdate;
111112

112113
private transient BranchFilter branchFilter;
113114
private transient PushHookTriggerHandler pushHookTriggerHandler;
@@ -347,6 +348,11 @@ public boolean getCancelPendingBuildsOnUpdate() {
347348
return this.cancelPendingBuildsOnUpdate;
348349
}
349350

351+
@Override
352+
public boolean getCancelRunningBuildsOnUpdate() {
353+
return this.cancelRunningBuildsOnUpdate;
354+
}
355+
350356
@DataBoundSetter
351357
public void setTriggerOnPush(boolean triggerOnPush) {
352358
this.triggerOnPush = triggerOnPush;
@@ -516,6 +522,11 @@ public void setCancelPendingBuildsOnUpdate(boolean cancelPendingBuildsOnUpdate)
516522
this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate;
517523
}
518524

525+
@DataBoundSetter
526+
public void setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) {
527+
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
528+
}
529+
519530
// executes when the Trigger receives a push request
520531
public void onPost(final PushHook hook) {
521532
if (branchFilter == null) {

src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,13 @@ public interface MergeRequestTriggerConfig {
2020
String getLabelsThatForcesBuildIfAdded();
2121

2222
boolean getCancelPendingBuildsOnUpdate();
23+
24+
/**
25+
* Whether running builds for the same merge request source branch should be aborted when the
26+
* merge request is updated. Independent of {@link #getCancelPendingBuildsOnUpdate()} — either
27+
* can be enabled on its own.
28+
*/
29+
default boolean getCancelRunningBuildsOnUpdate() {
30+
return false;
31+
}
2332
}

src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/AbstractWebHookTriggerHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
public abstract class AbstractWebHookTriggerHandler<H extends WebHook> implements WebHookTriggerHandler<H> {
3737

3838
private static final Logger LOGGER = Logger.getLogger(AbstractWebHookTriggerHandler.class.getName());
39-
protected PendingBuildsHandler pendingBuildsHandler = new PendingBuildsHandler();
39+
protected MergeRequestBuildHandler mergeRequestBuildHandler = new MergeRequestBuildHandler();
4040

4141
@Override
4242
public void handle(
@@ -70,7 +70,7 @@ public void handle(
7070

7171
private void setCommitStatusPendingIfNecessary(Job<?, ?> job, H hook) {
7272
try {
73-
String buildName = PendingBuildsHandler.resolvePendingBuildName(job);
73+
String buildName = MergeRequestBuildHandler.resolvePendingBuildName(job);
7474
if (StringUtils.isNotBlank(buildName)) {
7575
GitLabConnectionProperty connectionProperty = job.getProperty(GitLabConnectionProperty.class);
7676
if (connectionProperty != null) {

src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/PendingBuildsHandler.java renamed to src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@
1010
import com.dabsquared.gitlabjenkins.util.LoggerUtil;
1111
import hudson.model.AbstractProject;
1212
import hudson.model.Cause;
13+
import hudson.model.Executor;
1314
import hudson.model.Job;
1415
import hudson.model.Queue;
16+
import hudson.model.Result;
17+
import hudson.model.Run;
1518
import java.util.Objects;
1619
import java.util.logging.Level;
1720
import java.util.logging.Logger;
21+
import jenkins.model.CauseOfInterruption;
1822
import jenkins.model.Jenkins;
1923
import org.apache.commons.lang3.StringUtils;
2024
import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
2125
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
2226

23-
public class PendingBuildsHandler {
27+
public class MergeRequestBuildHandler {
2428

25-
private static final Logger LOGGER = Logger.getLogger(PendingBuildsHandler.class.getName());
29+
private static final Logger LOGGER = Logger.getLogger(MergeRequestBuildHandler.class.getName());
2630

2731
public void cancelPendingBuilds(Job<?, ?> job, Integer projectId, String branch) {
2832
Queue queue = Objects.requireNonNull(Jenkins.getInstance()).getQueue();
@@ -45,6 +49,63 @@ public void cancelPendingBuilds(Job<?, ?> job, Integer projectId, String branch)
4549
}
4650
}
4751

52+
/**
53+
* Aborts in-flight builds of {@code job} whose {@link GitLabWebHookCause} matches the given
54+
* source project id and source branch. Intended to be invoked when a merge request is updated
55+
* and the user has opted in via {@code cancelRunningBuildsOnUpdate}.
56+
*/
57+
public void cancelRunningBuilds(Job<?, ?> job, Integer projectId, String sourceBranch) {
58+
for (Run<?, ?> build : job.getBuilds()) {
59+
if (!build.isBuilding()) {
60+
continue;
61+
}
62+
GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class);
63+
if (cause == null) {
64+
continue;
65+
}
66+
CauseData causeData = cause.getData();
67+
if (!projectId.equals(causeData.getSourceProjectId())) {
68+
continue;
69+
}
70+
if (!sourceBranch.equals(causeData.getSourceBranch())) {
71+
continue;
72+
}
73+
stopBuild(build, job.getName(), sourceBranch);
74+
setCommitStatusCancelledIfNecessary(causeData, job);
75+
}
76+
}
77+
78+
private void stopBuild(Run<?, ?> build, String jobName, String sourceBranch) {
79+
Executor executor = build.getExecutor();
80+
if (executor == null) {
81+
return;
82+
}
83+
try {
84+
LOGGER.log(
85+
Level.INFO,
86+
"Stopping build {0} of job {1} superseded by merge request update on branch {2}",
87+
LoggerUtil.toArray(build.getDisplayName(), jobName, sourceBranch));
88+
executor.interrupt(Result.ABORTED, new SupersededByMergeRequestUpdate(sourceBranch));
89+
} catch (Exception e) {
90+
LOGGER.log(Level.SEVERE, "Error stopping running build", e);
91+
}
92+
}
93+
94+
/** Surfaced in the build log so users understand why the build was killed. */
95+
public static final class SupersededByMergeRequestUpdate extends CauseOfInterruption {
96+
private static final long serialVersionUID = 1L;
97+
private final String sourceBranch;
98+
99+
public SupersededByMergeRequestUpdate(String sourceBranch) {
100+
this.sourceBranch = sourceBranch;
101+
}
102+
103+
@Override
104+
public String getShortDescription() {
105+
return "Superseded by merge request update on source branch '" + sourceBranch + "'";
106+
}
107+
}
108+
48109
private GitLabWebHookCause getGitLabWebHookCauseData(Queue.Item item) {
49110
for (Cause cause : item.getCauses()) {
50111
if (cause instanceof GitLabWebHookCause hookCause) {

src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(
2828
boolean skipWorkInProgressMergeRequest,
2929
String labelsThatForcesBuildIfAdded,
3030
boolean triggerOnApprovedMergeRequest,
31-
boolean cancelPendingBuildsOnUpdate) {
31+
boolean cancelPendingBuildsOnUpdate,
32+
boolean cancelRunningBuildsOnUpdate) {
3233

3334
TriggerConfigChain chain = new TriggerConfigChain();
3435
chain.rejectUnless(
@@ -54,7 +55,8 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(
5455
triggerOnlyWithNewCommitsPushed,
5556
skipWorkInProgressMergeRequest,
5657
labelsThatForcesBuildIfAddedSet,
57-
cancelPendingBuildsOnUpdate);
58+
cancelPendingBuildsOnUpdate,
59+
cancelRunningBuildsOnUpdate);
5860
}
5961

6062
public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(MergeRequestTriggerConfig config) {
@@ -67,7 +69,8 @@ public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(M
6769
config.isSkipWorkInProgressMergeRequest(),
6870
config.getLabelsThatForcesBuildIfAdded(),
6971
config.isTriggerOnApprovedMergeRequest(),
70-
config.getCancelPendingBuildsOnUpdate());
72+
config.getCancelPendingBuildsOnUpdate(),
73+
config.getCancelRunningBuildsOnUpdate());
7174
}
7275

7376
public static Config withConfig() {
@@ -84,6 +87,7 @@ public static class Config implements MergeRequestTriggerConfig {
8487
private String labelsThatForcesBuildIfAdded;
8588
private boolean triggerOnApprovedMergeRequest = false;
8689
private boolean cancelPendingBuildsOnUpdate = false;
90+
private boolean cancelRunningBuildsOnUpdate = false;
8791

8892
@Override
8993
public boolean getTriggerOnMergeRequest() {
@@ -130,6 +134,11 @@ public boolean getCancelPendingBuildsOnUpdate() {
130134
return cancelPendingBuildsOnUpdate;
131135
}
132136

137+
@Override
138+
public boolean getCancelRunningBuildsOnUpdate() {
139+
return cancelRunningBuildsOnUpdate;
140+
}
141+
133142
public Config setTriggerOnMergeRequest(boolean triggerOnMergeRequest) {
134143
this.triggerOnMergeRequest = triggerOnMergeRequest;
135144
return this;
@@ -175,6 +184,11 @@ public Config setCancelPendingBuildsOnUpdate(boolean cancelPendingBuildsOnUpdate
175184
return this;
176185
}
177186

187+
public Config setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) {
188+
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
189+
return this;
190+
}
191+
178192
public MergeRequestHookTriggerHandler build() {
179193
return newMergeRequestHookTriggerHandler(this);
180194
}

src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,27 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler<M
4747
private final EnumSet<Action> skipBuiltYetCheckActions = EnumSet.of(Action.open, Action.approved, Action.merge);
4848
private final EnumSet<Action> skipAllowedStateForActions = EnumSet.of(Action.approved);
4949
private final boolean cancelPendingBuildsOnUpdate;
50+
private final boolean cancelRunningBuildsOnUpdate;
5051

5152
MergeRequestHookTriggerHandlerImpl(
5253
Collection<State> allowedStates,
5354
boolean skipWorkInProgressMergeRequest,
5455
boolean cancelPendingBuildsOnUpdate) {
56+
this(allowedStates, skipWorkInProgressMergeRequest, cancelPendingBuildsOnUpdate, false);
57+
}
58+
59+
MergeRequestHookTriggerHandlerImpl(
60+
Collection<State> allowedStates,
61+
boolean skipWorkInProgressMergeRequest,
62+
boolean cancelPendingBuildsOnUpdate,
63+
boolean cancelRunningBuildsOnUpdate) {
5564
this(
5665
allowedStates,
5766
EnumSet.noneOf(Action.class),
5867
false,
5968
skipWorkInProgressMergeRequest,
60-
cancelPendingBuildsOnUpdate);
69+
cancelPendingBuildsOnUpdate,
70+
cancelRunningBuildsOnUpdate);
6171
}
6272

6373
// this retains internal API, however, the plugin code no longer instantiates the handler this way.
@@ -69,12 +79,30 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler<M
6979
boolean onlyIfNewCommitsPushed,
7080
boolean skipWorkInProgressMergeRequest,
7181
boolean cancelPendingBuildsOnUpdate) {
82+
this(
83+
allowedStates,
84+
allowedActions,
85+
onlyIfNewCommitsPushed,
86+
skipWorkInProgressMergeRequest,
87+
cancelPendingBuildsOnUpdate,
88+
false);
89+
}
90+
91+
@Deprecated
92+
MergeRequestHookTriggerHandlerImpl(
93+
Collection<State> allowedStates,
94+
Collection<Action> allowedActions,
95+
boolean onlyIfNewCommitsPushed,
96+
boolean skipWorkInProgressMergeRequest,
97+
boolean cancelPendingBuildsOnUpdate,
98+
boolean cancelRunningBuildsOnUpdate) {
7299
this(
73100
new TriggerConfigChain().add(allowedStates, null).add(null, allowedActions),
74101
onlyIfNewCommitsPushed,
75102
skipWorkInProgressMergeRequest,
76103
emptySet(),
77-
cancelPendingBuildsOnUpdate);
104+
cancelPendingBuildsOnUpdate,
105+
cancelRunningBuildsOnUpdate);
78106
}
79107

80108
MergeRequestHookTriggerHandlerImpl(
@@ -83,11 +111,28 @@ class MergeRequestHookTriggerHandlerImpl extends AbstractWebHookTriggerHandler<M
83111
boolean skipWorkInProgressMergeRequest,
84112
Set<String> labelsThatForcesBuildIfAdded,
85113
boolean cancelPendingBuildsOnUpdate) {
114+
this(
115+
triggerConfig,
116+
onlyIfNewCommitsPushed,
117+
skipWorkInProgressMergeRequest,
118+
labelsThatForcesBuildIfAdded,
119+
cancelPendingBuildsOnUpdate,
120+
false);
121+
}
122+
123+
MergeRequestHookTriggerHandlerImpl(
124+
Predicate<MergeRequestObjectAttributes> triggerConfig,
125+
boolean onlyIfNewCommitsPushed,
126+
boolean skipWorkInProgressMergeRequest,
127+
Set<String> labelsThatForcesBuildIfAdded,
128+
boolean cancelPendingBuildsOnUpdate,
129+
boolean cancelRunningBuildsOnUpdate) {
86130
this.triggerConfig = triggerConfig;
87131
this.onlyIfNewCommitsPushed = onlyIfNewCommitsPushed;
88132
this.skipWorkInProgressMergeRequest = skipWorkInProgressMergeRequest;
89133
this.labelsThatForcesBuildIfAdded = labelsThatForcesBuildIfAdded;
90134
this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate;
135+
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
91136
}
92137

93138
@Override
@@ -158,16 +203,20 @@ protected boolean isCiSkip(MergeRequestHook hook) {
158203

159204
@Override
160205
protected void cancelPendingBuildsIfNecessary(Job<?, ?> job, MergeRequestHook hook) {
161-
if (!this.cancelPendingBuildsOnUpdate) {
206+
if (!this.cancelPendingBuildsOnUpdate && !this.cancelRunningBuildsOnUpdate) {
162207
return;
163208
}
164209
if (!hook.getObjectAttributes().getAction().equals(Action.update)) {
165210
return;
166211
}
167-
this.pendingBuildsHandler.cancelPendingBuilds(
168-
job,
169-
hook.getObjectAttributes().getSourceProjectId(),
170-
hook.getObjectAttributes().getSourceBranch());
212+
Integer sourceProjectId = hook.getObjectAttributes().getSourceProjectId();
213+
String sourceBranch = hook.getObjectAttributes().getSourceBranch();
214+
if (this.cancelPendingBuildsOnUpdate) {
215+
this.mergeRequestBuildHandler.cancelPendingBuilds(job, sourceProjectId, sourceBranch);
216+
}
217+
if (this.cancelRunningBuildsOnUpdate) {
218+
this.mergeRequestBuildHandler.cancelRunningBuilds(job, sourceProjectId, sourceBranch);
219+
}
171220
}
172221

173222
@Override

src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import com.dabsquared.gitlabjenkins.gitlab.hook.model.State;
1717
import com.dabsquared.gitlabjenkins.trigger.filter.BranchFilter;
1818
import com.dabsquared.gitlabjenkins.trigger.filter.MergeRequestLabelFilter;
19-
import com.dabsquared.gitlabjenkins.trigger.handler.PendingBuildsHandler;
19+
import com.dabsquared.gitlabjenkins.trigger.handler.MergeRequestBuildHandler;
2020
import com.dabsquared.gitlabjenkins.util.LoggerUtil;
2121
import hudson.model.Action;
2222
import hudson.model.CauseAction;
@@ -181,7 +181,7 @@ private CauseData retrieveCauseData(PushHook hook, Project project, MergeRequest
181181
}
182182

183183
private void setCommitStatusPendingIfNecessary(Job<?, ?> job, Integer projectId, String commit, String ref) {
184-
String buildName = PendingBuildsHandler.resolvePendingBuildName(job);
184+
String buildName = MergeRequestBuildHandler.resolvePendingBuildName(job);
185185
if (StringUtils.isNotBlank(buildName)) {
186186
GitLabClient client =
187187
job.getProperty(GitLabConnectionProperty.class).getClient();

src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/config.jelly

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
<f:entry title="Cancel pending merge request builds on update" field="cancelPendingBuildsOnUpdate">
6060
<f:checkbox default="false"/>
6161
</f:entry>
62+
<f:entry title="Cancel running merge request builds on update" field="cancelRunningBuildsOnUpdate">
63+
<f:checkbox default="false"/>
64+
</f:entry>
6265

6366
<f:entry title="Allowed branches">
6467
<div>

0 commit comments

Comments
 (0)