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>