Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions src/main/frontend/common/components/status-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -67,6 +68,17 @@ private PipelineGraph createTree(PipelineGraphBuilderApi builder) {
// these are completely new representations.
List<PipelineStageInternal> 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate PR could refactor the need for this instanceof check pretty easily, there's only one implementation and the indirection is no longer needed

stages.forEach(stage -> {
stage.setBuilder(stepBuilder);
stage.setInputAction(inputAction);
});
}

// id => stage
Map<String, PipelineStageInternal> stageMap = stages.stream()
.collect(Collectors.toMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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,
Expand Down Expand Up @@ -113,12 +116,60 @@ public void setAgent(String aAgent) {
this.agent = aAgent;
}

public void setBuilder(PipelineStepBuilderApi builder) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be avoided by doing the refactoring mentioned in PipelineGraphApi comment and then you could just pass the builder in the constructor (or another constructor) as I'd prefer not to be mutating state

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<PipelineStage> 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we get some test coverage for this please?

A pipeline that sets an input step on it and validates the stage is now paused.

}
}
}

// Check steps for this stage
if (builder != null && id != null) {
List<FlowNodeWrapper> 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<PipelineStage> children, String runUrl) {
boolean waitingForInput = isWaitingForInput(children);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we only do this check if there is an InputAction attached to the WorkflowRun?

I think that should be optimisable so this check is only run over stages that could be waiting for input?

PipelineState effectiveState = waitingForInput ? PipelineState.PAUSED : state;

return new PipelineStage(
id,
name,
children,
state,
effectiveState,
type.name(),
title,
seqContainerName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.pipelinegraphview.utils;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

Expand Down Expand Up @@ -532,4 +533,38 @@ 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<WorkflowRun> 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);
}

// Check the graph while paused on input
PipelineGraphApi api = new PipelineGraphApi(run);

await().until(() -> {
PipelineGraph graph = api.createTree();
PipelineStage inputStage = graph.stages.stream()
.filter(s -> s.name.equals("Input"))
.findFirst()
.orElse(null);
return inputStage == null ? null : 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));
}
}