Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.jenkins.plugins.pipelinegraphview.consoleview;

import static io.jenkins.plugins.pipelinegraphview.utils.PipelineGraphApi.BuildScheduleResult;

import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.console.AnnotatedLargeText;
import hudson.model.Result;
import hudson.util.HttpResponses;
import io.jenkins.plugins.pipelinegraphview.Messages;
import io.jenkins.plugins.pipelinegraphview.PipelineGraphViewConfiguration;
import io.jenkins.plugins.pipelinegraphview.cards.RunDetailsCard;
import io.jenkins.plugins.pipelinegraphview.cards.RunDetailsItem;
Expand All @@ -24,13 +28,16 @@
import java.util.HashMap;
import java.util.List;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.workflow.cps.replay.ReplayAction;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.GET;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -78,6 +85,47 @@
return target.getUrl();
}

/**
* Handles the rerun request using ReplayAction feature
*/
@RequirePOST
@JavaScriptMethod
public HttpResponse doRerun() {
BuildScheduleResult result = api.scheduleBuild(run -> {
ReplayAction replayAction = run.getAction(ReplayAction.class);
return replayAction.run2(replayAction.getOriginalScript(), replayAction.getOriginalLoadedScripts());
});

if (result instanceof BuildScheduleResult.NotScheduled nope) {

Check warning on line 99 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 99 is only partially covered, one branch is missing
return HttpResponses.errorJSON(nope.message());

Check warning on line 100 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 100 is not covered by tests
}

JSONObject obj = new JSONObject();
BuildScheduleResult.Scheduled scheduled = (BuildScheduleResult.Scheduled) result;
obj.put("buildNumber", scheduled.buildNumber());
obj.put("message", scheduled.message());
return HttpResponses.okJSON(obj);
}

/**
* Handles the cancel request.
*/
@RequirePOST
@JavaScriptMethod
public HttpResponse doCancel() {
if (run == null) {

Check warning on line 116 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 116 is only partially covered, one branch is missing
return HttpResponses.errorJSON("No run to cancel");

Check warning on line 117 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 117 is not covered by tests
}
run.checkPermission(getCancelPermission());
if (run.isBuilding()) {

Check warning on line 120 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 120 is only partially covered, one branch is missing
run.doStop();
return HttpResponses.okJSON();
}
String message =
Result.ABORTED.equals(run.getResult()) ? Messages.run_alreadyCancelled() : Messages.run_isFinished();
return HttpResponses.errorJSON(message);

Check warning on line 126 in src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 125-126 are not covered by tests
}

// Legacy - leave in case we want to update a sub section of steps (e.g. if a stage is still
// running).
@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@
import hudson.model.BallColor;
import hudson.model.Item;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.security.Permission;
import hudson.util.HttpResponses;
import io.jenkins.plugins.pipelinegraphview.Messages;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
Expand All @@ -23,13 +19,10 @@
import net.sf.json.JSONObject;
import org.jenkins.ui.icon.IconSpec;
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.RestartDeclarativePipelineAction;
import org.jenkinsci.plugins.workflow.cps.replay.ReplayAction;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import org.kohsuke.stapler.interceptor.RequirePOST;

public abstract class AbstractPipelineViewAction implements Action, IconSpec {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Expand Down Expand Up @@ -88,47 +81,6 @@ public boolean isRestartFromStageAvailable() {
return false;
}

/**
* Handles the rerun request using ReplayAction feature
*/
@RequirePOST
@JavaScriptMethod
public boolean doRerun() throws IOException, ExecutionException {
if (run != null) {
run.checkAnyPermission(Item.BUILD);
ReplayAction replayAction = run.getAction(ReplayAction.class);
Queue.Item item =
replayAction.run2(replayAction.getOriginalScript(), replayAction.getOriginalLoadedScripts());

if (item == null) {
return false;
}
return true;
}
return false;
}

/**
* Handles the cancel request.
*/
@RequirePOST
@JavaScriptMethod
public HttpResponse doCancel() throws IOException, ExecutionException {
if (run != null) {
run.checkPermission(getCancelPermission());
if (run.isBuilding()) {
run.doStop();
return HttpResponses.okJSON();
} else {
String message = Result.ABORTED.equals(run.getResult())
? Messages.run_alreadyCancelled()
: Messages.run_isFinished();
return HttpResponses.errorJSON(message);
}
}
return HttpResponses.errorJSON("No run to cancel");
}

public String getFullBuildDisplayName() {
return run.getFullDisplayName();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import static java.util.Collections.emptyList;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Queue;
import io.jenkins.plugins.pipelinegraphview.Messages;
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
import java.io.IOException;
import java.util.*;
Expand All @@ -23,28 +25,57 @@

public class PipelineGraphApi {
private static final Logger logger = LoggerFactory.getLogger(PipelineGraphApi.class);
private static final BuildScheduleResult NOT_SCHEDULED =
new BuildScheduleResult.NotScheduled(Messages.scheduled_failure());

private final transient WorkflowRun run;

public PipelineGraphApi(WorkflowRun run) {
this.run = run;
}

public Integer replay() throws ExecutionException, InterruptedException, TimeoutException {
run.checkPermission(Item.BUILD);
BuildScheduleResult result = scheduleBuild(run -> {
CauseAction causeAction = new CauseAction(new Cause.UserIdCause());
return Queue.getInstance()
.schedule2(run.getParent(), 0, causeAction)
.getItem();
});
// when java 21+ we can use switch expression
if (result instanceof BuildScheduleResult.Scheduled s) {
return s.buildNumber();
}
return null;

Check warning on line 48 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 38-48 are not covered by tests
}

CauseAction causeAction = new CauseAction(new Cause.UserIdCause());
public @NonNull BuildScheduleResult scheduleBuild(Function<WorkflowRun, Queue.Item> scheduler) {
if (run == null) {

Check warning on line 52 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 52 is only partially covered, one branch is missing
return NOT_SCHEDULED;
}

run.checkPermission(Item.BUILD);

if (!run.getParent().isBuildable()) {
return null;
return NOT_SCHEDULED;

Check warning on line 59 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 53-59 are not covered by tests
}

Queue.Item item =
Queue.getInstance().schedule2(run.getParent(), 0, causeAction).getItem();
Queue.Item item = scheduler.apply(run);

if (item == null) {
return null;
return NOT_SCHEDULED;

Check warning on line 65 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 65 is not covered by tests
}

return run.getParent().getNextBuildNumber();
return new BuildScheduleResult.Scheduled(run.getParent().getNextBuildNumber());
}

public sealed interface BuildScheduleResult {
record NotScheduled(String message) implements BuildScheduleResult {}

record Scheduled(int buildNumber) implements BuildScheduleResult {
public String message() {
return Messages.scheduled_success(buildNumber);
}
}
}

private List<PipelineStageInternal> getPipelineNodes(PipelineGraphBuilderApi builder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ FlowNodeWrapper.noStage=System Generated
run.alreadyCancelled=Run was already cancelled
run.isFinished=Run is already finished

scheduled.success=Build scheduled - #{0}
scheduled.failure=Could not schedule rebuild

# Settings
settings=Settings
settings.showStageName=Show stage names
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,18 @@
</a>
</j:if>
${it.buildDisplayName}
<j:if test="${it.nextBuildNumber!=null}">
<a href="../../${it.nextBuildNumber}/pipeline-overview" class="jenkins-button jenkins-button--tertiary app-details__prev_next" tooltip="${%Next Build}">
<l:icon class="symbol-chevron-forward-outline plugin-ionicons-api icon-md" />
</a>
</j:if>
<j:choose>
<j:when test="${it.nextBuildNumber!=null}">
<a href="../../${it.nextBuildNumber}/pipeline-overview" class="jenkins-button jenkins-button--tertiary app-details__prev_next" tooltip="${%Next Build}">
<l:icon class="symbol-chevron-forward-outline plugin-ionicons-api icon-md" />
</a>
</j:when>
<j:otherwise>
<a data-module="next-build" data-url-pattern="../../NEXT_BUILD_NUMBER/pipeline-overview" class="jenkins-button jenkins-button--tertiary app-details__prev_next jenkins-hidden" tooltip="${%Next Build}">
<l:icon class="symbol-chevron-forward-outline plugin-ionicons-api icon-md" />
</a>
</j:otherwise>
</j:choose>
</t:buildCaption>

<div class="pgv-details">
Expand Down
20 changes: 18 additions & 2 deletions src/main/webapp/js/build.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
/* global notificationBar */

const rerunButton = document.getElementById('pgv-rerun');

if (rerunButton) {
rerunButton.addEventListener('click', event => {
event.preventDefault();
const rerunAction = window[`${rerunButton.dataset.proxyName}`];
function updateNextBuildButton(buildNumber) {
const nextBuild = document.querySelector("[data-module='next-build']");
if (nextBuild) {
nextBuild.classList.remove("jenkins-hidden");
nextBuild.href = nextBuild.dataset.urlPattern.replace("NEXT_BUILD_NUMBER", buildNumber);
nextBuild.removeAttribute("data-module");
nextBuild.removeAttribute("data-url-pattern");
}
}

rerunAction.doRerun(function (success) {
const result = success.responseJSON;
if (result) {
notificationBar.show(rerunButton.dataset.successMessage, notificationBar.SUCCESS);
if (result?.status === "ok") {
notificationBar.show(result.data.message, notificationBar.SUCCESS);
updateNextBuildButton(result.data.buildNumber);
} else {
const failMessage = result?.status === "error" && result.data.message ? result.data.message : "Unknown error occurred while trying to rerun the build.";
notificationBar.show(failMessage, notificationBar.WARNING);
}
});
})
Expand Down