Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/twbs/rorschach.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Rebert <code@rebertia.com>2014-07-02 11:49:28 +0400
committerChris Rebert <code@rebertia.com>2014-07-02 11:49:28 +0400
commit8f9e91f089a2bcc42cb885051ab69f03907baab8 (patch)
tree02e1460844374e0d5b824bbdb0235367f665456b
parent86402bd72bc370545614294c08576ce5708d864e (diff)
progress
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/auditing/BaseAndHeadBranchesAuditor.scala24
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/auditing/ModifiedFilesAuditor.scala54
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/github/CommitFileStatus.scala28
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/github/CommitSha.scala5
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/github/GitHubActorWithLogging.scala12
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/github/GitHubIssueCommenter.scala40
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/github/PullRequestFeedback.scala9
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/server/Boot.scala8
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/server/PullRequestEventHandler.scala61
-rw-r--r--src/main/scala/com/getbootstrap/rorschach/server/RorschachActor.scala49
10 files changed, 285 insertions, 5 deletions
diff --git a/src/main/scala/com/getbootstrap/rorschach/auditing/BaseAndHeadBranchesAuditor.scala b/src/main/scala/com/getbootstrap/rorschach/auditing/BaseAndHeadBranchesAuditor.scala
new file mode 100644
index 0000000..0122490
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/auditing/BaseAndHeadBranchesAuditor.scala
@@ -0,0 +1,24 @@
+package com.getbootstrap.rorschach.auditing
+
+object BaseAndHeadBranchesAuditor {
+ def audit(baseBranch: String, headBranch: String): Seq[String] = {
+ Seq(
+ auditThatNotAgainstGhPages(baseBranch),
+ auditThatNotMergingGhPagesIntoMaster(baseBranch = baseBranch, headBranch = headBranch)
+ ).flatten
+ }
+
+ def auditThatNotAgainstGhPages(baseBranch: String): Option[String] = {
+ baseBranch match {
+ case "gh-pages" => Some("Normal pull requests should never be against the gh-pages branch.")
+ case _ => None
+ }
+ }
+
+ def auditThatNotMergingGhPagesIntoMaster(baseBranch: String, headBranch: String): Option[String] = {
+ (headBranch, baseBranch) match {
+ case ("gh-pages", "master") => Some("Normally, gh-pages should not be merged into master.")
+ case _ => None
+ }
+ }
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/auditing/ModifiedFilesAuditor.scala b/src/main/scala/com/getbootstrap/rorschach/auditing/ModifiedFilesAuditor.scala
new file mode 100644
index 0000000..03dc9fd
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/auditing/ModifiedFilesAuditor.scala
@@ -0,0 +1,54 @@
+package com.getbootstrap.rorschach.auditing
+
+object ModifiedFilesAuditor {
+ private implicit class PathString(filepath: String) {
+ def isDist: Boolean = filepath.startsWith("dist/")
+ def isNonMinifiedCss: Boolean = filepath.endsWith(".css") && !filepath.endsWith(".min.css")
+ def isSourceLess: Boolean = filepath.startsWith("less/") && filepath.endsWith(".less")
+ def isNonMinifiedJs: Boolean = filepath.endsWith(".js") && !filepath.endsWith(".min.js")
+ def isDistCss: Boolean = filepath.isDist && filepath.isNonMinifiedCss
+ def isDistJs: Boolean = filepath.isDist && filepath.isNonMinifiedJs
+ def isSourceJs: Boolean = filepath.startsWith("js/") && filepath.isNonMinifiedJs
+ }
+
+ def audit(filepaths: Set[String]): Seq[String] = {
+ Seq(auditCname(filepaths), auditCss(filepaths), auditJs(filepaths)).flatten
+ }
+
+ /**
+ * If dist/bootstrap.css etc. is modified, then less/<*>.less must also have been modified.
+ */
+ private def auditCss(filepaths: Set[String]): Option[String] = {
+ val cssModified = filepaths.exists{ _.isDistCss }
+ val lessModified = filepaths.exists{ _.isSourceLess }
+ if (cssModified && !lessModified) {
+ Some("Changes must be made to the original Less source code, not just the compiled CSS.")
+ }
+ else {
+ None
+ }
+ }
+
+ /**
+ * If dist/js/bootstrap.js etc. is modified, then js/<*>.js must also have been modified.
+ */
+ private def auditJs(filepaths: Set[String]): Option[String] = {
+ val distJsModified = filepaths.exists{ _.isDistJs }
+ val sourceJsModified = filepaths.exists{ _.isSourceJs }
+ if (distJsModified && !sourceJsModified) {
+ Some("Changes must be made to the original JS source code, not just the generated concatenated JS.")
+ }
+ else {
+ None
+ }
+ }
+
+ private def auditCname(filepaths: Set[String]): Option[String] = {
+ if (filepaths.contains("CNAME")) {
+ Some("The CNAME file should never be modified (except in extremely unlikely circumstances).")
+ }
+ else {
+ None
+ }
+ }
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/github/CommitFileStatus.scala b/src/main/scala/com/getbootstrap/rorschach/github/CommitFileStatus.scala
new file mode 100644
index 0000000..d108a2d
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/github/CommitFileStatus.scala
@@ -0,0 +1,28 @@
+package com.getbootstrap.rorschach.github
+
+sealed trait CommitFileStatus {
+ val StatusString: String
+}
+object Added extends CommitFileStatus {
+ override val StatusString = "added"
+}
+object Modified extends CommitFileStatus {
+ override val StatusString = "modified"
+}
+object Deleted extends CommitFileStatus {
+ override val StatusString = "deleted"
+}
+object Unknown extends CommitFileStatus {
+ override val StatusString = "UNKNOWN"
+}
+
+object CommitFileStatus {
+ def apply(status: String): CommitFileStatus = {
+ status match {
+ case Added.StatusString => Added
+ case Modified.StatusString => Modified
+ case Deleted.StatusString => Deleted
+ case _ => Unknown
+ }
+ }
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/github/CommitSha.scala b/src/main/scala/com/getbootstrap/rorschach/github/CommitSha.scala
new file mode 100644
index 0000000..d55002f
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/github/CommitSha.scala
@@ -0,0 +1,5 @@
+package com.getbootstrap.rorschach.github
+
+class CommitSha(val sha: String) extends AnyVal {
+ override def toString = s"CommitSha(${sha})"
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/github/GitHubActorWithLogging.scala b/src/main/scala/com/getbootstrap/rorschach/github/GitHubActorWithLogging.scala
new file mode 100644
index 0000000..d2ab6f6
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/github/GitHubActorWithLogging.scala
@@ -0,0 +1,12 @@
+package com.getbootstrap.rorschach.github
+
+import org.eclipse.egit.github.core.RepositoryId
+import org.eclipse.egit.github.core.client.GitHubClient
+import com.getbootstrap.rorschach.server.{Settings, ActorWithLogging}
+
+abstract class GitHubActorWithLogging extends ActorWithLogging {
+ private val settings = Settings(context.system)
+ protected val BootstrapRepoId = new RepositoryId("twbs", "bootstrap")
+ protected val gitHubClient = new GitHubClient()
+ gitHubClient.setCredentials(settings.BotUsername, settings.BotPassword)
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/github/GitHubIssueCommenter.scala b/src/main/scala/com/getbootstrap/rorschach/github/GitHubIssueCommenter.scala
new file mode 100644
index 0000000..7e99154
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/github/GitHubIssueCommenter.scala
@@ -0,0 +1,40 @@
+package com.getbootstrap.rorschach.github
+
+import scala.util.{Try,Failure,Success}
+import org.eclipse.egit.github.core.client.GitHubClient
+import org.eclipse.egit.github.core.service.IssueService
+import org.eclipse.egit.github.core.RepositoryId
+import com.getbootstrap.rorschach.server.{ActorWithLogging, Settings}
+
+
+class GitHubIssueCommenter extends GitHubActorWithLogging {
+ // val settings = Settings(context.system)
+
+ private def tryToCommentOn(repo: RepositoryId, issue: IssueNumber, commentMarkdown: String) = {
+ val issueService = new IssueService(gitHubClient)
+ Try { issueService.createComment(repo, issue.number, commentMarkdown) }
+ }
+
+ override def receive = {
+ case PullRequestFeedback(prNum, requester, messages) => {
+ val username = requester.getLogin
+ val commentMarkdown = s"""
+ |Hi @${username}!
+ |
+ |Thanks for wanting to contribute to Bootstrap by sending this pull request.
+ |Unfortunately, your pull request seems to have some problems:
+ |${messagesMarkdown}
+ |
+ |You'll need to **fix these mistakes** and revise your pull request before we can proceed further.
+ |Thanks!
+ |
+ |(*Please note that this is a fully automated comment.*)
+ """.stripMargin
+
+ tryToCommentOn(BootstrapRepoId, prNum, commentMarkdown) match {
+ case Success(comment) => log.info(s"Successfully posted comment ${comment.getUrl} for ${prNum}")
+ case Failure(exc) => log.error(exc, s"Error posting comment for ${prNum}")
+ }
+ }
+ }
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/github/PullRequestFeedback.scala b/src/main/scala/com/getbootstrap/rorschach/github/PullRequestFeedback.scala
new file mode 100644
index 0000000..a5dc57e
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/github/PullRequestFeedback.scala
@@ -0,0 +1,9 @@
+package com.getbootstrap.rorschach.github
+
+import org.eclipse.egit.github.core.User
+
+case class PullRequestFeedback(
+ prNum: IssueNumber,
+ requester: User,
+ messages: Seq[String]
+)
diff --git a/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala b/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala
index 5c77670..36923f8 100644
--- a/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala
+++ b/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala
@@ -31,11 +31,9 @@ object Boot extends App {
implicit val system = ActorSystem("on-spray-can")
// import actorSystem.dispatcher
- val commenter = system.actorOf(Props(classOf[GitHubIssueCommenter]))
- val localValidator = system.actorOf(Props(classOf[ValidatorSingletonActor], commenter), "validator-service")
- val exampleFetcherPool = system.actorOf(SmallestMailboxPool(5).props(Props(classOf[LiveExampleFetcher], localValidator)), "example-fetcher-pool")
- val issueCommentEventHandler = system.actorOf(Props(classOf[IssueCommentEventHandler], exampleFetcherPool), "issue-comment-event-handler")
- val webService = system.actorOf(Props(classOf[LmvtfyActor], issueCommentEventHandler), "lmvtfy-service")
+ val commenter = system.actorOf(SmallestMailboxPool(3).props(Props(classOf[GitHubIssueCommenter])), "gh-pr-commenter")
+ val prAuditorPool = system.actorOf(SmallestMailboxPool(5).props(Props(classOf[PullRequestEventHandler], commenter)), "pr-auditor-pool")
+ val webService = system.actorOf(Props(classOf[RorschachActor], prAuditorPool), "rorschach-service")
implicit val timeout = Timeout(15.seconds)
IO(Http) ? Http.Bind(webService, interface = "0.0.0.0", port = port)
diff --git a/src/main/scala/com/getbootstrap/rorschach/server/PullRequestEventHandler.scala b/src/main/scala/com/getbootstrap/rorschach/server/PullRequestEventHandler.scala
new file mode 100644
index 0000000..0b908bb
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/server/PullRequestEventHandler.scala
@@ -0,0 +1,61 @@
+package com.getbootstrap.rorschach.server
+
+import com.getbootstrap.rorschach.auditing.{BaseAndHeadBranchesAuditor, ModifiedFilesAuditor}
+
+import scala.collection.JavaConverters._
+import scala.util.{Try,Success,Failure}
+import akka.actor.ActorRef
+import com.getbootstrap.rorschach.github._
+import org.eclipse.egit.github.core.service.CommitService
+import org.eclipse.egit.github.core._
+
+class PullRequestEventHandler(commenter: ActorRef) extends GitHubActorWithLogging {
+ implicit class RichRepository(repo: Repository) {
+ def repositoryId: RepositoryId = new RepositoryId(repo.getOwner.getLogin, repo.getName)
+ }
+ implicit class RichPullRequestMarker(marker: PullRequestMarker) {
+ def commitSha: CommitSha = new CommitSha(marker.getSha)
+ }
+ implicit class RichCommitFile(file: CommitFile) {
+ def status: CommitFileStatus = CommitFileStatus(file.getStatus)
+ }
+
+ private def modifiedFilesFor(repoId: RepositoryId, base: CommitSha, head: CommitSha) = {
+ val commitService = new CommitService(gitHubClient)
+ Try { commitService.compare(repoId, base.sha, head.sha) }.map { comparison =>
+ val affectedFiles = comparison.getFiles.asScala
+ affectedFiles.filter{ _.status == Modified }.map{ _.getFilename }.toSet[String]
+ }
+ }
+
+ override def receive = {
+ case pr: PullRequest => {
+ val bsBase = pr.getBase
+ val prHead = pr.getHead
+ bsBase.getRepo.repositoryId match {
+ case BootstrapRepoId => {
+ val base = bsBase.commitSha
+ val head = prHead.commitSha
+ val foreignRepoId = prHead.getRepo.repositoryId
+
+ val fileMessages = modifiedFilesFor(foreignRepoId, base, head) match {
+ case Failure(exc) => {
+ log.error(exc, s"Could not get modified files for commits ${base}...${head} for ${foreignRepoId}")
+ Nil
+ }
+ case Success(modifiedFiles) => {
+ ModifiedFilesAuditor.audit(modifiedFiles)
+ }
+ }
+ val branchMessages = BaseAndHeadBranchesAuditor.audit(baseBranch = bsBase.getRef, headBranch = prHead.getRef)
+
+ val allMessages = fileMessages ++ branchMessages
+ if (allMessages.nonEmpty) {
+ commenter ! PullRequestFeedback(pr.getNumber, pr.getUser, allMessages)
+ }
+ }
+ case otherRepo => log.error(s"Received event from GitHub about irrelevant repository: ${otherRepo}")
+ }
+ }
+ }
+}
diff --git a/src/main/scala/com/getbootstrap/rorschach/server/RorschachActor.scala b/src/main/scala/com/getbootstrap/rorschach/server/RorschachActor.scala
new file mode 100644
index 0000000..7dfd09e
--- /dev/null
+++ b/src/main/scala/com/getbootstrap/rorschach/server/RorschachActor.scala
@@ -0,0 +1,49 @@
+package com.getbootstrap.rorschach.server
+
+import akka.actor.ActorRef
+import spray.routing._
+import spray.http._
+
+
+class RorschachActor(protected val pullRequestEventHandler: ActorRef) extends ActorWithLogging with HttpService {
+ import GitHubPullRequestWebHooksDirectives.authenticatedPullRequestEvent
+
+ val settings = Settings(context.system)
+ override def actorRefFactory = context
+ override def receive = runRoute(theOnlyRoute)
+
+ val theOnlyRoute =
+ pathPrefix("rorschach") { pathEndOrSingleSlash {
+ get {
+ complete(StatusCodes.OK, "Hi! Rorschach is online.")
+ } ~
+ post {
+ headerValueByName("X-Github-Event") { githubEvent =>
+ githubEvent match {
+ case "ping" => {
+ log.info("Successfully received GitHub webhook ping.")
+ complete(StatusCodes.OK)
+ }
+ case "pull_request" => {
+ authenticatedPullRequestEvent(settings.WebHookSecretKey.toArray) { event =>
+ event.getAction match {
+ case "opened" | "synchronize" | "reopened" => {
+ var pr = event.getPullRequest
+ if (pr.getState == "open") {
+ pullRequestEventHandler ! pr
+ complete(StatusCodes.OK)
+ }
+ else {
+ complete(StatusCodes.OK, s"Ignoring event about closed pull request #${pr.getId}")
+ }
+ }
+ case _ => complete(StatusCodes.OK, "Ignoring irrelevant action")
+ }
+ }
+ }
+ case _ => complete(StatusCodes.BadRequest, "Unexpected event type")
+ }
+ }
+ }
+ }}
+}