Skip to content

Instantly share code, notes, and snippets.

@weirded
Created October 21, 2015 17:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save weirded/572619a0d3f522a03e5e to your computer and use it in GitHub Desktop.
Save weirded/572619a0d3f522a03e5e to your computer and use it in GitHub Desktop.
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
Copy link

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