From ee51c9c0d786afdec184cac41545854c540826b8 Mon Sep 17 00:00:00 2001 From: Antoine Musso Date: Thu, 17 Oct 2024 14:30:14 +0200 Subject: [PATCH] Handle NOT_BUILT and ABORTED as other results Use case -------- My use case is a post build script triggering a job which does not need to fully complete. Since the build step can be cancelled while waiting or building, it causes the triggering build to be marked as a failure despite asking to never block. To fix that, I need the plugin to pass the result of the build step through BlockingBehaviour. That lets one define how to behave when the triggered build is NOT_BUILT (it got cancelled from the queue). In my case I need it to never block. See: https://phabricator.wikimedia.org/T352319 Solution -------- There are multiple reasons for a job to not fully complete: - it is interrupted, an InterruptedException is thrown, this is still rethrown and Jenkins will mark the build as ABORTED. - it can be cancelled from the build queue raising a CancellationException. This previously raised an AbortException which Jenkins handles by marking the build as a failure. I have changed it to a NOT_BUILT result which can be process as other results (addressing my use case to have it to never block). The Jenkins Result class ranks the results as: - SUCCESS - UNSTABLE - FAILURE - NOT_BUILT - ABORTED. The NOT_BUILT and ABORTED results are thus worse than a FAILURE and would be matched as such in BlockingBehavior mapBuildStepResult() and mapBuildResult() which both use isWorseOrEqualTo() for comparison. Add a test testCancelledFromBuildQueue() to cover the CancellationException() is caught and it results in a SUCCESS (since the test blocking behavior is to never block). The ResultConditionTest test covers that BlockingBehavior is able to map NOT_BUILD and ABORTED since it has two tests explicitly cancelling and interrupting jobs. Examples -------- When a build is ongoing and when aborting it: Waiting for the completion of downstream-project downstream-project #7 started. downstream-project #7 completed. Result was ABORTED Build step 'Trigger/call builds on other projects' marked build as failure Finished: FAILURE When it is waiting in the build queue and get cancelled: Waiting for the completion of downstream-project Not built: downstream-project has been cancelled while waiting in the queue. Build step 'Trigger/call builds on other projects' marked build as failure Finished: FAILURE --- .../parameterizedtrigger/TriggerBuilder.java | 64 ++++++++++--------- .../test/TriggerBuilderTest.java | 33 ++++++++++ 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java b/src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java index 55bd1c35..fd5c179b 100644 --- a/src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java +++ b/src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java @@ -150,55 +150,59 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen continue; } for (QueueTaskFuture future : futures.get(p)) { - try { - if (future == null) { - listener.getLogger() - .println("Skipping " + ModelHyperlinkNote.encodeTo(p) - + ". The project was not triggered for some reason."); - continue; - } - + if (future == null) { listener.getLogger() - .println("Waiting for the completion of " - + HyperlinkNote.encodeTo('/' + p.getUrl(), p.getFullDisplayName())); - Run startedRun; - try { - startedRun = future.waitForStart(); - } catch (InterruptedException x) { - listener.getLogger() - .println("Build aborting: cancelling queued project " - + HyperlinkNote.encodeTo('/' + p.getUrl(), p.getFullDisplayName())); - future.cancel(true); - throw x; // rethrow so that the triggering project get flagged as cancelled - } + .println("Skipping " + ModelHyperlinkNote.encodeTo(p) + + ". The project was not triggered for some reason."); + continue; + } + listener.getLogger() + .println("Waiting for the completion of " + + HyperlinkNote.encodeTo('/' + p.getUrl(), p.getFullDisplayName())); + Run startedRun; + Result completedResult; + try { + startedRun = future.waitForStart(); listener.getLogger() .println(HyperlinkNote.encodeTo( '/' + startedRun.getUrl(), startedRun.getFullDisplayName()) + " started."); Run completedRun = future.get(); - Result completedResult = completedRun.getResult(); + completedResult = completedRun.getResult(); listener.getLogger() .println(HyperlinkNote.encodeTo( '/' + completedRun.getUrl(), completedRun.getFullDisplayName()) + " completed. Result was " + completedResult); + BuildInfoExporterAction.addBuildInfoExporterAction( build, completedRun.getParent().getFullName(), completedRun.getNumber(), completedResult); - if (buildStepResult && config.getBlock().mapBuildStepResult(completedResult)) { - Result r = config.getBlock().mapBuildResult(completedResult); - if (r != null) { // The blocking job is not a success - build.setResult(r); - } - } else { - buildStepResult = false; - } + } catch (InterruptedException x) { + listener.getLogger() + .println("Build aborting: cancelling queued project " + + HyperlinkNote.encodeTo('/' + p.getUrl(), p.getFullDisplayName())); + future.cancel(true); + throw x; // rethrow so that the triggering project get flagged as cancelled } catch (CancellationException x) { - throw new AbortException(p.getFullDisplayName() + " aborted."); + listener.getLogger() + .println("Not built: " + p.getFullDisplayName() + + " has been cancelled while waiting in the queue."); + completedResult = Result.NOT_BUILT; + } + + if (buildStepResult && config.getBlock().mapBuildStepResult(completedResult)) { + + Result r = config.getBlock().mapBuildResult(completedResult); + if (r != null) { // The blocking job is not a success + build.setResult(r); + } + } else { + buildStepResult = false; } } } diff --git a/src/test/java/hudson/plugins/parameterizedtrigger/test/TriggerBuilderTest.java b/src/test/java/hudson/plugins/parameterizedtrigger/test/TriggerBuilderTest.java index 733349eb..78aeaed1 100644 --- a/src/test/java/hudson/plugins/parameterizedtrigger/test/TriggerBuilderTest.java +++ b/src/test/java/hudson/plugins/parameterizedtrigger/test/TriggerBuilderTest.java @@ -253,6 +253,39 @@ public void testCancelsDownstreamBuildWhenInterrupted() throws Exception { assertEquals("No build left in queue", 0, r.jenkins.getQueue().countBuildableItems()); } + @Test + public void testCancelledFromBuildQueue() throws Exception { + r.jenkins.setNumExecutors(1); // the downstream-project would be in the build queue + + FreeStyleProject triggerProject = r.createFreeStyleProject("upstream-project"); + FreeStyleProject downstreamProject = r.createFreeStyleProject("downstream-project"); + + TriggerBuilder triggerBuilder = new TriggerBuilder(createTriggerConfig("downstream-project")); + + triggerProject.getBuildersList().add(triggerBuilder); + QueueTaskFuture parentBuild = triggerProject.scheduleBuild2(0); + + parentBuild.waitForStart(); + Thread.sleep(500); + + assertEquals( + "Downstream project is in build queue", 1, r.jenkins.getQueue().countBuildableItems()); + + // Cancel the queued build + r.jenkins.getQueue().clear(); + parentBuild.get(); + + assertLines( + triggerProject.getLastBuild(), + "Waiting for the completion of downstream-project", + "Not built: downstream-project has been cancelled while waiting in the queue.", + // The test class configures the BlockingBehaviour to never + // fail and that includes cancelled job. + "Finished: SUCCESS"); + assertNull("No downstream build has been run", downstreamProject.getLastBuild()); + assertEquals("No build left in queue", 0, r.jenkins.getQueue().countBuildableItems()); + } + @Test public void testConsoleOutputWithCounterParameters() throws Exception { r.createFreeStyleProject("project1");