CoverallsReportMojo.java
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2026 The Coveralls Maven Plugin Project Contributors:
* https://github.com/hazendaz/coveralls-maven-plugin/graphs/contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.eluder.coveralls.maven.plugin;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.eluder.coveralls.maven.plugin.domain.GitRepository;
import org.eluder.coveralls.maven.plugin.domain.Job;
import org.eluder.coveralls.maven.plugin.httpclient.CoverallsClient;
import org.eluder.coveralls.maven.plugin.httpclient.CoverallsProxyClient;
import org.eluder.coveralls.maven.plugin.json.JsonWriter;
import org.eluder.coveralls.maven.plugin.logging.CoverageTracingLogger;
import org.eluder.coveralls.maven.plugin.logging.DryRunLogger;
import org.eluder.coveralls.maven.plugin.logging.JobLogger;
import org.eluder.coveralls.maven.plugin.logging.Logger;
import org.eluder.coveralls.maven.plugin.logging.Logger.Position;
import org.eluder.coveralls.maven.plugin.service.Appveyor;
import org.eluder.coveralls.maven.plugin.service.Bamboo;
import org.eluder.coveralls.maven.plugin.service.Circle;
import org.eluder.coveralls.maven.plugin.service.General;
import org.eluder.coveralls.maven.plugin.service.GitHub;
import org.eluder.coveralls.maven.plugin.service.Jenkins;
import org.eluder.coveralls.maven.plugin.service.ServiceSetup;
import org.eluder.coveralls.maven.plugin.service.Shippable;
import org.eluder.coveralls.maven.plugin.service.Travis;
import org.eluder.coveralls.maven.plugin.service.Wercker;
import org.eluder.coveralls.maven.plugin.source.SourceCallback;
import org.eluder.coveralls.maven.plugin.source.SourceLoader;
import org.eluder.coveralls.maven.plugin.source.UniqueSourceCallback;
import org.eluder.coveralls.maven.plugin.util.CoverageParsersFactory;
import org.eluder.coveralls.maven.plugin.util.SourceLoaderFactory;
import org.eluder.coveralls.maven.plugin.util.TimestampParser;
/**
* The Class CoverallsReportMojo.
*/
@Mojo(name = "report", threadSafe = false, aggregator = true)
public class CoverallsReportMojo extends AbstractMojo {
/**
* In a Maven Multi-Module project, it's common to configure JaCoCo with a Maven project that's used for aggregating
* all JaCoCo reports into a single location. In this circumstance use the "jacocoAggregateReport" property so that
* only this location is used for coverage rather than looking for JaCoCo reports in each Maven project.
*
* @since 5.0.0
*/
@Parameter(property = "jacocoAggregateReport")
private File jacocoAggregateReport;
/**
* File paths to additional JaCoCo coverage report files.
* <p>
* By default, this plugin will look for a files in the standard JaCoCo locations. If a merged report is found in
*
* <pre>
* ${project.reporting.outputDirectory}/jacoco-merged-report/jacoco.xml
* </pre>
*
* then it will be used. Otherwise, the plugin will fallback to looking for files in both
*
* <pre>
* ${project.reporting.outputDirectory}/jacoco/jacoco.xml
* </pre>
*
* and
*
* <pre>
* ${project.reporting.outputDirectory}/jacoco-it/jacoco.xml
* </pre>
* <p>
* If the "jacocoAggregateReport" property is set then this property will be ignored
*/
@Parameter(property = "jacocoReports")
private List<File> jacocoReports;
/**
* File paths to additional Cobertura coverage report files.
* <p>
* Coveralls Maven Plugin 4.5.0 is last to work with Cobertura as java 8 required.
*
* @deprecated Cobertura is deprecated and no longer maintained. Use JaCoCo instead.
*/
@Deprecated(since = "5.0.0", forRemoval = true)
@Parameter(property = "coberturaReports")
private List<File> coberturaReports;
/**
* File paths to additional Saga coverage report files.
*/
@Parameter(property = "sagaReports")
private List<File> sagaReports;
/**
* File paths to additional Clover coverage report files.
*/
@Parameter(property = "cloverReports")
private List<File> cloverReports;
/**
* Directories for relative per module specific report files.
*/
@Parameter(property = "relativeReportDirs")
private List<String> relativeReportDirs;
/**
* File path to write and submit Coveralls data.
*/
@Parameter(property = "coverallsFile", defaultValue = "${project.build.directory}/coveralls.json")
File coverallsFile;
/**
* Url for the Coveralls API.
*/
@Parameter(property = "coverallsUrl", defaultValue = "https://coveralls.io/api/v1/jobs")
private String coverallsUrl;
/**
* Source directories.
*/
@Parameter(property = "sourceDirectories")
List<File> sourceDirectories;
/**
* Source file encoding.
*/
@Parameter(property = "sourceEncoding", defaultValue = "${project.build.sourceEncoding}")
String sourceEncoding;
/**
* CI service name.
*/
@Parameter(property = "serviceName")
String serviceName;
/**
* CI service job id.
*/
@Parameter(property = "serviceJobId")
String serviceJobId;
/**
* CI service build number.
*/
@Parameter(property = "serviceBuildNumber")
String serviceBuildNumber;
/**
* CI service build url.
*/
@Parameter(property = "serviceBuildUrl")
String serviceBuildUrl;
/**
* CI service specific environment properties.
*/
@Parameter(property = "serviceEnvironment")
Properties serviceEnvironment;
/**
* Coveralls repository token.
*/
@Parameter(property = "repoToken")
String repoToken;
/**
* Git branch name.
*/
@Parameter(property = "branch")
String branch;
/**
* GitHub pull request identifier.
*/
@Parameter(property = "pullRequest")
String pullRequest;
/**
* Coveralls parallel flag.
*/
@Parameter(property = "parallel")
private boolean parallel;
/**
* Build timestamp format. Must be in format supported by DateTimeFormatter.
*/
@Parameter(property = "timestampFormat", defaultValue = "${maven.build.timestamp.format}")
private String timestampFormat;
/**
* Build timestamp. Must be in format defined by 'timestampFormat' if it's available or in default timestamp format
* yyyy-MM-dd'T'HH:mm:ss'Z'.
*/
@Parameter(property = "timestamp", defaultValue = "${maven.build.timestamp}")
private String timestamp;
/**
* Dry run Coveralls report without actually sending it.
*/
@Parameter(property = "dryRun", defaultValue = "false")
boolean dryRun;
/**
* Fail build if Coveralls service is not available or submission fails for internal errors.
*/
@Parameter(property = "failOnServiceError", defaultValue = "true")
boolean failOnServiceError;
/**
* Scan subdirectories for source files.
*/
@Parameter(property = "scanForSources", defaultValue = "false")
private boolean scanForSources;
/**
* Base directory of the project.
*/
@Parameter(property = "coveralls.basedir", defaultValue = "${project.basedir}")
File basedir;
/**
* Skip the plugin execution.
*/
@Parameter(property = "coveralls.skip", defaultValue = "false")
boolean skip;
/**
* Maven settings.
*/
@Parameter(defaultValue = "${settings}", readonly = true, required = true)
Settings settings;
/**
* Maven project for runtime value resolution.
*/
@Parameter(defaultValue = "${project}", readonly = true)
MavenProject project;
/**
* Instantiates a new coveralls report mojo.
*/
public CoverallsReportMojo() {
// do nothing
}
@Override
public final void execute() throws MojoExecutionException, MojoFailureException {
if (this.skip) {
this.getLog().info("Skip property set, skipping plugin execution");
return;
}
try {
this.createEnvironment().setup();
final var job = this.createJob();
job.validate().throwOrInform(this.getLog());
final var sourceLoader = this.createSourceLoader(job);
final var parsers = this.createCoverageParsers(sourceLoader);
final var client = this.createCoverallsClient();
final List<Logger> reporters = new ArrayList<>();
reporters.add(new JobLogger(job));
try (var writer = this.createJsonWriter(job)) {
// For tests (its the same instance as in writer)
this.coverallsFile = writer.getCoverallsFile();
final var sourceCallback = this.createSourceCallbackChain(writer, reporters);
reporters.add(new DryRunLogger(job.isDryRun(), this.coverallsFile));
this.report(reporters, Position.BEFORE);
this.writeCoveralls(writer, sourceCallback, parsers);
this.report(reporters, Position.AFTER);
}
if (!job.isDryRun()) {
this.submitData(client, this.coverallsFile);
}
} catch (final ProcessingException e) {
throw new MojoFailureException("Processing of input or output data failed", e);
} catch (final IOException e) {
throw new MojoFailureException("I/O operation failed", e);
} catch (final Exception e) {
throw new MojoExecutionException("Build error", e);
}
}
/**
* Creates the coverage parsers.
*
* @param sourceLoader
* source loader that extracts source files
*
* @return coverage parsers for all maven modules and additional reports
*
* @throws IOException
* if parsers cannot be created
*/
protected List<CoverageParser> createCoverageParsers(final SourceLoader sourceLoader) throws IOException {
return new CoverageParsersFactory(this.project, sourceLoader).withJaCoCoReports(this.jacocoReports)
.withJacocoAggregateReport(this.jacocoAggregateReport).withCoberturaReports(this.coberturaReports)
.withSagaReports(this.sagaReports).withCloverReports(this.cloverReports)
.withRelativeReportDirs(this.relativeReportDirs).createParsers();
}
/**
* Creates the source loader.
*
* @param job
* the job describing the coveralls report
*
* @return source loader that extracts source files
*/
protected SourceLoader createSourceLoader(final Job job) {
return new SourceLoaderFactory(job.getGit().getBaseDir(), this.project, Charset.forName(this.sourceEncoding))
.withSourceDirectories(this.sourceDirectories).withScanForSources(this.scanForSources)
.createSourceLoader();
}
/**
* Creates the environment.
*
* @return environment to setup mojo and service specific properties
*/
protected Environment createEnvironment() {
return new Environment(this, this.getServices());
}
/**
* Gets the services.
*
* @return list of available continuous integration services
*/
protected List<ServiceSetup> getServices() {
final var env = System.getenv();
final List<ServiceSetup> services = new ArrayList<>();
services.add(new GitHub(env));
services.add(new Shippable(env));
services.add(new Travis(env));
services.add(new Circle(env));
services.add(new Jenkins(env));
services.add(new Bamboo(env));
services.add(new Appveyor(env));
services.add(new Wercker(env));
services.add(new General(env));
return services;
}
/**
* Creates the job.
*
* @return job that describes the coveralls report
*
* @throws ProcessingException
* if processing of timestamp fails
* @throws IOException
* if an I/O error occurs
*/
protected Job createJob() throws ProcessingException, IOException {
final var git = new GitRepository(this.basedir).load();
final var time = this.timestamp == null ? null
: new TimestampParser(this.timestampFormat).parse(this.timestamp).toEpochMilli();
// Log all non-secret items for debugging and transparency
this.getLog().info("Coveralls Job Configuration:");
this.getLog().info(" serviceName: " + this.serviceName);
this.getLog().info(" serviceJobId: " + this.serviceJobId);
this.getLog().info(" serviceBuildNumber: " + this.serviceBuildNumber);
this.getLog().info(" serviceBuildUrl: " + this.serviceBuildUrl);
this.getLog().info(" parallel: " + this.parallel);
this.getLog().info(" branch: " + this.branch);
this.getLog().info(" pullRequest: " + this.pullRequest);
this.getLog().info(" timestamp: " + time);
this.getLog().info(" basedir: " + (this.basedir != null ? this.basedir.getAbsolutePath() : "null"));
this.getLog().info(" sourceEncoding: " + this.sourceEncoding);
this.getLog().info(
" coverallsFile: " + (this.coverallsFile != null ? this.coverallsFile.getAbsolutePath() : "null"));
this.getLog().info(" coverallsUrl: " + this.coverallsUrl);
return new Job().withRepoToken(this.repoToken).withServiceName(this.serviceName)
.withServiceJobId(this.serviceJobId).withServiceBuildNumber(this.serviceBuildNumber)
.withServiceBuildUrl(this.serviceBuildUrl).withParallel(this.parallel)
.withServiceEnvironment(this.serviceEnvironment).withDryRun(this.dryRun).withBranch(this.branch)
.withPullRequest(this.pullRequest).withTimestamp(time).withGit(git);
}
/**
* Creates the json writer.
*
* @param job
* the job describing the coveralls report
*
* @return JSON writer that writes the coveralls data
*
* @throws IOException
* if an I/O error occurs
*/
protected JsonWriter createJsonWriter(final Job job) throws IOException {
return new JsonWriter(job, this.coverallsFile);
}
/**
* Creates the coveralls client.
*
* @return http client that submits the coveralls data
*/
protected CoverallsClient createCoverallsClient() {
return new CoverallsProxyClient(this.coverallsUrl, this.settings.getActiveProxy());
}
/**
* Creates the source callback chain.
*
* @param writer
* the JSON writer
* @param reporters
* the logging reporters
*
* @return source callback chain for different source handlers
*/
protected SourceCallback createSourceCallbackChain(final JsonWriter writer, final List<Logger> reporters) {
SourceCallback chain = writer;
if (this.getLog().isInfoEnabled()) {
final var coverageTracingReporter = new CoverageTracingLogger(chain);
chain = coverageTracingReporter;
reporters.add(coverageTracingReporter);
}
return new UniqueSourceCallback(chain);
}
/**
* Writes coverage data to JSON file.
*
* @param writer
* JSON writer that writes the coveralls data
* @param sourceCallback
* the source callback handler
* @param parsers
* list of coverage parsers
*
* @throws ProcessingException
* if process to to create JSON file fails
* @throws IOException
* if an I/O error occurs
*/
protected void writeCoveralls(final JsonWriter writer, final SourceCallback sourceCallback,
final List<CoverageParser> parsers) throws ProcessingException, IOException {
this.getLog().info("Writing Coveralls data to " + this.coverallsFile.getAbsolutePath() + "...");
final var now = System.currentTimeMillis();
sourceCallback.onBegin();
for (final CoverageParser parser : parsers) {
this.getLog().info("Processing coverage report from " + parser.getCoverageFile().getAbsolutePath());
parser.parse(sourceCallback);
}
sourceCallback.onComplete();
final var duration = System.currentTimeMillis() - now;
this.getLog().info("Successfully wrote Coveralls data in " + duration + "ms");
}
/**
* Submit data.
*
* @param client
* the client
* @param coverallsFile
* the coveralls file
*
* @throws ProcessingException
* the processing exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private void submitData(final CoverallsClient client, final File coverallsFile)
throws ProcessingException, IOException {
this.getLog().info("Submitting Coveralls data to API");
final var now = System.currentTimeMillis();
try {
final var response = client.submit(coverallsFile);
final var duration = System.currentTimeMillis() - now;
this.getLog()
.info("Successfully submitted Coveralls data in " + duration + "ms for " + response.getMessage());
this.getLog().info(response.getUrl());
this.getLog().info("*** Coverage results are usually available immediately on Coveralls.");
this.getLog().info(" If you see question marks or missing data, please allow some time for processing.");
} catch (final ProcessingException e) {
final var duration = System.currentTimeMillis() - now;
final var message = "Submission failed in " + duration + "ms while processing data";
this.handleSubmissionError(e, message, true);
} catch (final IOException e) {
final var duration = System.currentTimeMillis() - now;
final var message = "Submission failed in " + duration + "ms while handling I/O operations";
this.handleSubmissionError(e, message, this.failOnServiceError);
} catch (final InterruptedException e) {
final var duration = System.currentTimeMillis() - now;
final var message = "Submission failed in " + duration + "ms due to an interuption";
this.getLog().error(message, e);
Thread.currentThread().interrupt();
}
}
/**
* Handle submission error.
*
* @param <T>
* the generic type
* @param ex
* the ex
* @param message
* the message
* @param failOnException
* the fail on exception
*
* @throws T
* the t
*/
private <T extends Exception> void handleSubmissionError(final T ex, final String message,
final boolean failOnException) throws T {
if (failOnException) {
this.getLog().error(message);
throw ex;
}
this.getLog().warn(message);
}
/**
* Report.
*
* @param reporters
* the reporters
* @param position
* the position
*/
private void report(final List<Logger> reporters, final Position position) {
for (final Logger reporter : reporters) {
if (position.equals(reporter.getPosition())) {
reporter.log(this.getLog());
}
}
}
}