Created
October 21, 2015 17:42
-
-
Save weirded/572619a0d3f522a03e5e to your computer and use it in GitHub Desktop.
Sumo Logic Change Management Board sumobot plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.sumologic.bender.plugins.cmb | |
import java.text.DateFormat | |
import java.util.{Date, GregorianCalendar} | |
import akka.actor.{ActorLogging, Props} | |
import com.atlassian.jira.rest.client.api.domain.Issue | |
import com.atlassian.jira.rest.client.api.{IssueRestClient, RestClientException} | |
import com.sumologic.bender.plugins.jira.{BlockingJiraUserMapping, JiraClient, JiraUserMapping} | |
import com.sumologic.sumobot.core.model.{IncomingMessage, OutgoingMessage, PublicChannel} | |
import com.sumologic.sumobot.plugins.BotPlugin | |
import org.codehaus.jettison.json.{JSONArray, JSONObject} | |
import slack.models.User | |
import scala.collection.JavaConverters._ | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import scala.concurrent.Future | |
import scala.concurrent.duration._ | |
import scala.util.{Failure, Success} | |
object Cmb { | |
case object ScheduledReminder | |
case object CheckForApprovals | |
val Proposal = BotPlugin.matchText(".*(propose|proposing).*(SUMO\\-\\d+).*") | |
val PendingScrs = BotPlugin.matchText("pending scrs") | |
val ExtractionRegex = "(?i)(SUMO\\-\\d+)".r | |
val PlusOne = BotPlugin.matchText(".*\\+1.*") | |
val ScheduledDate = ".*(\\d{4})-(\\d{2})-(\\d{2}).*".r | |
val StatusToRemaining = Map( | |
"Waiting for CMB approval" -> 3, | |
"CMB First Approval" -> 2, | |
"CMB Second Approval" -> 1 | |
) | |
val AllPendingRequestsJQL = "filter = \"All Open Jiras\" " + | |
"AND type = \"System Change\" " + | |
s"AND status in (${StatusToRemaining.keys.map(k => "\"" + k + "\"").mkString(", ")}) " + | |
"ORDER BY key" | |
} | |
class Cmb | |
extends BotPlugin | |
with ActorLogging { | |
override protected def help: String = | |
""" | |
|There are a few things I can help you with on the CMB: | |
| | |
|pending scrs - shows pending change requests. | |
|proposing SUMO-nnnn - shows a summary of the proposed SCR. | |
""".stripMargin | |
import Cmb._ | |
private val CmbChannel = "cmb-public" | |
private val ListenChannels = Set(CmbChannel, "slack_test") | |
// Determined by running CmbTest in a debugger and eyeballing results. | |
// There's probably a more elegant way to do it ;) | |
private val ProposedStatusId = 10016 | |
private val ApprovedStatusId = 10018 | |
private val timeFormat = DateFormat.getDateTimeInstance | |
private val restClient = JiraClient.createRestClient(config) | |
private val jiraUserMapping = context.system.actorOf(Props(classOf[JiraUserMapping], restClient)) | |
private val baseUrl = JiraClient.baseUrl(config) | |
private val userMapping = new BlockingJiraUserMapping(jiraUserMapping) | |
private var previouslyOpenRequests: Set[String] = Set.empty | |
readProposedIssues(AllPendingRequestsJQL).onComplete { | |
case Success(issues) => | |
previouslyOpenRequests = issues.map(_.key).toSet | |
case Failure(cause) => | |
log.error(cause, "Could not load issues from Jira.") | |
} | |
override protected def pluginPreStart(): Unit = { | |
scheduleActorMessage(self.path.name + "-reminder", "0 0 8,12,20 ? * MON-FRI", ScheduledReminder) | |
scheduleActorMessage(self.path.name + "-check-approvals", "0 * * ? * *", CheckForApprovals) | |
} | |
override protected def pluginReceive: Receive = { | |
case ScheduledReminder => | |
log.info("Checking for pending SCRs (scheduled reminder)") | |
readProposedIssues(AllPendingRequestsJQL).onComplete { | |
case Success(issues) => | |
if (issues.nonEmpty) { | |
val text = formatForListing(issues) | |
publicChannel(CmbChannel).foreach { | |
channel => | |
sendMessage(OutgoingMessage(channel, text)) | |
} | |
} | |
case Failure(cause) => | |
log.error(cause, "Could not query Jira for pending SCRs.") | |
} | |
case CheckForApprovals => | |
readProposedIssues(AllPendingRequestsJQL).onComplete { | |
case Failure(cause) => | |
log.error(cause, "Could not query Jira for pending SCRs.") | |
case Success(issues) => | |
log.info("Checking for approved SCRs") | |
val pendingNowKeys = issues.map(_.key).toSet | |
val removedKeys = previouslyOpenRequests.diff(pendingNowKeys) | |
previouslyOpenRequests = pendingNowKeys | |
val approvedIssues = removedKeys.toSeq.par.flatMap { | |
key => | |
val issue = restClient.getIssueClient.getIssue(key).claim() | |
if (issue.getStatus.getId == ApprovedStatusId) { | |
Some(issue) | |
} else { | |
None | |
} | |
}.seq | |
def renderApprovedIssue(issue: Issue): String = { | |
userMapping.jiraToSlack(issue.getReporter) match { | |
case Some(slackUser) => | |
s"${issue.getKey} proposed by ${mention(slackUser)}" | |
case None => | |
issue.getKey | |
} | |
} | |
if (approvedIssues.nonEmpty) { | |
val approvedList = approvedIssues.toSeq.sortBy(_.getKey).map(renderApprovedIssue).mkString(", ") | |
publicChannel(CmbChannel).foreach { | |
channel => | |
sendMessage(OutgoingMessage(channel, s"Now approved: $approvedList")) | |
} | |
} | |
} | |
} | |
override protected def receiveIncomingMessage: ReceiveIncomingMessage = { | |
case msg@IncomingMessage(PendingScrs(), _, PublicChannel(_, channelName), _) if ListenChannels.contains(channelName) => | |
readProposedIssues(AllPendingRequestsJQL).onComplete { | |
case Success(issues) => | |
if (issues.nonEmpty) { | |
msg.say(formatForListing(issues)) | |
} else { | |
msg.say("There are no pending SCRs") | |
} | |
case Failure(cause) => | |
log.error(cause, "Could not load pending SCRs.") | |
msg.say("Could not load pending SCRs. Possibly Jira is unreachable.") | |
} | |
case msg@IncomingMessage(Proposal(_, issueKey), _, PublicChannel(_, channelName), _) if ListenChannels.contains(channelName) => | |
msg.respondInFuture { | |
message => | |
val issuesIds = for (issueId <- ExtractionRegex findAllMatchIn msg.canonicalText) | |
yield issueId group 1 | |
try { | |
val allIssues = issuesIds.map { | |
key => | |
val proposedIssue = restClient.getIssueClient.getIssue(key.trim).claim() | |
formatForProposal(message.sentByUser, proposedIssue) | |
}.mkString("\n") | |
message.message(allIssues) | |
} catch { | |
case rce: RestClientException => | |
message.message(s"Could not find issue $issueKey") | |
} | |
} | |
case msg@IncomingMessage(PlusOne(), _, PublicChannel(_, channelName), _) if ListenChannels.contains(channelName) => | |
context.system.scheduler.scheduleOnce(30.seconds, self, CheckForApprovals) | |
} | |
case class ProposedIssue(key: String, summary: String, createdBy: String, proposedBy: String, proposalTime: String, remainingVotes: Int) | |
private def formatForProposal(proposer: User, proposal: Issue): String = { | |
def highlight(risk: String): String = { | |
risk.toLowerCase match { | |
case "high" => s":exclamation: $risk :exclamation:" | |
case "medium" => s":warning: $risk" | |
case _ => risk | |
} | |
} | |
def warningForDate(date: String): String = { | |
date match { | |
case ScheduledDate(year, month, day) => | |
val cal = new GregorianCalendar() | |
cal.set(year.toInt, month.toInt - 1, day.toInt, 23, 59) | |
val scheduled = cal.getTime.getTime | |
println(s"scheduled: $scheduled, now: ${new Date().getTime}") | |
if (scheduled < new Date().getTime) { | |
":warning: " | |
} else { | |
"" | |
} | |
case _ => | |
"" | |
} | |
} | |
val risk = Option(proposal.getFieldByName("Risk")). | |
filter(_.getValue != null). | |
map(_.getValue.asInstanceOf[JSONObject].get("value").toString). | |
filter(_.toLowerCase != "low"). | |
map(r => s"\n*Risk:* ${highlight(r)}"). | |
getOrElse("") | |
val scheduleDate = Option(proposal.getFieldByName("Schedule Date")).map(_.getValue.toString).getOrElse("missing field") | |
val scheduleReq = Option(proposal.getFieldByName("Schedule requirements")) | |
.map(_.getValue.asInstanceOf[JSONObject].get("value")).getOrElse("missing field") | |
val depJson = Option(proposal.getFieldByName("Required For Deployments")).map(_.getValue.asInstanceOf[JSONArray]).get | |
val depl = (0 to depJson.length() - 1).map(i => depJson.get(i).asInstanceOf[JSONObject]).map(_.get("value").toString) | |
val deployments = if (depl.isEmpty) { | |
":warning: No deployment specified." | |
} else { | |
depl.map(_.toLowerCase.trim).mkString(", ") | |
} | |
val inProposedStatus = StatusToRemaining.contains(proposal.getStatus.getName) | |
val proposalWarning = if (!inProposedStatus) s"\n:x: Not in proposed status! Please update the Jira, ${mention(proposer)} :x:" else "" | |
s"Details on ${proposal.getKey}:\n\n" + | |
s"*Deployments:* $deployments\n" + | |
s"*Summary:* ${proposal.getSummary}\n" + | |
s"*Schedule requirements:* $scheduleReq\n" + | |
s"*Schedule date:* ${warningForDate(scheduleDate)}$scheduleDate" + | |
s"$risk$proposalWarning" | |
} | |
private def formatForListing(issues: Seq[ProposedIssue]): String = { | |
s"${issues.size} pending change requests:\n\n" + | |
issues.map { | |
proposal => | |
val reporter = if (proposal.createdBy != proposal.proposedBy) { | |
s", reported by ${proposal.createdBy}" | |
} else { | |
"" | |
} | |
s"${baseUrl}browse/${proposal.key}\n" + | |
s"*${proposal.summary}*\n" + | |
s"_*needs ${proposal.remainingVotes} approvals*, proposed by ${proposal.proposedBy} on ${proposal.proposalTime}${reporter}_\n" | |
}.mkString("\n") | |
} | |
private def readProposedIssues(jql: String): Future[Seq[ProposedIssue]] = { | |
Future { | |
restClient.getSearchClient.searchJql(jql).get().getIssues.asScala.toSeq.par.map { | |
issue => | |
val expandos: java.lang.Iterable[IssueRestClient.Expandos] = List(IssueRestClient.Expandos.CHANGELOG).toIterable.asJava | |
val detailedIssue: Issue = restClient.getIssueClient.getIssue(issue.getKey, expandos).claim() | |
val proposalDetails = detailedIssue.getChangelog.asScala.find { | |
group => | |
group.getItems.asScala.exists { | |
item => | |
item.getField == "status" && item.getTo == ProposedStatusId.toString | |
} | |
} | |
proposalDetails match { | |
case Some(change) => | |
ProposedIssue(issue.getKey, | |
issue.getSummary, | |
issue.getReporter.getName, | |
change.getAuthor.getName, | |
timeFormat.format(change.getCreated.toDate), | |
StatusToRemaining(issue.getStatus.getName)) | |
case None => | |
ProposedIssue(issue.getKey, | |
issue.getSummary, | |
issue.getReporter.getName, | |
"unknown", | |
"unknown time", | |
StatusToRemaining(issue.getStatus.getName)) | |
} | |
}.seq.sortBy(_.proposalTime) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any chance you could open source the
com.sumologic.bender.plugins.jira
in your sumobot repo?This seems to be the only thing missing before I can get this working.
Thanks!