From fdea504e53e18b91dea8ace15d368fb3d428d1ea Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Wed, 26 Nov 2025 16:37:01 +0200 Subject: [PATCH 01/16] fix: added support for paused status in stages that are waiting for input --- .../common/components/status-icon.tsx | 2 + .../utils/PipelineGraphApi.java | 6 +++ .../utils/PipelineStage.java | 8 +++- .../utils/PipelineStageInternal.java | 41 ++++++++++++++++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/common/components/status-icon.tsx b/src/main/frontend/common/components/status-icon.tsx index c7b7aeb9c..03f8c3023 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-!-running-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..9fcfdc055 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java @@ -67,6 +67,12 @@ private PipelineGraph createTree(PipelineGraphBuilderApi builder) { // these are completely new representations. List stages = getPipelineNodes(builder); + // Set the builder on each stage so they can check for paused steps + if (builder instanceof PipelineStepBuilderApi) { + PipelineStepBuilderApi stepBuilder = (PipelineStepBuilderApi) builder; + stages.forEach(stage -> stage.setBuilder(stepBuilder)); + } + // id => stage Map stageMap = stages.stream() .collect(Collectors.toMap( diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java index 06f7017f7..a3c9b72d5 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java @@ -17,6 +17,7 @@ public class PipelineStage extends AbstractPipelineNode { private final boolean placeholder; final String agent; private final String url; + public final boolean waitingForInput; public PipelineStage( String id, @@ -32,8 +33,9 @@ public PipelineStage( boolean placeholder, TimingInfo timingInfo, String agent, - String runUrl) { - super(id, name, state, type, title, timingInfo); + String runUrl, + boolean waitingForInput) { + super(id, name, waitingForInput ? PipelineState.PAUSED : state, type, title, timingInfo); this.children = children; this.seqContainerName = seqContainerName; this.nextSibling = nextSibling; @@ -42,6 +44,7 @@ public PipelineStage( this.placeholder = placeholder; this.agent = agent; this.url = "/" + runUrl + URL_NAME + "?selected-node=" + id; + this.waitingForInput = waitingForInput; } public static class PipelineStageJsonProcessor extends AbstractPipelineNodeJsonProcessor { @@ -64,6 +67,7 @@ public JSONObject processBean(Object bean, JsonConfig config) { json.element("placeholder", stage.placeholder); json.element("agent", stage.agent); json.element("url", stage.url); + json.element("waitingForInput", stage.waitingForInput); return json; } } 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..1c53b243a 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -19,6 +19,7 @@ class PipelineStageInternal { private boolean synthetic; private TimingInfo timingInfo; private String agent; + private PipelineStepBuilderApi builder; public PipelineStageInternal( String id, @@ -113,7 +114,44 @@ public void setAgent(String aAgent) { this.agent = aAgent; } + public void setBuilder(PipelineStepBuilderApi builder) { + this.builder = builder; + } + + /** + * 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. + */ + private boolean isWaitingForInput(List children) { + // Check if any child stages are waiting for input + if (children != null && !children.isEmpty()) { + for (PipelineStage child : children) { + if (child.waitingForInput) { + 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); return new PipelineStage( id, name, @@ -128,6 +166,7 @@ public PipelineStage toPipelineStage(List children, String runUrl synthetic && name.equals(Messages.FlowNodeWrapper_noStage()), timingInfo, agent, - runUrl); + runUrl, + waitingForInput); } } From bb3e1eb797e839f290eada26320455db3b357a8c Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Thu, 27 Nov 2025 07:40:35 +0200 Subject: [PATCH 02/16] fix: updated result color for paused icon --- src/main/frontend/common/components/status-icon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/common/components/status-icon.tsx b/src/main/frontend/common/components/status-icon.tsx index 03f8c3023..666fef5fd 100644 --- a/src/main/frontend/common/components/status-icon.tsx +++ b/src/main/frontend/common/components/status-icon.tsx @@ -253,7 +253,7 @@ export function resultToColor(result: Result, skeleton: boolean | undefined) { case "unstable": return "jenkins-!-warning-color"; case "paused": - return "jenkins-!-running-color"; + return "jenkins-!-accent-color"; default: return "jenkins-!-skipped-color"; } From 82b0bb6727d47bed93e5f26970487f354aa8a730 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Thu, 27 Nov 2025 12:37:03 +0200 Subject: [PATCH 03/16] fix: added a test and fixed bad test setup --- .../common/components/status-icon.spec.tsx | 17 +++++++++++++++++ src/main/frontend/setupTests.ts | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/frontend/common/components/status-icon.spec.tsx b/src/main/frontend/common/components/status-icon.spec.tsx index 9bdcb0f29..1079e04b3 100644 --- a/src/main/frontend/common/components/status-icon.spec.tsx +++ b/src/main/frontend/common/components/status-icon.spec.tsx @@ -127,6 +127,23 @@ describe("StatusIcon", () => { expect(result.current).to.equal(0); unmount(); }); + it("should continue progress when waitingForInput is true", async () => { + const { result, unmount } = renderHook(() => { + return useStageProgress({ + ...mockStage, + state: Result.running, + startTimeMillis: now - 2_000, + previousTotalDurationMillis: 20_000, + waitingForInput: true, + }); + }); + expect(result.current).to.equal(10); + await act(() => vi.advanceTimersByTime(1_000)); + expect(result.current).to.equal(15); + await act(() => vi.advanceTimersByTime(1_000)); + expect(result.current).to.equal(20); + unmount(); + }); }); describe("other states", function () { for (const state in Result) { diff --git a/src/main/frontend/setupTests.ts b/src/main/frontend/setupTests.ts index 43b851665..02efd1272 100644 --- a/src/main/frontend/setupTests.ts +++ b/src/main/frontend/setupTests.ts @@ -8,3 +8,20 @@ const IntersectionObserverMock = vi.fn(() => ({ })); vi.stubGlobal("IntersectionObserver", IntersectionObserverMock); + +// Ensure a functional localStorage implementation for tests that persist user preferences. +if ( + !('localStorage' in window) || + typeof window.localStorage?.getItem !== 'function' || + typeof window.localStorage?.setItem !== 'function' +) { + const store: Record = {}; + const localStoragePolyfill = { + getItem: (key: string) => (key in store ? store[key] : null), + setItem: (key: string, value: string) => { store[key] = String(value); }, + removeItem: (key: string) => { delete store[key]; }, + clear: () => { Object.keys(store).forEach(k => delete store[k]); }, + }; + // @ts-expect-error override for test environment + window.localStorage = localStoragePolyfill; +} From 8894ffa9d706c0bdfe49a79b3db7f0033356f766 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Thu, 27 Nov 2025 12:37:24 +0200 Subject: [PATCH 04/16] chore: updated pipeline model --- openapi.yaml | 3 +++ .../pipeline-graph/main/PipelineGraphModel.tsx | 1 + 2 files changed, 4 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index fcd27d459..9d0f81647 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -376,6 +376,9 @@ components: - null format: int64 description: Total duration in milliseconds (null if still running) + waitingForInput: + type: boolean + description: Whether the stage is waiting for user input children: type: array items: diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx index c3a437505..7bc222dcf 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx @@ -57,6 +57,7 @@ export interface StageInfo { skeleton?: boolean; pauseLiveTotal?: boolean; + waitingForInput?: boolean; } interface BaseNodeInfo { From a8c30935c54f631e2944760e7e593ecff7a85622 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Thu, 27 Nov 2025 13:21:29 +0200 Subject: [PATCH 05/16] fix: fixed code style issues --- src/main/frontend/setupTests.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/frontend/setupTests.ts b/src/main/frontend/setupTests.ts index 02efd1272..c5411a038 100644 --- a/src/main/frontend/setupTests.ts +++ b/src/main/frontend/setupTests.ts @@ -11,16 +11,22 @@ vi.stubGlobal("IntersectionObserver", IntersectionObserverMock); // Ensure a functional localStorage implementation for tests that persist user preferences. if ( - !('localStorage' in window) || - typeof window.localStorage?.getItem !== 'function' || - typeof window.localStorage?.setItem !== 'function' + !("localStorage" in window) || + typeof window.localStorage?.getItem !== "function" || + typeof window.localStorage?.setItem !== "function" ) { const store: Record = {}; const localStoragePolyfill = { getItem: (key: string) => (key in store ? store[key] : null), - setItem: (key: string, value: string) => { store[key] = String(value); }, - removeItem: (key: string) => { delete store[key]; }, - clear: () => { Object.keys(store).forEach(k => delete store[k]); }, + setItem: (key: string, value: string) => { + store[key] = String(value); + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + Object.keys(store).forEach((k) => delete store[k]); + }, }; // @ts-expect-error override for test environment window.localStorage = localStoragePolyfill; From c063d8807c5af8a45b63a21baf994e8343368ac0 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Thu, 27 Nov 2025 14:04:04 +0200 Subject: [PATCH 06/16] fix: more formatting fixes --- .../plugins/pipelinegraphview/utils/PipelineStageInternal.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 1c53b243a..acfa74a51 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -139,8 +139,7 @@ private boolean isWaitingForInput(List children) { 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) { + if (step.getInputStep() != null && step.getStatus().state == BlueRun.BlueRunState.PAUSED) { return true; } } From ea480b6307fc1a23b85bb4ddf51bf0e31a3e042c Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Mon, 1 Dec 2025 12:13:34 +0200 Subject: [PATCH 07/16] fix: removed storage implementation for tests --- src/main/frontend/setupTests.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/main/frontend/setupTests.ts b/src/main/frontend/setupTests.ts index c5411a038..43b851665 100644 --- a/src/main/frontend/setupTests.ts +++ b/src/main/frontend/setupTests.ts @@ -8,26 +8,3 @@ const IntersectionObserverMock = vi.fn(() => ({ })); vi.stubGlobal("IntersectionObserver", IntersectionObserverMock); - -// Ensure a functional localStorage implementation for tests that persist user preferences. -if ( - !("localStorage" in window) || - typeof window.localStorage?.getItem !== "function" || - typeof window.localStorage?.setItem !== "function" -) { - const store: Record = {}; - const localStoragePolyfill = { - getItem: (key: string) => (key in store ? store[key] : null), - setItem: (key: string, value: string) => { - store[key] = String(value); - }, - removeItem: (key: string) => { - delete store[key]; - }, - clear: () => { - Object.keys(store).forEach((k) => delete store[k]); - }, - }; - // @ts-expect-error override for test environment - window.localStorage = localStoragePolyfill; -} From 0f36e990e765653c3d4ea15b3b69baa52bad0a24 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Tue, 2 Dec 2025 08:55:56 +0200 Subject: [PATCH 08/16] fix: removed waitingForInput from PipelineGraphModel --- src/main/frontend/common/components/status-icon.spec.tsx | 1 - .../pipeline-graph/main/PipelineGraphModel.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/frontend/common/components/status-icon.spec.tsx b/src/main/frontend/common/components/status-icon.spec.tsx index 1079e04b3..83ac51eb8 100644 --- a/src/main/frontend/common/components/status-icon.spec.tsx +++ b/src/main/frontend/common/components/status-icon.spec.tsx @@ -134,7 +134,6 @@ describe("StatusIcon", () => { state: Result.running, startTimeMillis: now - 2_000, previousTotalDurationMillis: 20_000, - waitingForInput: true, }); }); expect(result.current).to.equal(10); diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx index 7bc222dcf..c3a437505 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx @@ -57,7 +57,6 @@ export interface StageInfo { skeleton?: boolean; pauseLiveTotal?: boolean; - waitingForInput?: boolean; } interface BaseNodeInfo { From 974423142f1b342ace8a5d587f58a49ff3adc88a Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Tue, 2 Dec 2025 13:34:09 +0200 Subject: [PATCH 09/16] fix: removed additional references to waiting for input that were are not used --- openapi.yaml | 3 --- .../jenkins/plugins/pipelinegraphview/utils/PipelineStage.java | 1 - 2 files changed, 4 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 9d0f81647..fcd27d459 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -376,9 +376,6 @@ components: - null format: int64 description: Total duration in milliseconds (null if still running) - waitingForInput: - type: boolean - description: Whether the stage is waiting for user input children: type: array items: diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java index a3c9b72d5..588fb389e 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java @@ -67,7 +67,6 @@ public JSONObject processBean(Object bean, JsonConfig config) { json.element("placeholder", stage.placeholder); json.element("agent", stage.agent); json.element("url", stage.url); - json.element("waitingForInput", stage.waitingForInput); return json; } } From 0d4b32027f309643fdb6426e86646383fad83c45 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Mon, 8 Dec 2025 09:09:23 +0200 Subject: [PATCH 10/16] refactor: moved logic for paused stages into PipelineStageInternal --- .../plugins/pipelinegraphview/utils/PipelineStage.java | 9 +++------ .../pipelinegraphview/utils/PipelineStageInternal.java | 9 +++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java index 588fb389e..a0f0d6a0f 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java @@ -17,8 +17,7 @@ public class PipelineStage extends AbstractPipelineNode { private final boolean placeholder; final String agent; private final String url; - public final boolean waitingForInput; - + public PipelineStage( String id, String name, @@ -33,9 +32,8 @@ public PipelineStage( boolean placeholder, TimingInfo timingInfo, String agent, - String runUrl, - boolean waitingForInput) { - super(id, name, waitingForInput ? PipelineState.PAUSED : state, type, title, timingInfo); + String runUrl) { + super(id, name, state, type, title, timingInfo); this.children = children; this.seqContainerName = seqContainerName; this.nextSibling = nextSibling; @@ -44,7 +42,6 @@ public PipelineStage( this.placeholder = placeholder; this.agent = agent; this.url = "/" + runUrl + URL_NAME + "?selected-node=" + id; - this.waitingForInput = waitingForInput; } public static class PipelineStageJsonProcessor extends AbstractPipelineNodeJsonProcessor { 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 acfa74a51..7aca3a36e 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -127,7 +127,7 @@ private boolean isWaitingForInput(List children) { // Check if any child stages are waiting for input if (children != null && !children.isEmpty()) { for (PipelineStage child : children) { - if (child.waitingForInput) { + if (child.state == PipelineState.PAUSED) { return true; } } @@ -151,11 +151,13 @@ private boolean isWaitingForInput(List children) { 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, @@ -165,7 +167,6 @@ public PipelineStage toPipelineStage(List children, String runUrl synthetic && name.equals(Messages.FlowNodeWrapper_noStage()), timingInfo, agent, - runUrl, - waitingForInput); + runUrl); } } From 22ed4cc61c307e9ec4a8f72c523363fc63d2b42e Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Mon, 8 Dec 2025 09:13:56 +0200 Subject: [PATCH 11/16] refactor: removed unnecessary test step --- .../common/components/status-icon.spec.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/frontend/common/components/status-icon.spec.tsx b/src/main/frontend/common/components/status-icon.spec.tsx index 83ac51eb8..9bdcb0f29 100644 --- a/src/main/frontend/common/components/status-icon.spec.tsx +++ b/src/main/frontend/common/components/status-icon.spec.tsx @@ -127,22 +127,6 @@ describe("StatusIcon", () => { expect(result.current).to.equal(0); unmount(); }); - it("should continue progress when waitingForInput is true", async () => { - const { result, unmount } = renderHook(() => { - return useStageProgress({ - ...mockStage, - state: Result.running, - startTimeMillis: now - 2_000, - previousTotalDurationMillis: 20_000, - }); - }); - expect(result.current).to.equal(10); - await act(() => vi.advanceTimersByTime(1_000)); - expect(result.current).to.equal(15); - await act(() => vi.advanceTimersByTime(1_000)); - expect(result.current).to.equal(20); - unmount(); - }); }); describe("other states", function () { for (const state in Result) { From cc945d74057e31cec14313ed32a5a61de0f43bcf Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 8 Dec 2025 07:45:54 +0000 Subject: [PATCH 12/16] Fix lint --- .../jenkins/plugins/pipelinegraphview/utils/PipelineStage.java | 2 +- .../plugins/pipelinegraphview/utils/PipelineStageInternal.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java index a0f0d6a0f..06f7017f7 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java @@ -17,7 +17,7 @@ public class PipelineStage extends AbstractPipelineNode { private final boolean placeholder; final String agent; private final String url; - + public PipelineStage( String id, String name, 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 7aca3a36e..779910892 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -152,7 +152,7 @@ private boolean isWaitingForInput(List children) { public PipelineStage toPipelineStage(List children, String runUrl) { boolean waitingForInput = isWaitingForInput(children); PipelineState effectiveState = waitingForInput ? PipelineState.PAUSED : state; - + return new PipelineStage( id, name, From a2c91402142ba5e8b43cedf1ef065ea763f7c1d4 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Mon, 8 Dec 2025 10:02:07 +0200 Subject: [PATCH 13/16] chore: codestyle fixes --- .../jenkins/plugins/pipelinegraphview/utils/PipelineStage.java | 2 +- .../plugins/pipelinegraphview/utils/PipelineStageInternal.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java index a0f0d6a0f..06f7017f7 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java @@ -17,7 +17,7 @@ public class PipelineStage extends AbstractPipelineNode { private final boolean placeholder; final String agent; private final String url; - + public PipelineStage( String id, String name, 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 7aca3a36e..779910892 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -152,7 +152,7 @@ private boolean isWaitingForInput(List children) { public PipelineStage toPipelineStage(List children, String runUrl) { boolean waitingForInput = isWaitingForInput(children); PipelineState effectiveState = waitingForInput ? PipelineState.PAUSED : state; - + return new PipelineStage( id, name, From 25917da3f244edb5589f9834cd4172b721ce3e3f Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 8 Dec 2025 09:03:22 +0000 Subject: [PATCH 14/16] Use pattern variable --- .../plugins/pipelinegraphview/utils/PipelineGraphApi.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9fcfdc055..a1fd9a874 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java @@ -68,8 +68,7 @@ private PipelineGraph createTree(PipelineGraphBuilderApi builder) { List stages = getPipelineNodes(builder); // Set the builder on each stage so they can check for paused steps - if (builder instanceof PipelineStepBuilderApi) { - PipelineStepBuilderApi stepBuilder = (PipelineStepBuilderApi) builder; + if (builder instanceof PipelineStepBuilderApi stepBuilder) { stages.forEach(stage -> stage.setBuilder(stepBuilder)); } From b9b21c4e63acbd263008d5685d102094a271f966 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Wed, 10 Dec 2025 15:31:40 +0200 Subject: [PATCH 15/16] fix: added test for input pause --- .../utils/PipelineGraphApiTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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..14949aa2c 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)); + } } From 337faa3e08c852e588638dd4b0f362a342c1c507 Mon Sep 17 00:00:00 2001 From: Leemalin Moodley Date: Fri, 12 Dec 2025 12:02:41 +0200 Subject: [PATCH 16/16] refactor: optimize input step detection by checking InputAction existence --- .../pipelinegraphview/utils/PipelineGraphApi.java | 11 +++++++++-- .../utils/PipelineStageInternal.java | 12 ++++++++++++ .../utils/PipelineGraphApiTest.java | 8 ++++---- 3 files changed, 25 insertions(+), 6 deletions(-) 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 a1fd9a874..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,9 +68,15 @@ private PipelineGraph createTree(PipelineGraphBuilderApi builder) { // these are completely new representations. List stages = getPipelineNodes(builder); - // Set the builder on each stage so they can check for paused steps + // 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)); + stages.forEach(stage -> { + stage.setBuilder(stepBuilder); + stage.setInputAction(inputAction); + }); } // id => stage 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 779910892..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 { @@ -20,6 +21,7 @@ class PipelineStageInternal { private TimingInfo timingInfo; private String agent; private PipelineStepBuilderApi builder; + private InputAction inputAction; public PipelineStageInternal( String id, @@ -118,12 +120,22 @@ 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) { 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 14949aa2c..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,7 +532,7 @@ void createTree_branchResult() throws Exception { "unstable-branch{unstable}", "]"))); } - + @Issue("GH#967") @Test void createTree_stageWithInputStepShowsAsPaused() throws Exception { @@ -564,11 +564,11 @@ void createTree_stageWithInputStepShowsAsPaused() throws Exception { 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); + 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)); } }