package jenkins.plugins.mattermost; import hudson.EnvVars; import hudson.Util; import hudson.model.*; import hudson.scm.ChangeLogSet; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.LogTaskListener; import org.apache.commons.lang.StringUtils; import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Logger; import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; @SuppressWarnings("rawtypes") public class ActiveNotifier implements FineGrainedNotifier { private static final Logger logger = Logger.getLogger(MattermostListener.class.getName()); MattermostNotifier notifier; BuildListener listener; private final TokenExpander tokenExpander; public ActiveNotifier(MattermostNotifier notifier, BuildListener listener, TokenExpander tokenExpander) { super(); this.notifier = notifier; this.listener = listener; this.tokenExpander = tokenExpander; } private MattermostService getMattermost(AbstractBuild r) { return notifier.newMattermostService(r, listener); } public void deleted(AbstractBuild r) { } public void started(AbstractBuild build) { // AbstractProject<?, ?> project = build.getProject(); CauseAction causeAction = build.getAction(CauseAction.class); if (causeAction != null) { //TODO CHANGED //Cause scmCause = causeAction.findCause(SCMTrigger.SCMTriggerCause.class); List<Cause> scmCauses = causeAction.getCauses(); if (scmCauses.size() == 0) { MessageBuilder message = new MessageBuilder(notifier, build, this.tokenExpander); message.append(causeAction.getShortDescription()); message.appendOpenLink(); if (notifier.getIncludeCustomMessage()) { message.appendCustomMessage(build.getResult()); } notifyStart(build, message.toString()); // Cause was found, exit early to prevent double-message return; } } String changes = getChanges(build, notifier.getIncludeCustomAttachmentMessage()); if (changes != null) { notifyStart(build, changes); } else { notifyStart( build, getBuildStatusMessage(build, false, notifier.getIncludeCustomAttachmentMessage())); } } private void notifyStart(AbstractBuild build, String attachmentMessage) { AbstractProject<?, ?> project = (build != null) ? build.getProject() : null; AbstractBuild<?, ?> previousBuild = (project != null && project.getLastBuild() != null) ? project.getLastBuild().getPreviousCompletedBuild() : null; String expandedCustomMessage = getExpandedCustomMessage(build); if (previousBuild == null) { getMattermost(build).publish(attachmentMessage, expandedCustomMessage, "good"); } else { getMattermost(build) .publish(attachmentMessage, expandedCustomMessage, getBuildColor(previousBuild)); } } public void finalized(AbstractBuild r) { } public void completed(AbstractBuild r) { AbstractProject<?, ?> project = r.getProject(); Result result = r.getResult(); AbstractBuild<?, ?> previousBuild = project.getLastBuild(); if (previousBuild != null) { do { previousBuild = previousBuild.getPreviousCompletedBuild(); } while (previousBuild != null && previousBuild.getResult() == Result.ABORTED); } Result previousResult = (previousBuild != null) ? previousBuild.getResult() : Result.SUCCESS; if ((result == Result.ABORTED && notifier.getNotifyAborted()) || (result == Result.FAILURE // notify only on // single failed // build && previousResult != Result.FAILURE && notifier.getNotifyFailure()) || (result == Result.FAILURE // notify only on repeated failures && previousResult == Result.FAILURE && notifier.getNotifyRepeatedFailure()) || (result == Result.NOT_BUILT && notifier.getNotifyNotBuilt()) || (result == Result.SUCCESS && (previousResult == Result.FAILURE || previousResult == Result.UNSTABLE) && notifier.getNotifyBackToNormal()) || (result == Result.SUCCESS && notifier.getNotifySuccess()) || (result == Result.UNSTABLE && notifier.getNotifyUnstable())) { String expandedCustomMessage = getExpandedCustomMessage(r); getMattermost(r) .publish( getBuildStatusMessage( r, notifier.getIncludeTestSummary(), notifier.getIncludeCustomAttachmentMessage()), expandedCustomMessage, getBuildColor(r)); if (notifier.getCommitInfoChoice().showAnything()) { getMattermost(r).publish(getCommitList(r), expandedCustomMessage, getBuildColor(r)); } } } String getChanges(AbstractBuild r, boolean includeCustomAttachmentMessage) { if (!r.hasChangeSetComputed()) { logger.info("No change set computed..."); return null; } ChangeLogSet changeSet = r.getChangeSet(); List<ChangeLogSet.Entry> entries = new LinkedList<>(); Set<ChangeLogSet.AffectedFile> files = new HashSet<ChangeLogSet.AffectedFile>(); for (Object o : changeSet.getItems()) { ChangeLogSet.Entry entry = (ChangeLogSet.Entry) o; logger.info("Entry " + o); entries.add(entry); files.addAll(entry.getAffectedFiles()); } if (entries.isEmpty()) { logger.info("Empty change..."); return null; } Set<String> authors = new HashSet<String>(); for (ChangeLogSet.Entry entry : entries) { authors.add(entry.getAuthor().getDisplayName()); } MessageBuilder message = new MessageBuilder(notifier, r, this.tokenExpander); message.append(":pray: Started by changes from "); message.append(StringUtils.join(authors, ", ")); message.append(" ("); message.append(files.size()); message.append(" file(s) changed)"); message.appendOpenLink(); if (includeCustomAttachmentMessage) { message.appendCustomAttachmentMessage(); } return message.toString(); } String getCommitList(AbstractBuild r) { ChangeLogSet changeSet = r.getChangeSet(); List<ChangeLogSet.Entry> entries = new LinkedList<ChangeLogSet.Entry>(); for (Object o : changeSet.getItems()) { ChangeLogSet.Entry entry = (ChangeLogSet.Entry) o; logger.info("Entry " + o); entries.add(entry); } if (entries.isEmpty()) { logger.info("Empty change..."); Cause.UpstreamCause c = (Cause.UpstreamCause) r.getCause(Cause.UpstreamCause.class); if (c == null) { return "No Changes."; } String upProjectName = c.getUpstreamProject(); int buildNumber = c.getUpstreamBuild(); AbstractProject project = Hudson.getInstance().getItemByFullName(upProjectName, AbstractProject.class); if (project == null) { return "No upstream project."; } AbstractBuild upBuild = (AbstractBuild) project.getBuildByNumber(buildNumber); return getCommitList(upBuild); } Set<String> commits = new HashSet<String>(); for (ChangeLogSet.Entry entry : entries) { StringBuffer commit = new StringBuffer(); CommitInfoChoice commitInfoChoice = notifier.getCommitInfoChoice(); if (commitInfoChoice.showTitle()) { commit.append(entry.getMsg()); } if (commitInfoChoice.showAuthor()) { commit.append(" [").append(entry.getAuthor().getDisplayName()).append("]"); } commits.add(commit.toString()); } MessageBuilder message = new MessageBuilder(notifier, r, this.tokenExpander); message.append("Changes:\n- "); message.append(StringUtils.join(commits, "\n- ")); return message.toString(); } static String getBuildColor(AbstractBuild r) { Result result = r.getResult(); if (result == Result.SUCCESS) { return "good"; } else if (result == Result.FAILURE) { return "danger"; } else { return "warning"; } } String getBuildStatusMessage( AbstractBuild r, boolean includeTestSummary, boolean includeCustomAttachmentMessage) { MessageBuilder message = new MessageBuilder(notifier, r, this.tokenExpander); message.appendStatusMessage(); message.appendDuration(); message.appendOpenLink(); if (includeTestSummary) { message.appendTestSummary(); } if (includeCustomAttachmentMessage) { message.appendCustomAttachmentMessage(); } return message.toString(); } String getExpandedCustomMessage(AbstractBuild build) { String result = ""; if (notifier.getIncludeCustomMessage()) { String customMessage = notifier.getCustomMessage(); EnvVars envVars = new EnvVars(); try { envVars = build.getEnvironment(new LogTaskListener(logger, INFO)); } catch (IOException | InterruptedException e) { logger.log(SEVERE, e.getMessage(), e); } result = envVars.expand(customMessage); } return result; } public static class MessageBuilder { private static final String STARTING_STATUS_MESSAGE = ":pray: Starting...", BACK_TO_NORMAL_STATUS_MESSAGE = ":white_check_mark: Back to normal", STILL_FAILING_STATUS_MESSAGE = ":no_entry_sign: Still Failing", SUCCESS_STATUS_MESSAGE = ":white_check_mark: Success", FAILURE_STATUS_MESSAGE = ":no_entry_sign: Failure", ABORTED_STATUS_MESSAGE = ":warning: Aborted", NOT_BUILT_STATUS_MESSAGE = ":warning: Not built", UNSTABLE_STATUS_MESSAGE = ":warning: Unstable", UNKNOWN_STATUS_MESSAGE = ":question: Unknown"; private StringBuffer message; private MattermostNotifier notifier; private AbstractBuild build; private final TokenExpander tokenExpander; public MessageBuilder(MattermostNotifier notifier, AbstractBuild build, TokenExpander tokenExpander) { this.notifier = notifier; this.message = new StringBuffer(); this.build = build; this.tokenExpander = tokenExpander; startMessage(); } public MessageBuilder appendStatusMessage() { message.append(this.escape(getStatusMessage(build))); return this; } static String getStatusMessage(AbstractBuild r) { if (r.isBuilding()) { return STARTING_STATUS_MESSAGE; } Result result = r.getResult(); Result previousResult; Run lastBuild = r.getProject().getLastBuild(); Run previousBuild = (lastBuild != null) ? lastBuild.getPreviousBuild() : null; Run previousSuccessfulBuild = r.getPreviousSuccessfulBuild(); boolean buildHasSucceededBefore = previousSuccessfulBuild != null; /* * If the last build was aborted, go back to find the last non-aborted build. * This is so that aborted builds do not affect build transitions. I.e. if build * 1 was failure, build 2 was aborted and build 3 was a success the transition * should be failure -> success (and therefore back to normal) not aborted -> * success. */ Run lastNonAbortedBuild = previousBuild; while (lastNonAbortedBuild != null && lastNonAbortedBuild.getResult() == Result.ABORTED) { lastNonAbortedBuild = lastNonAbortedBuild.getPreviousBuild(); } /* * If all previous builds have been aborted, then use SUCCESS as a default * status so an aborted message is sent */ if (lastNonAbortedBuild == null) { previousResult = Result.SUCCESS; } else { previousResult = lastNonAbortedBuild.getResult(); } /* * Back to normal should only be shown if the build has actually succeeded at * some point. Also, if a build was previously unstable and has now succeeded * the status should be "Back to normal" */ if (result == Result.SUCCESS && (previousResult == Result.FAILURE || previousResult == Result.UNSTABLE) && buildHasSucceededBefore) { return BACK_TO_NORMAL_STATUS_MESSAGE; } if (result == Result.FAILURE && previousResult == Result.FAILURE) { return STILL_FAILING_STATUS_MESSAGE; } if (result == Result.SUCCESS) { return SUCCESS_STATUS_MESSAGE; } if (result == Result.FAILURE) { return FAILURE_STATUS_MESSAGE; } if (result == Result.ABORTED) { return ABORTED_STATUS_MESSAGE; } if (result == Result.NOT_BUILT) { return NOT_BUILT_STATUS_MESSAGE; } if (result == Result.UNSTABLE) { return UNSTABLE_STATUS_MESSAGE; } return UNKNOWN_STATUS_MESSAGE; } public MessageBuilder append(String string) { message.append(this.escape(string)); return this; } public MessageBuilder append(Object string) { message.append(this.escape(string.toString())); return this; } private MessageBuilder startMessage() { message.append(this.escapeDisplayName(build.getProject().getFullDisplayName())); message.append(" - "); message.append(this.escapeDisplayName(build.getDisplayName())); message.append(" "); return this; } public MessageBuilder appendOpenLink() { String url = notifier.getBuildServerUrl() + build.getUrl(); message.append(" [Open](").append(url).append(")"); return this; } public MessageBuilder appendDuration() { message.append(" after "); String durationString; if (message.toString().contains(BACK_TO_NORMAL_STATUS_MESSAGE)) { durationString = createBackToNormalDurationString(); } else { durationString = build.getDurationString(); } message.append(durationString); return this; } public MessageBuilder appendTestSummary() { AbstractTestResultAction<?> action = this.build.getAction(AbstractTestResultAction.class); if (action != null) { int total = action.getTotalCount(); int failed = action.getFailCount(); int skipped = action.getSkipCount(); message.append("\nTest Status:\n"); message.append("\tPassed: " + (total - failed - skipped)); message.append(", Failed: " + failed); message.append(", Skipped: " + skipped); } else { message.append("\nNo Tests found."); } return this; } public MessageBuilder appendCustomMessage(Result buildResult) { String replaced = tokenExpander.expand(notifier.getCustomMessage(), build); message.append("\n"); message.append(replaced); return this; } public MessageBuilder appendCustomAttachmentMessage() { String customAttachmentMessage = notifier.getCustomAttachmentMessage(); EnvVars envVars = new EnvVars(); try { envVars = build.getEnvironment(new LogTaskListener(logger, INFO)); } catch (IOException e) { logger.log(SEVERE, e.getMessage(), e); } catch (InterruptedException e) { logger.log(SEVERE, e.getMessage(), e); } message.append("\n"); String replaced = tokenExpander.expand(envVars.expand(customAttachmentMessage), build); message.append(replaced); message.append("\n"); return this; } private String createBackToNormalDurationString() { Run previousSuccessfulBuild = build.getPreviousSuccessfulBuild(); if (previousSuccessfulBuild == null) { return "unknown"; } // long previousSuccessStartTime = previousSuccessfulBuild.getStartTimeInMillis(); // long previousSuccessDuration = previousSuccessfulBuild.getDuration(); // long previousSuccessEndTime = previousSuccessStartTime + previousSuccessDuration; // long buildStartTime = build.getStartTimeInMillis(); // long buildDuration = build.getDuration(); // long buildEndTime = buildStartTime + buildDuration; // long backToNormalDuration = buildEndTime - previousSuccessEndTime; //TODO CHANGED long currentBuildStartTime = build.getTimeInMillis(); long lastSuccessBuildStartTime = previousSuccessfulBuild.getTimeInMillis(); long diff = currentBuildStartTime - lastSuccessBuildStartTime; return Util.getTimeSpanString(diff); } public String escape(String string) { string = string.replace("&", "&"); string = string.replace("<", "<"); string = string.replace(">", ">"); return string; } public String escapeDisplayName(String displayName) { // escape HTML displayName = escape(displayName); // escape mattermost markdown which _may_ occur in job display names displayName = displayName.replace("~", "\\~"); displayName = displayName.replace("*", "\\*"); displayName = displayName.replace("_", "\\_"); displayName = displayName.replace("`", "\\`"); return displayName; } public String toString() { return message.toString(); } } }