From a95cb34ef00de439568ce662fbdc083624b15247 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 which does not need to fully complete. The step can be cancelled while waiting and causes the triggering build to fail. As a result I need the build step result to be passed through the BlockingBehavior so that I can set it to never block and thus be considered a SUCCESS even when NOT_BUILT or ABORTED. See: https://phabricator.wikimedia.org/T352319 Solution -------- When a triggered job is aborted (InterruptedException), the build step was throwing an AbortException which marks the parent job has having been aborted. This change catches it as an ABORTED result and passes it through the BlockingBehavior to determine the build step outcome. Jenkins Result defines ordinal ranking 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 uses isWorseOrEqualTo() for comparison. BREAKING CHANGE: since ABORTED has a worse ordinal than FAILURE, the aborted build step causes the build to now be marked FAILURE. This is reflected in testCancelsDownstreamBuildWhenInterrupted() test which now becomes a SUCCESS (since the test blocking behavior is to never block). When a job is cancelled from the build queue (CancellationException), catch it, set the result to NOT_BUILD and pass it through the BlockingBehavior. This lets one to configure the build step to never fail or mark the build unstable when previously the exception would bubble up and call the build to fail. 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). ResultConditionTest covers the BlockingBehavior is able to map NOT_BUILD and ABORTED since it has two tests explicitly cancelling and interrupting jobs. Examples -------- When a build is aborted, by 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 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");