diff --git a/src/main/frontend/common/components/status-icon.tsx b/src/main/frontend/common/components/status-icon.tsx index c7b7aeb9c..666fef5fd 100644 --- a/src/main/frontend/common/components/status-icon.tsx +++ b/src/main/frontend/common/components/status-icon.tsx @@ -252,6 +252,8 @@ export function resultToColor(result: Result, skeleton: boolean | undefined) { return "jenkins-!-accent-color"; case "unstable": return "jenkins-!-warning-color"; + case "paused": + return "jenkins-!-accent-color"; default: return "jenkins-!-skipped-color"; } diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java index a3c24412e..5962e625d 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java @@ -11,6 +11,7 @@ import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.support.steps.input.InputAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,6 +68,17 @@ private PipelineGraph createTree(PipelineGraphBuilderApi builder) { // these are completely new representations. List stages = getPipelineNodes(builder); + // Get InputAction once for all stages + InputAction inputAction = run.getAction(InputAction.class); + + // Set the builder and inputAction on each stage so they can check for paused steps + if (builder instanceof PipelineStepBuilderApi stepBuilder) { + stages.forEach(stage -> { + stage.setBuilder(stepBuilder); + stage.setInputAction(inputAction); + }); + } + // id => stage Map stageMap = stages.stream() .collect(Collectors.toMap( diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java index 9c7eae53c..fe217e254 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -4,6 +4,7 @@ import io.jenkins.plugins.pipelinegraphview.analysis.TimingInfo; import java.util.Collections; import java.util.List; +import org.jenkinsci.plugins.workflow.support.steps.input.InputAction; class PipelineStageInternal { @@ -19,6 +20,8 @@ class PipelineStageInternal { private boolean synthetic; private TimingInfo timingInfo; private String agent; + private PipelineStepBuilderApi builder; + private InputAction inputAction; public PipelineStageInternal( String id, @@ -113,12 +116,60 @@ public void setAgent(String aAgent) { this.agent = aAgent; } + public void setBuilder(PipelineStepBuilderApi builder) { + this.builder = builder; + } + + public void setInputAction(InputAction inputAction) { + this.inputAction = inputAction; + } + + /** + * Checks if this stage or any of its children are waiting for input. + * A stage is waiting for input if any of its steps have a non-null inputStep + * and the step state is PAUSED. + * Only performs the check if there is an InputAction attached to the WorkflowRun. + */ + private boolean isWaitingForInput(List children) { + // Early exit if there's no InputAction on the run + if (inputAction == null) { + return false; + } + + // Check if any child stages are waiting for input + if (children != null && !children.isEmpty()) { + for (PipelineStage child : children) { + if (child.state == PipelineState.PAUSED) { + return true; + } + } + } + + // Check steps for this stage + if (builder != null && id != null) { + List steps = builder.getStageSteps(id); + if (steps != null) { + for (FlowNodeWrapper step : steps) { + // Check if step has an input and is paused + if (step.getInputStep() != null && step.getStatus().state == BlueRun.BlueRunState.PAUSED) { + return true; + } + } + } + } + + return false; + } + public PipelineStage toPipelineStage(List children, String runUrl) { + boolean waitingForInput = isWaitingForInput(children); + PipelineState effectiveState = waitingForInput ? PipelineState.PAUSED : state; + return new PipelineStage( id, name, children, - state, + effectiveState, type.name(), title, seqContainerName, diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java index 114e62bd1..336345d17 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java @@ -532,4 +532,43 @@ void createTree_branchResult() throws Exception { "unstable-branch{unstable}", "]"))); } + + @Issue("GH#967") + @Test + void createTree_stageWithInputStepShowsAsPaused() throws Exception { + WorkflowJob job = TestUtils.createJob(j, "pipelineWithInput", "input.jenkinsfile"); + QueueTaskFuture futureRun = job.scheduleBuild2(0); + WorkflowRun run = futureRun.waitForStart(); + + // Wait for the input action to be available and have executions + org.jenkinsci.plugins.workflow.support.steps.input.InputAction inputAction = null; + while (inputAction == null || inputAction.getExecutions().isEmpty()) { + inputAction = run.getAction(org.jenkinsci.plugins.workflow.support.steps.input.InputAction.class); + Thread.sleep(100); + } + + // Wait a bit more for the pause to be registered + Thread.sleep(500); + + // Check the graph while paused on input + PipelineGraphApi api = new PipelineGraphApi(run); + PipelineGraph graph = api.createTree(); + + // Find the stage with input + PipelineStage inputStage = graph.stages.stream() + .filter(s -> s.name.equals("Input")) + .findFirst() + .orElseThrow(); + + // Verify it shows as PAUSED + assertThat(inputStage.state, equalTo(PipelineState.PAUSED)); + + // Approve the input and wait for completion + org.jenkinsci.plugins.workflow.support.steps.input.InputStepExecution execution = + inputAction.getExecutions().get(0); + execution.proceed(null); + j.waitForCompletion(run); + + assertThat(run.getResult(), equalTo(Result.SUCCESS)); + } }