You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sj...@apache.org on 2026/04/22 16:37:49 UTC

(maven-invoker-plugin) branch master updated: Implement simply retry mechanism for failed build jobs (#723)

This is an automated email from the ASF dual-hosted git repository.

slawekjaranowski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-invoker-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 3d2112e  Implement simply retry mechanism for failed build jobs (#723)
3d2112e is described below

commit 3d2112ebfa0f5dc87a0fcd3e0a9c15def05581a8
Author: Slawomir Jaranowski <s....@gmail.com>
AuthorDate: Wed Apr 22 18:37:43 2026 +0200

    Implement simply retry mechanism for failed build jobs (#723)
    
    * Implement simply retry mechanism for failed build jobs
    
    What changed:
    - Added `rerunFailingTestsCount` parameter to specify retry attempts for failed jobs.
    - Refactored job execution to include retry logic for both setup and non-setup jobs.
    
    TODO:
     - reporting flaky tests
---
 src/it/rerun-build/pom.xml                         | 62 ++++++++++++++++++
 src/it/rerun-build/src/it/project/pom.xml          | 33 ++++++++++
 src/it/rerun-build/src/it/project/postbuild.groovy | 26 ++++++++
 src/it/rerun-build/src/it/setup/pom.xml            | 33 ++++++++++
 src/it/rerun-build/src/it/setup/postbuild.groovy   | 27 ++++++++
 src/it/rerun-build/verify.groovy                   | 44 +++++++++++++
 .../maven/plugins/invoker/AbstractInvokerMojo.java | 74 ++++++++++++++++++----
 .../apache/maven/plugins/invoker/FileLogger.java   | 12 +---
 .../{FileLogger.java => FileLoggerAppender.java}   | 26 ++++----
 src/main/mdo/invocation.mdo                        |  7 ++
 10 files changed, 307 insertions(+), 37 deletions(-)

diff --git a/src/it/rerun-build/pom.xml b/src/it/rerun-build/pom.xml
new file mode 100644
index 0000000..47a921e
--- /dev/null
+++ b/src/it/rerun-build/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugins.invoker</groupId>
+  <artifactId>fail-build</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <description>Test to check reruning failed jobs</description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-invoker-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <rerunFailingTestsCount>1</rerunFailingTestsCount>
+          <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
+          <goals>
+            <goal>validate</goal>
+          </goals>
+        </configuration>
+        <executions>
+          <execution>
+            <id>integration-test</id>
+            <phase>initialize</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/src/it/rerun-build/src/it/project/pom.xml b/src/it/rerun-build/src/it/project/pom.xml
new file mode 100644
index 0000000..a787ea9
--- /dev/null
+++ b/src/it/rerun-build/src/it/project/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>test</groupId>
+  <artifactId>rerun-build</artifactId>
+  <version>0.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+</project>
diff --git a/src/it/rerun-build/src/it/project/postbuild.groovy b/src/it/rerun-build/src/it/project/postbuild.groovy
new file mode 100644
index 0000000..82978ca
--- /dev/null
+++ b/src/it/rerun-build/src/it/project/postbuild.groovy
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+def touchFile = new File(basedir, 'touch.txt')
+if (!touchFile.exists()) {
+    touchFile.text = 'touch'
+    return false
+}
+
+true
diff --git a/src/it/rerun-build/src/it/setup/pom.xml b/src/it/rerun-build/src/it/setup/pom.xml
new file mode 100644
index 0000000..76e5a3f
--- /dev/null
+++ b/src/it/rerun-build/src/it/setup/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>test</groupId>
+  <artifactId>rerun-setup</artifactId>
+  <version>0.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+</project>
diff --git a/src/it/rerun-build/src/it/setup/postbuild.groovy b/src/it/rerun-build/src/it/setup/postbuild.groovy
new file mode 100644
index 0000000..4fa6d01
--- /dev/null
+++ b/src/it/rerun-build/src/it/setup/postbuild.groovy
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+def touchFile = new File(basedir, 'touch.txt')
+if (!touchFile.exists()) {
+    touchFile.text = 'touch'
+    println 'fail on first run'
+    return false
+}
+
+true
diff --git a/src/it/rerun-build/verify.groovy b/src/it/rerun-build/verify.groovy
new file mode 100644
index 0000000..a165880
--- /dev/null
+++ b/src/it/rerun-build/verify.groovy
@@ -0,0 +1,44 @@
+import java.nio.file.Files
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+assert new File(basedir, 'target/it/setup/touch.txt' ).exists()
+assert new File(basedir, 'target/it/project/touch.txt' ).exists()
+
+def buildLog = new File(basedir, 'build.log').text
+
+def fs = File.separator
+
+assert buildLog.count("setup${fs}pom.xml .................................... FAILED") == 1
+assert buildLog.count("setup${fs}pom.xml .................................... SUCCESS") == 1
+
+assert buildLog.count("project${fs}pom.xml .................................. FAILED") == 1
+assert buildLog.count("project${fs}pom.xml .................................. SUCCESS") == 1
+
+def setupBuildLog = new File(basedir, 'target/it/setup/build.log').text
+
+// The project should be built twice, once for the first run and once for the rerun.
+assert setupBuildLog.count("[INFO] BUILD SUCCESS") == 2
+
+def projectBuildLog = new File(basedir, 'target/it/project/build.log').text
+
+// The project should be built twice, once for the first run and once for the rerun.
+assert projectBuildLog.count("[INFO] BUILD SUCCESS") == 2
diff --git a/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java b/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
index 285c08b..8ded034 100644
--- a/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
+++ b/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java
@@ -58,6 +58,7 @@ import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecution;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.invoker.model.BuildJob;
 import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Writer;
@@ -720,6 +721,14 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
     @Parameter
     private List<String> collectedProjects;
 
+    /**
+     * Specifies the number of times a failed execution should be retried.
+     *
+     * @since 3.10.0
+     */
+    @Parameter(defaultValue = "0", property = "invoker.rerunFailingTestsCount")
+    private int rerunFailingTestsCount;
+
     // internal state variables
 
     /**
@@ -891,7 +900,7 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
             // Jobs are ordered according to ordinal value from invoker.properties
             getLog().info("Running " + setupBuildJobs.size() + " setup job" + ((setupBuildJobs.size() < 2) ? "" : "s")
                     + ":");
-            runBuilds(projectsDir, setupBuildJobs, 1);
+            runBuildsWithRetry(projectsDir, setupBuildJobs, 1);
             getLog().info("Setup done.");
         }
 
@@ -900,7 +909,7 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
         if (setupBuildJobs.isEmpty() || setupBuildJobs.stream().allMatch(BuildJob::isNotError)) {
             // We will run the non setup jobs with the configured
             // parallelThreads number.
-            runBuilds(projectsDir, nonSetupBuildJobs, getParallelThreadsCount());
+            runBuildsWithRetry(projectsDir, nonSetupBuildJobs, getParallelThreadsCount());
         } else {
             for (BuildJob buildJob : nonSetupBuildJobs) {
                 buildJob.setResult(BuildJob.Result.SKIPPED);
@@ -913,6 +922,32 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
         processResults(new InvokerSession(buildJobs));
     }
 
+    void runBuildsWithRetry(File projectsDir, List<BuildJob> buildJobs, int runWithParallelThreads)
+            throws MojoExecutionException {
+        List<BuildJob> jobsToExecute = buildJobs;
+        int executionCount = 0;
+        do {
+            if (executionCount > 0) {
+                getLog().warn("Rerunning " + jobsToExecute.size() + " failed job"
+                        + ((jobsToExecute.size() < 2) ? "" : "s"));
+            }
+
+            executionCount++;
+
+            // set current execution count for jobs
+            for (BuildJob buildJob : jobsToExecute) {
+                buildJob.setExecutionCount(executionCount);
+            }
+
+            runBuilds(projectsDir, jobsToExecute, runWithParallelThreads);
+            jobsToExecute = getFailedJobs(jobsToExecute);
+            if (getLog().isDebugEnabled()) {
+                getLog().debug("Execution count: " + executionCount + ", failed jobs: "
+                        + jobsToExecute.stream().map(BuildJob::getProject).collect(Collectors.joining(", ", "[", "]")));
+            }
+        } while (executionCount <= rerunFailingTestsCount && !jobsToExecute.isEmpty());
+    }
+
     /**
      * We need add missing {@code .mnvn} directories for executing projects
      *
@@ -1002,6 +1037,10 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
                 .collect(Collectors.toList());
     }
 
+    private List<BuildJob> getFailedJobs(List<BuildJob> buildJobs) {
+        return buildJobs.stream().filter(buildJob -> !buildJob.isNotError()).collect(Collectors.toList());
+    }
+
     private void handleScriptRunnerWithScriptClassPath() {
         final List<String> scriptClassPath;
         if (addTestClassPath) {
@@ -1371,7 +1410,7 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
             }
 
             if (Files.isRegularFile(currentInvokerProperties)) {
-                try (InputStream in = new FileInputStream(currentInvokerProperties.toFile())) {
+                try (InputStream in = Files.newInputStream(currentInvokerProperties)) {
                     currentProperties.load(in);
                 } catch (IOException e) {
                     throw new MojoExecutionException("Failed to read invoker properties: " + currentInvokerProperties);
@@ -1624,7 +1663,7 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
                 long startTime = System.currentTimeMillis();
                 boolean executed;
 
-                FileLogger buildLogger = setupBuildLogFile(basedir);
+                FileLogger buildLogger = setupBuildLogFile(basedir, buildJob.getExecutionCount());
                 if (buildLogger != null) {
                     buildJob.setBuildlog(buildLogger.getOutputFile().getAbsolutePath());
                 }
@@ -1643,6 +1682,7 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
 
                 if (executed) {
                     buildJob.setResult(BuildJob.Result.SUCCESS);
+                    buildJob.setFailureMessage(null);
 
                     if (!suppressSummaries) {
                         getLog().info(pad(buildJob).success("SUCCESS").a(' ') + "("
@@ -1985,6 +2025,9 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
         } catch (ScriptReturnException e) {
             return false;
         } catch (ScriptException e) {
+            if (logger != null) {
+                logger.consumeLine("Error executing selector script: " + e.getMessage());
+            }
             throw new RunFailureException(BuildJob.Result.ERROR, e);
         } catch (IOException e) {
             throw new MojoExecutionException(e.getMessage(), e);
@@ -1998,6 +2041,9 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
             String hookName = invocationIndex > 0 ? preBuildHookScript + "." + invocationIndex : preBuildHookScript;
             scriptRunner.run("pre-build script", basedir, hookName, context, logger);
         } catch (ScriptException e) {
+            if (logger != null) {
+                logger.consumeLine("Error executing pre-build hook: " + e.getMessage());
+            }
             throw new RunFailureException(BuildJob.Result.FAILURE_PRE_HOOK, e);
         } catch (IOException e) {
             throw new MojoExecutionException(e.getMessage(), e);
@@ -2012,6 +2058,9 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
         } catch (IOException e) {
             throw new MojoExecutionException(e.getMessage(), e);
         } catch (ScriptException e) {
+            if (logger != null) {
+                logger.consumeLine("Error executing post-build hook: " + e.getMessage());
+            }
             throw new RunFailureException(e.getMessage(), BuildJob.Result.FAILURE_POST_HOOK, e);
         }
     }
@@ -2028,10 +2077,11 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
      * {@code build.log}.
      *
      * @param basedir The base directory of the project, must not be <code>null</code>.
+     * @param executionCount current execution count of the build job, used to determine whether to append to or create a new log file
      * @return The build logger or <code>null</code> if logging has been disabled.
      * @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created.
      */
-    private FileLogger setupBuildLogFile(File basedir) throws MojoExecutionException {
+    private FileLogger setupBuildLogFile(File basedir, int executionCount) throws MojoExecutionException {
         FileLogger logger = null;
 
         if (!noLog) {
@@ -2046,16 +2096,16 @@ public abstract class AbstractInvokerMojo extends AbstractMojo {
                         logDirectory.toPath().resolve(projectsDirectory.toPath().relativize(basedir.toPath()));
             }
 
+            Log streamLogger = streamLogs ? getLog() : null;
+            File logFile = projectLogDirectory.resolve("build.log").toFile();
             try {
-                if (streamLogs) {
-                    logger = new FileLogger(
-                            projectLogDirectory.resolve("build.log").toFile(), getLog());
+                if (executionCount > 1) {
+                    logger = new FileLoggerAppender(logFile, streamLogger);
+                    getLog().debug("Append to build log: " + logFile);
                 } else {
-                    logger = new FileLogger(
-                            projectLogDirectory.resolve("build.log").toFile());
+                    logger = new FileLogger(logFile, streamLogger);
+                    getLog().debug("New build log initialized: " + logFile);
                 }
-
-                getLog().debug("Build log initialized in: " + projectLogDirectory);
             } catch (IOException e) {
                 throw new MojoExecutionException("Error initializing build logfile in: " + projectLogDirectory, e);
             }
diff --git a/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java b/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
index 15d79a9..1a0d535 100644
--- a/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
+++ b/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
@@ -29,16 +29,6 @@ import org.apache.maven.shared.invoker.InvocationOutputHandler;
  */
 class FileLogger extends org.apache.maven.shared.scriptinterpreter.FileLogger implements InvocationOutputHandler {
 
-    /**
-     * Creates a new logger that writes to the specified file.
-     *
-     * @param outputFile The path to the output file, must not be <code>null</code>.
-     * @throws IOException If the output file could not be created.
-     */
-    FileLogger(File outputFile) throws IOException {
-        super(outputFile);
-    }
-
     /**
      * Creates a new logger that writes to the specified file and optionally mirrors messages to the given mojo logger.
      *
@@ -47,6 +37,6 @@ class FileLogger extends org.apache.maven.shared.scriptinterpreter.FileLogger im
      * @throws IOException If the output file could not be created.
      */
     FileLogger(File outputFile, final Log log) throws IOException {
-        super(outputFile, log::info);
+        super(outputFile, log != null ? log::info : null);
     }
 }
diff --git a/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java b/src/main/java/org/apache/maven/plugins/invoker/FileLoggerAppender.java
similarity index 63%
copy from src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
copy to src/main/java/org/apache/maven/plugins/invoker/FileLoggerAppender.java
index 15d79a9..db0009e 100644
--- a/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
+++ b/src/main/java/org/apache/maven/plugins/invoker/FileLoggerAppender.java
@@ -20,33 +20,31 @@ package org.apache.maven.plugins.invoker;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 
 import org.apache.maven.plugin.logging.Log;
-import org.apache.maven.shared.invoker.InvocationOutputHandler;
 
 /**
  *
  */
-class FileLogger extends org.apache.maven.shared.scriptinterpreter.FileLogger implements InvocationOutputHandler {
+class FileLoggerAppender extends FileLogger {
 
     /**
-     * Creates a new logger that writes to the specified file.
+     * Creates a new logger that appends to the specified file and optionally mirrors messages to the given mojo logger.
      *
      * @param outputFile The path to the output file, must not be <code>null</code>.
+     * @param log The mojo logger to additionally output messages to, may be <code>null</code> if not used.
      * @throws IOException If the output file could not be created.
      */
-    FileLogger(File outputFile) throws IOException {
-        super(outputFile);
+    FileLoggerAppender(File outputFile, final Log log) throws IOException {
+        super(outputFile, log);
     }
 
-    /**
-     * Creates a new logger that writes to the specified file and optionally mirrors messages to the given mojo logger.
-     *
-     * @param outputFile The path to the output file, must not be <code>null</code>.
-     * @param log The mojo logger to additionally output messages to, may be <code>null</code> if not used.
-     * @throws IOException If the output file could not be created.
-     */
-    FileLogger(File outputFile, final Log log) throws IOException {
-        super(outputFile, log::info);
+    @Override
+    protected OutputStream createOutputStream(Path outputPath) throws IOException {
+        return Files.newOutputStream(outputPath, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
     }
 }
diff --git a/src/main/mdo/invocation.mdo b/src/main/mdo/invocation.mdo
index f4af425..a9daeef 100644
--- a/src/main/mdo/invocation.mdo
+++ b/src/main/mdo/invocation.mdo
@@ -104,6 +104,13 @@ under the License.
           <type>int</type>
           <description>BuildJobs will be sorted in the descending order of the ordinal. In other words, the BuildJobs with the highest numbers will be executed first</description>
         </field>
+        <field xml.attribute="true">
+          <name>executionCount</name>
+          <version>1.0.0</version>
+          <required>false</required>
+          <type>int</type>
+          <description>The execution count of the build job</description>
+        </field>
         <field xml.attribute="true">
           <name>buildlog</name>
           <version>1.0.0</version>