Skip to content

Instantly share code, notes, and snippets.

@weirded weirded/Cmb.scala
Created Oct 21, 2015

Embed
What would you like to do?
Sumo Logic Change Management Board sumobot plugin
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)
}
}
}
@jordanwalsh23

This comment has been minimized.

Copy link

jordanwalsh23 commented Feb 12, 2016

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.