package jenkins.plugins.zulip; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.scm.ChangeLogSet; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.test.AbstractTestResultAction; import jenkins.tasks.SimpleBuildStep; import org.apache.commons.lang.exception.ExceptionUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; /** * Sends build result notification to stream based on the configuration */ public class ZulipNotifier extends Publisher implements SimpleBuildStep { private static final Logger logger = Logger.getLogger(ZulipNotifier.class.getName()); private String stream; private String topic; private String smartNotification; @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @DataBoundConstructor public ZulipNotifier() { } public String getStream() { return stream; } @DataBoundSetter public void setStream(String stream) { this.stream = stream; } public String getTopic() { return topic; } @DataBoundSetter public void setTopic(String topic) { this.topic = topic; } public String getSmartNotification() { return smartNotification; } @DataBoundSetter public void setSmartNotification(String smartNotification) { this.smartNotification = smartNotification; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { return publish(build, listener); } @Override public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { publish(run, listener); } private boolean publish(@Nonnull Run<?, ?> build, @Nonnull TaskListener listener) throws InterruptedException { if (shouldPublish(build)) { String configuredTopic = ZulipUtil.getDefaultValue(topic, DESCRIPTOR.getTopic()); Result result = getBuildResult(build); String changeString = ""; try { changeString = getChangeSet(build); } catch (Exception e) { logger.log(Level.WARNING, "Exception while computing changes since last build:\n" + ExceptionUtils.getStackTrace(e)); changeString += "\nError determining changes since last build - please contact [email protected]."; } String resultString = result.toString(); String message = ""; // If we are sending to fixed topic, we will want to add project name into the message if (ZulipUtil.isValueSet(configuredTopic)) { message += hundsonUrlMesssage("Project: ", build.getParent().getDisplayName(), build.getParent().getUrl(), DESCRIPTOR) + " : "; } message += hundsonUrlMesssage("Build: ", build.getDisplayName(), build.getUrl(), DESCRIPTOR); message += ": "; message += "**" + resultString + "**"; if (result == Result.SUCCESS) { message += " :check_mark:"; } else if (result == Result.UNSTABLE) { message += " :warning:"; AbstractTestResultAction testResultAction = build.getAction(AbstractTestResultAction.class); String failCount = testResultAction != null ? Integer.toString(testResultAction.getFailCount()) : "?"; message += " (" + failCount + " broken tests)"; } else { message += " :x:"; } if (changeString.length() > 0) { message += "\n\n"; message += changeString; } String destinationStream = ZulipUtil.expandVariables(build, listener, ZulipUtil.getDefaultValue(stream, DESCRIPTOR.getStream())); String destinationTopic = ZulipUtil.expandVariables(build, listener, ZulipUtil.getDefaultValue(configuredTopic, build.getParent().getDisplayName())); Zulip zulip = new Zulip(DESCRIPTOR.getUrl(), DESCRIPTOR.getEmail(), DESCRIPTOR.getApiKey()); zulip.sendStreamMessage(destinationStream, destinationTopic, message); } return true; } private String getChangeSet(Run<?, ?> build) { StringBuilder changeString = new StringBuilder(); RunChangeSetWrapper wrapper = new RunChangeSetWrapper(build); if (!wrapper.hasChangeSetComputed()) { changeString.append("Could not determine changes since last build."); } else if (wrapper.hasChangeSet()) { // If there seems to be a commit message at all, try to list all the changes. changeString.append("Changes since last build:\n"); for (ChangeLogSet<? extends ChangeLogSet.Entry> changeLogSet : wrapper.getChangeSets()) { for (ChangeLogSet.Entry e : changeLogSet) { String commitMsg = e.getMsg().trim(); if (commitMsg.length() > 47) { commitMsg = commitMsg.substring(0, 46) + "..."; } String author = e.getAuthor().getDisplayName(); changeString.append("\n* `").append(author).append("` ").append(commitMsg); } } } return changeString.toString(); } /** * Tests if the build should actually published<br/> * If SmartNotify is enabled, only notify if: * <ol> * <li>There was no previous build</li> * <li>The current build did not succeed</li> * <li>The previous build failed and the current build succeeded.</li> * </ol> * * @return true if build should be published */ private boolean shouldPublish(Run<?, ?> build) { if (SmartNotification.isSmartNotifyEnabled(smartNotification, DESCRIPTOR.isSmartNotify())) { Run<?, ?> previousBuild = build.getPreviousBuild(); if (previousBuild == null || getBuildResult(build) != Result.SUCCESS || getBuildResult(previousBuild) != Result.SUCCESS) { return true; } } else { return true; } return false; } /** * Helper method to format build parameter as link to Jenkins with prefix * * @param prefix The prefix to add to the parameter (will be formatted as bold) * @param display The build parameter * @param url The Url to the Jenkins item * @param globalConfig Global step config * @return Formatted parameter */ private static String hundsonUrlMesssage(String prefix, String display, String url, DescriptorImpl globalConfig) { String message = display; String jenkinsUrl = ZulipUtil.getJenkinsUrl(globalConfig); if (ZulipUtil.isValueSet(jenkinsUrl)) { message = "[" + message + "](" + jenkinsUrl + url + ")"; } message = "**" + prefix + "**" + message; return message; } /** * Helper method to get build result from {@link Run}<br/> * <i>Since scripted pipeline have no post build concept, the Result variable of successful builds will<br/> * not be set yet. In that case we simply assume the build is a success</i> * * @param build The run to get build result from * @return The build result */ private static Result getBuildResult(Run<?, ?> build) { return build.getResult() != null ? build.getResult() : Result.SUCCESS; } }