Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -304,7 +305,8 @@ properties([
exclude: ""
],
pendingBuildName: "jenkins",
cancelPendingBuildsOnUpdate: true
cancelPendingBuildsOnUpdate: true,
cancelRunningBuildsOnUpdate: true
]
])
])
Expand Down Expand Up @@ -469,6 +471,7 @@ triggers {
excludeBranchesSpec: "",
pendingBuildName: "Jenkins",
cancelPendingBuildsOnUpdate: false,
cancelRunningBuildsOnUpdate: false,
secretToken: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEF",
triggerToBranchDeleteRequest: false,
triggerOnlyIfNewCommitsPushed: false,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
private volatile Secret secretToken;
private String pendingBuildName;
private boolean cancelPendingBuildsOnUpdate;
private boolean cancelRunningBuildsOnUpdate;

private transient BranchFilter branchFilter;
private transient PushHookTriggerHandler pushHookTriggerHandler;
Expand Down Expand Up @@ -347,6 +348,11 @@
return this.cancelPendingBuildsOnUpdate;
}

@Override
public boolean getCancelRunningBuildsOnUpdate() {
return this.cancelRunningBuildsOnUpdate;
}

@DataBoundSetter
public void setTriggerOnPush(boolean triggerOnPush) {
this.triggerOnPush = triggerOnPush;
Expand Down Expand Up @@ -516,6 +522,11 @@
this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate;
}

@DataBoundSetter
public void setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) {
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
}

Check warning on line 528 in src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 527-528 are not covered by tests

// executes when the Trigger receives a push request
public void onPost(final PushHook hook) {
if (branchFilter == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@
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;

Check warning on line 30 in src/main/java/com/dabsquared/gitlabjenkins/MergeRequestTriggerConfig.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 30 is not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
public abstract class AbstractWebHookTriggerHandler<H extends WebHook> implements WebHookTriggerHandler<H> {

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

@Override
public void handle(
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@
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 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;
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();
Expand All @@ -45,6 +49,63 @@
}
}

/**
* 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) {

Check warning on line 80 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 80 is only partially covered, one branch is missing
return;

Check warning on line 81 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 81 is not covered by tests
}
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);

Check warning on line 90 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 89-90 are not covered by tests
}
}

/** 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 + "'";

Check warning on line 105 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/MergeRequestBuildHandler.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 105 is not covered by tests
}
}

private GitLabWebHookCause getGitLabWebHookCauseData(Queue.Item item) {
for (Cause cause : item.getCauses()) {
if (cause instanceof GitLabWebHookCause hookCause) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
boolean skipWorkInProgressMergeRequest,
String labelsThatForcesBuildIfAdded,
boolean triggerOnApprovedMergeRequest,
boolean cancelPendingBuildsOnUpdate) {
boolean cancelPendingBuildsOnUpdate,
boolean cancelRunningBuildsOnUpdate) {
Comment thread
manjukion marked this conversation as resolved.

TriggerConfigChain chain = new TriggerConfigChain();
chain.rejectUnless(
Expand All @@ -54,7 +55,8 @@
triggerOnlyWithNewCommitsPushed,
skipWorkInProgressMergeRequest,
labelsThatForcesBuildIfAddedSet,
cancelPendingBuildsOnUpdate);
cancelPendingBuildsOnUpdate,
cancelRunningBuildsOnUpdate);
}

public static MergeRequestHookTriggerHandler newMergeRequestHookTriggerHandler(MergeRequestTriggerConfig config) {
Expand All @@ -67,7 +69,8 @@
config.isSkipWorkInProgressMergeRequest(),
config.getLabelsThatForcesBuildIfAdded(),
config.isTriggerOnApprovedMergeRequest(),
config.getCancelPendingBuildsOnUpdate());
config.getCancelPendingBuildsOnUpdate(),
config.getCancelRunningBuildsOnUpdate());
}

public static Config withConfig() {
Expand All @@ -84,6 +87,7 @@
private String labelsThatForcesBuildIfAdded;
private boolean triggerOnApprovedMergeRequest = false;
private boolean cancelPendingBuildsOnUpdate = false;
private boolean cancelRunningBuildsOnUpdate = false;

@Override
public boolean getTriggerOnMergeRequest() {
Expand Down Expand Up @@ -130,6 +134,11 @@
return cancelPendingBuildsOnUpdate;
}

@Override
public boolean getCancelRunningBuildsOnUpdate() {
return cancelRunningBuildsOnUpdate;
}

public Config setTriggerOnMergeRequest(boolean triggerOnMergeRequest) {
this.triggerOnMergeRequest = triggerOnMergeRequest;
return this;
Expand Down Expand Up @@ -175,6 +184,11 @@
return this;
}

public Config setCancelRunningBuildsOnUpdate(boolean cancelRunningBuildsOnUpdate) {
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
return this;

Check warning on line 189 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 188-189 are not covered by tests
}

public MergeRequestHookTriggerHandler build() {
return newMergeRequestHookTriggerHandler(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,27 @@
private final EnumSet<Action> skipBuiltYetCheckActions = EnumSet.of(Action.open, Action.approved, Action.merge);
private final EnumSet<Action> skipAllowedStateForActions = EnumSet.of(Action.approved);
private final boolean cancelPendingBuildsOnUpdate;
private final boolean cancelRunningBuildsOnUpdate;

MergeRequestHookTriggerHandlerImpl(
Collection<State> allowedStates,
boolean skipWorkInProgressMergeRequest,
boolean cancelPendingBuildsOnUpdate) {
this(allowedStates, skipWorkInProgressMergeRequest, cancelPendingBuildsOnUpdate, false);
}

Check warning on line 57 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 56-57 are not covered by tests

MergeRequestHookTriggerHandlerImpl(
Collection<State> 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.
Expand All @@ -69,12 +79,30 @@
boolean onlyIfNewCommitsPushed,
boolean skipWorkInProgressMergeRequest,
boolean cancelPendingBuildsOnUpdate) {
this(
allowedStates,
allowedActions,
onlyIfNewCommitsPushed,
skipWorkInProgressMergeRequest,
cancelPendingBuildsOnUpdate,
false);
}

@Deprecated
MergeRequestHookTriggerHandlerImpl(
Collection<State> allowedStates,
Collection<Action> 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(
Expand All @@ -83,11 +111,28 @@
boolean skipWorkInProgressMergeRequest,
Set<String> labelsThatForcesBuildIfAdded,
boolean cancelPendingBuildsOnUpdate) {
this(
triggerConfig,
onlyIfNewCommitsPushed,
skipWorkInProgressMergeRequest,
labelsThatForcesBuildIfAdded,
cancelPendingBuildsOnUpdate,
false);
}

Check warning on line 121 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 114-121 are not covered by tests

MergeRequestHookTriggerHandlerImpl(
Predicate<MergeRequestObjectAttributes> triggerConfig,
boolean onlyIfNewCommitsPushed,
boolean skipWorkInProgressMergeRequest,
Set<String> labelsThatForcesBuildIfAdded,
boolean cancelPendingBuildsOnUpdate,
boolean cancelRunningBuildsOnUpdate) {
this.triggerConfig = triggerConfig;
this.onlyIfNewCommitsPushed = onlyIfNewCommitsPushed;
this.skipWorkInProgressMergeRequest = skipWorkInProgressMergeRequest;
this.labelsThatForcesBuildIfAdded = labelsThatForcesBuildIfAdded;
this.cancelPendingBuildsOnUpdate = cancelPendingBuildsOnUpdate;
this.cancelRunningBuildsOnUpdate = cancelRunningBuildsOnUpdate;
}

@Override
Expand Down Expand Up @@ -158,16 +203,20 @@

@Override
protected void cancelPendingBuildsIfNecessary(Job<?, ?> job, MergeRequestHook hook) {
if (!this.cancelPendingBuildsOnUpdate) {
if (!this.cancelPendingBuildsOnUpdate && !this.cancelRunningBuildsOnUpdate) {

Check warning on line 206 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 206 is only partially covered, one branch is missing
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) {

Check warning on line 214 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 214 is only partially covered, one branch is missing
this.mergeRequestBuildHandler.cancelPendingBuilds(job, sourceProjectId, sourceBranch);
}
if (this.cancelRunningBuildsOnUpdate) {

Check warning on line 217 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 217 is only partially covered, one branch is missing
this.mergeRequestBuildHandler.cancelRunningBuilds(job, sourceProjectId, sourceBranch);

Check warning on line 218 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/merge/MergeRequestHookTriggerHandlerImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 218 is not covered by tests
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -181,7 +181,7 @@
}

private void setCommitStatusPendingIfNecessary(Job<?, ?> job, Integer projectId, String commit, String ref) {
String buildName = PendingBuildsHandler.resolvePendingBuildName(job);
String buildName = MergeRequestBuildHandler.resolvePendingBuildName(job);

Check warning on line 184 in src/main/java/com/dabsquared/gitlabjenkins/trigger/handler/push/OpenMergeRequestPushHookTriggerHandler.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 184 is not covered by tests
if (StringUtils.isNotBlank(buildName)) {
GitLabClient client =
job.getProperty(GitLabConnectionProperty.class).getClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
<f:entry title="Cancel pending merge request builds on update" field="cancelPendingBuildsOnUpdate">
<f:checkbox default="false"/>
</f:entry>
<f:entry title="Cancel running merge request builds on update" field="cancelRunningBuildsOnUpdate">
<f:checkbox default="false"/>
</f:entry>

<f:entry title="Allowed branches">
<div>
Expand Down
Loading