// Copyright (c) 2015 Uber Technologies, Inc.
//
// 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 com.uber.jenkins.phabricator;
import com.uber.jenkins.phabricator.conduit.ConduitAPIClient;
import com.uber.jenkins.phabricator.conduit.ConduitAPIException;
import com.uber.jenkins.phabricator.conduit.Differential;
import com.uber.jenkins.phabricator.conduit.DifferentialClient;
import com.uber.jenkins.phabricator.coverage.CodeCoverageMetrics;
import com.uber.jenkins.phabricator.coverage.CoverageProvider;
import com.uber.jenkins.phabricator.credentials.ConduitCredentials;
import com.uber.jenkins.phabricator.provider.InstanceProvider;
import com.uber.jenkins.phabricator.tasks.NonDifferentialBuildTask;
import com.uber.jenkins.phabricator.tasks.NonDifferentialHarbormasterTask;
import com.uber.jenkins.phabricator.tasks.Task;
import com.uber.jenkins.phabricator.uberalls.UberallsClient;
import com.uber.jenkins.phabricator.unit.UnitTestProvider;
import com.uber.jenkins.phabricator.utils.CommonUtils;
import com.uber.jenkins.phabricator.utils.Logger;
import hudson.EnvVars;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Job;
import hudson.model.Result;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import jenkins.model.CauseOfInterruption;
import jenkins.model.InterruptedBuildAction;
import jenkins.model.Jenkins;
import org.apache.commons.io.FilenameUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class PhabricatorNotifier extends Notifier {
public static final String COBERTURA_CLASS_NAME = "com.uber.jenkins.phabricator.coverage.CoberturaCoverageProvider";
private static final String JUNIT_PLUGIN_NAME = "junit";
private static final String JUNIT_CLASS_NAME = "com.uber.jenkins.phabricator.unit.JUnitTestProvider";
private static final String COBERTURA_PLUGIN_NAME = "cobertura";
private static final String ABORT_TAG = "abort";
private static final String UBERALLS_TAG = "uberalls";
private static final String CONDUIT_TAG = "conduit";
// Post a comment on success. Useful for lengthy builds.
private final boolean commentOnSuccess;
private final boolean uberallsEnabled;
private final boolean coverageCheck;
private final boolean commentWithConsoleLinkOnFailure;
private final boolean preserveFormatting;
private final String commentFile;
private final String commentSize;
private final boolean customComment;
private final boolean processLint;
private final String lintFile;
private final String lintFileSize;
private final double coverageThreshold;
private UberallsClient uberallsClient;
// Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
@DataBoundConstructor
public PhabricatorNotifier(boolean commentOnSuccess, boolean uberallsEnabled, boolean coverageCheck,
double coverageThreshold,
boolean preserveFormatting, String commentFile, String commentSize,
boolean commentWithConsoleLinkOnFailure, boolean customComment, boolean processLint,
String lintFile, String lintFileSize) {
this.commentOnSuccess = commentOnSuccess;
this.uberallsEnabled = uberallsEnabled;
this.coverageCheck = coverageCheck;
this.commentFile = commentFile;
this.commentSize = commentSize;
this.lintFile = lintFile;
this.lintFileSize = lintFileSize;
this.preserveFormatting = preserveFormatting;
this.commentWithConsoleLinkOnFailure = commentWithConsoleLinkOnFailure;
this.customComment = customComment;
this.processLint = processLint;
this.coverageThreshold = coverageThreshold;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@Override
public final boolean perform(final AbstractBuild<?, ?> build, final Launcher launcher,
final BuildListener listener) throws InterruptedException, IOException {
EnvVars environment = build.getEnvironment(listener);
Logger logger = new Logger(listener.getLogger());
final String branch = environment.get("GIT_BRANCH");
final String gitUrl = environment.get("GIT_URL");
final UberallsClient uberallsClient = getUberallsClient(logger, gitUrl, branch);
final boolean needsDecoration = build.getActions(PhabricatorPostbuildAction.class).size() == 0;
final String diffID = environment.get(PhabricatorPlugin.DIFFERENTIAL_ID_FIELD);
final String phid = environment.get(PhabricatorPlugin.PHID_FIELD);
final boolean isDifferential = !CommonUtils.isBlank(diffID);
InterruptedBuildAction action = build.getAction(InterruptedBuildAction.class);
if (action != null) {
List<CauseOfInterruption> causes = action.getCauses();
for (CauseOfInterruption cause : causes) {
if (cause instanceof PhabricatorCauseOfInterruption) {
logger.warn(ABORT_TAG, "Skipping notification step since this build was interrupted"
+ " by a newer build with the same differential revision");
return true;
}
}
}
CoverageProvider coverageProvider;
// Handle non-differential build invocations. If PHID is present but DIFF_ID is not, it means somebody is doing
// a Harbormaster build on a commit rather than a differential, but still wants build status.
// If DIFF_ID is present but PHID is not, it means somebody is doing a Differential build without Harbormaster.
// So only skip build result processing if both are blank (e.g. master runs to update coverage data)
if (CommonUtils.isBlank(phid) && !isDifferential) {
if (needsDecoration) {
build.addAction(PhabricatorPostbuildAction.createShortText(branch, null));
}
coverageProvider = getCoverageProvider(build, listener, Collections.<String>emptySet());
CodeCoverageMetrics coverageResult = null;
if (coverageProvider != null) {
coverageResult = coverageProvider.getMetrics();
}
NonDifferentialBuildTask nonDifferentialBuildTask = new NonDifferentialBuildTask(
logger,
uberallsClient,
coverageResult,
uberallsEnabled,
environment.get("GIT_COMMIT")
);
// Ignore the result.
nonDifferentialBuildTask.run();
return true;
}
ConduitAPIClient conduitClient;
try {
conduitClient = getConduitClient(build.getParent());
} catch (ConduitAPIException e) {
e.printStackTrace(logger.getStream());
logger.warn(CONDUIT_TAG, e.getMessage());
return false;
}
final String buildUrl = environment.get("BUILD_URL");
if (!isDifferential) {
// Process harbormaster for non-differential builds
Task.Result result = new NonDifferentialHarbormasterTask(
logger,
phid,
conduitClient,
build.getResult(),
buildUrl
).run();
return result == Task.Result.SUCCESS;
}
DifferentialClient diffClient = new DifferentialClient(diffID, conduitClient);
Differential diff;
try {
diff = new Differential(diffClient.fetchDiff());
} catch (ConduitAPIException e) {
e.printStackTrace(logger.getStream());
logger.warn(CONDUIT_TAG, "Unable to fetch differential from Conduit API");
logger.warn(CONDUIT_TAG, e.getMessage());
return true;
}
if (needsDecoration) {
diff.decorate(build, this.getPhabricatorURL(build.getParent()));
}
Set<String> includeFileNames = new HashSet<String>();
for (String file : diff.getChangedFiles()) {
includeFileNames.add(FilenameUtils.getName(file));
}
coverageProvider = getCoverageProvider(build, listener, includeFileNames);
CodeCoverageMetrics coverageResult = null;
if (coverageProvider != null) {
coverageResult = coverageProvider.getMetrics();
}
BuildResultProcessor resultProcessor = new BuildResultProcessor(
logger,
build,
diff,
diffClient,
environment.get(PhabricatorPlugin.PHID_FIELD),
coverageResult,
buildUrl,
preserveFormatting,
coverageThreshold
);
if (uberallsEnabled) {
boolean passBuildOnUberalls = resultProcessor.processParentCoverage(uberallsClient);
if (!passBuildOnUberalls && coverageCheck) {
build.setResult(Result.FAILURE);
}
}
// Add in comments about the build result
resultProcessor.processBuildResult(commentOnSuccess, commentWithConsoleLinkOnFailure);
// Process unit tests results to send to Harbormaster
resultProcessor.processUnitResults(getUnitProvider(build, listener));
// Read coverage data to send to Harbormaster
resultProcessor.processCoverage(coverageProvider);
if (processLint) {
// Read lint results to send to Harbormaster
resultProcessor.processLintResults(lintFile, lintFileSize);
}
// Fail the build if we can't report to Harbormaster
if (!resultProcessor.processHarbormaster()) {
return false;
}
resultProcessor.processRemoteComment(commentFile, commentSize);
resultProcessor.sendComment(commentWithConsoleLinkOnFailure);
return true;
}
protected UberallsClient getUberallsClient(Logger logger, String gitUrl, String branch) {
if (uberallsClient != null) {
return uberallsClient;
}
setUberallsClient(new UberallsClient(
getDescriptor().getUberallsURL(),
logger,
gitUrl,
branch
));
return uberallsClient;
}
// Just for testing
protected void setUberallsClient(UberallsClient client) {
uberallsClient = client;
}
private ConduitAPIClient getConduitClient(Job owner) throws ConduitAPIException {
ConduitCredentials credentials = getConduitCredentials(owner);
if (credentials == null) {
throw new ConduitAPIException("No credentials configured for conduit");
}
return new ConduitAPIClient(credentials.getGateway(), credentials.getToken().getPlainText());
}
/**
* Get the cobertura coverage for the build
*
* @param build The current build
* @param listener The build listener
* @return The current cobertura coverage, if any
*/
private CoverageProvider getCoverageProvider(AbstractBuild build, BuildListener listener,
Set<String> includeFileNames) {
if (!build.getResult().isBetterOrEqualTo(Result.UNSTABLE)) {
return null;
}
Logger logger = new Logger(listener.getLogger());
InstanceProvider<CoverageProvider> provider = new InstanceProvider<CoverageProvider>(
Jenkins.getInstance(),
COBERTURA_PLUGIN_NAME,
COBERTURA_CLASS_NAME,
logger
);
CoverageProvider coverage = provider.getInstance();
if (coverage == null) {
return null;
}
coverage.setBuild(build);
coverage.setIncludeFileNames(includeFileNames);
if (coverage.hasCoverage()) {
return coverage;
} else {
logger.info(UBERALLS_TAG, "No cobertura results found");
return null;
}
}
private UnitTestProvider getUnitProvider(AbstractBuild build, BuildListener listener) {
Logger logger = new Logger(listener.getLogger());
InstanceProvider<UnitTestProvider> provider = new InstanceProvider<UnitTestProvider>(
Jenkins.getInstance(),
JUNIT_PLUGIN_NAME,
JUNIT_CLASS_NAME,
logger
);
UnitTestProvider unitProvider = provider.getInstance();
if (unitProvider == null) {
return null;
}
unitProvider.setBuild(build);
return unitProvider;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isCommentOnSuccess() {
return commentOnSuccess;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isCustomComment() {
return customComment;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isUberallsEnabled() {
return uberallsEnabled;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isCoverageCheck() {
return coverageCheck;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isCommentWithConsoleLinkOnFailure() {
return commentWithConsoleLinkOnFailure;
}
@SuppressWarnings("UnusedDeclaration")
public String getCommentFile() {
return commentFile;
}
@SuppressWarnings("UnusedDeclaration")
public String getCommentSize() {
return commentSize;
}
@SuppressWarnings("UnusedDeclaration")
public double getCoverageThreshold() {
return coverageThreshold;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isPreserveFormatting() {
return preserveFormatting;
}
@SuppressWarnings("UnusedDeclaration")
public boolean isProcessLint() {
return processLint;
}
@SuppressWarnings("UnusedDeclaration")
public String getLintFile() {
return lintFile;
}
@SuppressWarnings("UnusedDeclaration")
public String getLintFileSize() {
return lintFileSize;
}
private ConduitCredentials getConduitCredentials(Job owner) {
return getDescriptor().getCredentials(owner);
}
private String getPhabricatorURL(Job owner) {
ConduitCredentials credentials = getDescriptor().getCredentials(owner);
if (credentials != null) {
return credentials.getUrl();
}
return null;
}
// Overridden for better type safety.
@Override
public PhabricatorNotifierDescriptor getDescriptor() {
return (PhabricatorNotifierDescriptor) super.getDescriptor();
}
}