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, that does get 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 Oct 18, 2024
1 parent f60b7e3 commit ec4350c
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,58 +150,61 @@ 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) {
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.");
.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);
.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);
completedResult = Result.ABORTED;
} 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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,44 @@ public void testCancelsDownstreamBuildWhenInterrupted() throws Exception {
triggerProject.getLastBuild(),
"Waiting for the completion of downstream-project",
"Build aborting: cancelling queued project downstream-project",
"Build was aborted",
"Finished: ABORTED");
// 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 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());
}
Expand Down

0 comments on commit ec4350c

Please sign in to comment.