Skip to content

Commit

Permalink
Handle NOT_BUILT and ABORTED as other results
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hashar committed Nov 4, 2024
1 parent 458bf27 commit a95cb34
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,55 +150,59 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
continue;
}
for (QueueTaskFuture<AbstractBuild> 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) {

Check warning on line 153 in src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 153 is only partially covered, one branch is missing
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;

Check warning on line 157 in src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 154-157 are not covered by tests
}

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)) {

Check warning on line 198 in src/main/java/hudson/plugins/parameterizedtrigger/TriggerBuilder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 198 is only partially covered, 2 branches are missing

Result r = config.getBlock().mapBuildResult(completedResult);
if (r != null) { // The blocking job is not a success
build.setResult(r);
}
} else {
buildStepResult = false;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FreeStyleBuild> 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");
Expand Down

0 comments on commit a95cb34

Please sign in to comment.