diff --git a/bot/src/dotty/tools/bot/Main.scala b/bot/src/dotty/tools/bot/Main.scala new file mode 100644 index 000000000000..00b4a2735f94 --- /dev/null +++ b/bot/src/dotty/tools/bot/Main.scala @@ -0,0 +1,23 @@ +package dotty.tools.bot + +import org.http4s.server.{ Server, ServerApp } +import org.http4s.server.blaze._ + +import scalaz.concurrent.Task + +object Main extends ServerApp with PullRequestService { + + val user = sys.env("USER") + val token = sys.env("TOKEN") + val port = sys.env("PORT").toInt + + /** Services mounted to the server */ + final val services = prService + + override def server(args: List[String]): Task[Server] = { + BlazeBuilder + .bindHttp(port, "0.0.0.0") + .mountService(services, "/api") + .start + } +} diff --git a/bot/src/dotty/tools/bot/PullRequestService.scala b/bot/src/dotty/tools/bot/PullRequestService.scala new file mode 100644 index 000000000000..5ae0b37d0411 --- /dev/null +++ b/bot/src/dotty/tools/bot/PullRequestService.scala @@ -0,0 +1,189 @@ +package dotty.tools.bot + +import org.http4s.{ Status => _, _ } +import org.http4s.client.blaze._ +import org.http4s.client.Client +import org.http4s.headers.Authorization + +import cats.syntax.applicative._ +import scalaz.concurrent.Task +import scala.util.control.NonFatal +import scala.Function.tupled + +import io.circe._ +import io.circe.generic.auto._ +import io.circe.syntax._ +import org.http4s.circe._ +import org.http4s.dsl._ +import org.http4s.util._ + +import model.Github._ + +object TaskIsApplicative { + implicit val taskIsApplicative = new cats.Applicative[Task] { + def pure[A](x: A): Task[A] = Task.now(x) + def ap[A, B](ff: Task[A => B])(fa: Task[A]): Task[B] = + for(f <- ff; a <- fa) yield f(a) + } +} +import TaskIsApplicative._ + +trait PullRequestService { + + /** Username for authorized admin */ + def user: String + + /** OAuth token needed for user to create statuses */ + def token: String + + /** Pull Request HTTP service */ + val prService = HttpService { + case request @ POST -> Root => + request.as(jsonOf[Issue]).flatMap(checkPullRequest) + } + + private[this] lazy val authHeader = { + val creds = BasicCredentials(user, token) + new Authorization(creds) + } + + private final case class CLASignature( + user: String, + signed: Boolean, + version: String, + currentVersion: String + ) + + def claUrl(userName: String): String = + s"https://www.lightbend.com/contribute/cla/scala/check/$userName" + + def commitsUrl(prNumber: Int): String = + s"https://api.github.com/repos/lampepfl/dotty/pulls/$prNumber/commits?per_page=100" + + def statusUrl(sha: String): String = + s"https://api.github.com/repos/lampepfl/dotty/statuses/$sha" + + def toUri(url: String): Task[Uri] = + Uri.fromString(url).fold(Task.fail, Task.now) + + def getRequest(endpoint: Uri): Request = + Request(uri = endpoint, method = Method.GET).putHeaders(authHeader) + + def postRequest(endpoint: Uri): Request = + Request(uri = endpoint, method = Method.POST).putHeaders(authHeader) + + def shutdownClient(client: Client): Unit = + client.shutdownNow() + + sealed trait CommitStatus { + def commit: Commit + def isValid: Boolean + } + final case class Valid(user: String, commit: Commit) extends CommitStatus { def isValid = true } + final case class Invalid(user: String, commit: Commit) extends CommitStatus { def isValid = false } + final case class CLAServiceDown(user: String, commit: Commit) extends CommitStatus { def isValid = false } + final case class MissingUser(commit: Commit) extends CommitStatus { def isValid = false } + + /** Partitions invalid and valid commits */ + def checkCLA(xs: List[Commit], httpClient: Client): Task[List[CommitStatus]] = { + def checkUser(user: String): Task[Commit => CommitStatus] = { + val claStatus = for { + endpoint <- toUri(claUrl(user)) + claReq <- getRequest(endpoint).pure[Task] + claRes <- httpClient.expect(claReq)(jsonOf[CLASignature]) + } yield { (commit: Commit) => + if (claRes.signed) Valid(user, commit) + else Invalid(user, commit) + } + + claStatus.handleWith { + case NonFatal(e) => + println(e) + Task.now((commit: Commit) => CLAServiceDown(user, commit)) + } + } + + def checkCommit(author: Author, commit: List[Commit]): Task[List[CommitStatus]] = + author.login.map(checkUser) + .getOrElse(Task.now(MissingUser)) + .map(f => commit.map(f)) + + Task.gatherUnordered { + val groupedByAuthor: Map[Author, List[Commit]] = xs.groupBy(_.author) + groupedByAuthor.map(tupled(checkCommit)).toList + }.map(_.flatten) + } + + def sendStatuses(xs: List[CommitStatus], httpClient: Client): Task[List[StatusResponse]] = { + def setStatus(cm: CommitStatus): Task[StatusResponse] = { + val desc = + if (cm.isValid) "User signed CLA" + else "User needs to sign cla: https://www.lightbend.com/contribute/cla/scala" + + val stat = cm match { + case Valid(user, commit) => + Status("success", claUrl(user), desc) + case Invalid(user, commit) => + Status("failure", claUrl(user), desc) + case MissingUser(commit) => + Status("failure", "", "Missing valid github user for this PR") + case CLAServiceDown(user, commit) => + Status("pending", claUrl(user), "CLA Service is down") + } + + for { + endpoint <- toUri(statusUrl(cm.commit.sha)) + req <- postRequest(endpoint).withBody(stat.asJson).pure[Task] + res <- httpClient.expect(req)(jsonOf[StatusResponse]) + } yield res + } + + Task.gatherUnordered(xs.map(setStatus)) + } + + private[this] val ExtractLink = """<([^>]+)>; rel="([^"]+)"""".r + def findNext(header: Option[Header]): Option[String] = header.flatMap { header => + val value = header.value + + value + .split(',') + .collect { + case ExtractLink(url, kind) if kind == "next" => + url + } + .headOption + } + + def getCommits(issueNbr: Int, httpClient: Client): Task[List[Commit]] = { + def makeRequest(url: String): Task[List[Commit]] = + for { + endpoint <- toUri(url) + req <- getRequest(endpoint).pure[Task] + res <- httpClient.fetch(req){ res => + val link = CaseInsensitiveString("Link") + val next = findNext(res.headers.get(link)).map(makeRequest).getOrElse(Task.now(Nil)) + + res.as[List[Commit]](jsonOf[List[Commit]]).flatMap(commits => next.map(commits ++ _)) + } + } yield res + + makeRequest(commitsUrl(issueNbr)) + } + + def checkPullRequest(issue: Issue): Task[Response] = { + val httpClient = PooledHttp1Client() + + for { + // First get all the commits from the PR + commits <- getCommits(issue.number, httpClient) + + // Then check the CLA of each commit for both author and committer + statuses <- checkCLA(commits, httpClient) + + // Send statuses to Github and exit + _ <- sendStatuses(statuses, httpClient) + _ <- shutdownClient(httpClient).pure[Task] + resp <- Ok("All statuses checked") + } yield resp + } +} diff --git a/bot/src/dotty/tools/bot/model/Github.scala b/bot/src/dotty/tools/bot/model/Github.scala new file mode 100644 index 000000000000..fafa2b86a37a --- /dev/null +++ b/bot/src/dotty/tools/bot/model/Github.scala @@ -0,0 +1,43 @@ +package dotty.tools.bot +package model + +object Github { + case class PullRequest( + url: String, + id: Long, + commits_url: String + ) + + case class Issue( + number: Int, + pull_request: Option[PullRequest] + ) + + case class CommitInfo( + message: String + ) + + case class Commit( + sha: String, + author: Author, + committer: Author, + commit: CommitInfo + ) + + case class Author( + login: Option[String] + ) + + case class Status( + state: String, + target_url: String, + description: String, + context: String = "CLA" + ) + + case class StatusResponse( + url: String, + id: Long, + state: String + ) +} diff --git a/bot/test/PRServiceTests.scala b/bot/test/PRServiceTests.scala new file mode 100644 index 000000000000..08155d59dfd9 --- /dev/null +++ b/bot/test/PRServiceTests.scala @@ -0,0 +1,91 @@ +package dotty.tools.bot + +import org.junit.Assert._ +import org.junit.Test + +import io.circe._ +import io.circe.generic.auto._ +import io.circe.syntax._ +import io.circe.parser.decode + +import model.Github._ +import org.http4s.client.blaze._ +import scalaz.concurrent.Task + +class PRServiceTests extends PullRequestService { + val user = sys.env("USER") + val token = sys.env("TOKEN") + + def getResource(r: String): String = + Option(getClass.getResourceAsStream(r)).map(scala.io.Source.fromInputStream) + .map(_.mkString) + .getOrElse(throw new Exception(s"resource not found: $r")) + + @Test def canUnmarshalIssueJson = { + val json = getResource("/test-pr.json") + val issue: Issue = decode[Issue](json) match { + case Right(is: Issue) => is + case Left(ex) => throw ex + } + + assert(issue.pull_request.isDefined, "missing pull request") + } + + @Test def canGetAllCommitsFromPR = { + val httpClient = PooledHttp1Client() + val issueNbr = 1941 // has 2 commits: https://github.com/lampepfl/dotty/pull/1941/commits + + val List(c1, c2) = getCommits(issueNbr, httpClient).run + + assertEquals( + "Represent untyped operators as Ident instead of Name", + c1.commit.message.takeWhile(_ != '\n') + ) + + assertEquals( + "Better positions for infix term operations.", + c2.commit.message.takeWhile(_ != '\n') + ) + } + + @Test def canGetMoreThan100Commits = { + val httpClient = PooledHttp1Client() + val issueNbr = 1840 // has >100 commits: https://github.com/lampepfl/dotty/pull/1840/commits + + val numberOfCommits = getCommits(issueNbr, httpClient).run.length + + assert( + numberOfCommits > 100, + s"PR 1840, should have a number of commits greater than 100, but was: $numberOfCommits" + ) + } + + @Test def canCheckCLA = { + val httpClient = PooledHttp1Client() + val validUserCommit = Commit("sha-here", Author(Some("felixmulder")), Author(Some("felixmulder")), CommitInfo("")) + val statuses: List[CommitStatus] = checkCLA(validUserCommit :: Nil, httpClient).run + + assert(statuses.length == 1, s"wrong number of valid statuses: got ${statuses.length}, expected 1") + httpClient.shutdownNow() + } + + @Test def canSetStatus = { + val httpClient = PooledHttp1Client() + val sha = "fa64b4b613fe5e78a5b4185b4aeda89e2f1446ff" + val status = Invalid("smarter", Commit(sha, Author(Some("smarter")), Author(Some("smarter")), CommitInfo(""))) + + val statuses: List[StatusResponse] = sendStatuses(status :: Nil, httpClient).run + + assert( + statuses.length == 1, + s"assumed one status response would be returned, got: ${statuses.length}" + ) + + assert( + statuses.head.state == "failure", + s"status set had wrong state, expected 'failure', got: ${statuses.head.state}" + ) + + httpClient.shutdownNow() + } +} diff --git a/bot/test/resources/test-pr.json b/bot/test/resources/test-pr.json new file mode 100644 index 000000000000..599215f8c9a6 --- /dev/null +++ b/bot/test/resources/test-pr.json @@ -0,0 +1,437 @@ +{ + "action": "synchronize", + "number": 1958, + "pull_request": { + "url": "https://api.github.com/repos/lampepfl/dotty/pulls/1958", + "id": 105198014, + "html_url": "https://github.com/lampepfl/dotty/pull/1958", + "diff_url": "https://github.com/lampepfl/dotty/pull/1958.diff", + "patch_url": "https://github.com/lampepfl/dotty/pull/1958.patch", + "issue_url": "https://api.github.com/repos/lampepfl/dotty/issues/1958", + "number": 1958, + "state": "open", + "locked": false, + "title": "WIP Add \"enum\" construct", + "user": { + "login": "odersky", + "id": 795990, + "avatar_url": "https://avatars.githubusercontent.com/u/795990?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/odersky", + "html_url": "https://github.com/odersky", + "followers_url": "https://api.github.com/users/odersky/followers", + "following_url": "https://api.github.com/users/odersky/following{/other_user}", + "gists_url": "https://api.github.com/users/odersky/gists{/gist_id}", + "starred_url": "https://api.github.com/users/odersky/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/odersky/subscriptions", + "organizations_url": "https://api.github.com/users/odersky/orgs", + "repos_url": "https://api.github.com/users/odersky/repos", + "events_url": "https://api.github.com/users/odersky/events{/privacy}", + "received_events_url": "https://api.github.com/users/odersky/received_events", + "type": "User", + "site_admin": false + }, + "body": "This is a prototype implementation to add an \"enum\" construct to Scala. Scala enums give a more concise notation for \r\n\r\n - enums as in Java\r\n - ADTs\r\n - GADTs\r\n\r\nCurrent status\r\n\r\n - [x] First implementation\r\n - [x] Some test cases\r\n - [ ] A specification\r\n - [ ] An implementation of generic programming in the style of SYB. We need to clarify first exactly\r\n what we want from the compiler\r\n - [ ] A decision whether we want to go ahead with this", + "created_at": "2017-02-08T11:29:18Z", + "updated_at": "2017-02-09T09:18:27Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "7100d31c76a0317b8cd3445970c463b133215252", + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "commits_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1958/commits", + "review_comments_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1958/comments", + "review_comment_url": "https://api.github.com/repos/lampepfl/dotty/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/lampepfl/dotty/issues/1958/comments", + "statuses_url": "https://api.github.com/repos/lampepfl/dotty/statuses/ce4051743d590721dc969c92c44a35147e1e3abc", + "head": { + "label": "dotty-staging:add-enum", + "ref": "add-enum", + "sha": "ce4051743d590721dc969c92c44a35147e1e3abc", + "user": { + "login": "dotty-staging", + "id": 6998674, + "avatar_url": "https://avatars.githubusercontent.com/u/6998674?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dotty-staging", + "html_url": "https://github.com/dotty-staging", + "followers_url": "https://api.github.com/users/dotty-staging/followers", + "following_url": "https://api.github.com/users/dotty-staging/following{/other_user}", + "gists_url": "https://api.github.com/users/dotty-staging/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dotty-staging/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dotty-staging/subscriptions", + "organizations_url": "https://api.github.com/users/dotty-staging/orgs", + "repos_url": "https://api.github.com/users/dotty-staging/repos", + "events_url": "https://api.github.com/users/dotty-staging/events{/privacy}", + "received_events_url": "https://api.github.com/users/dotty-staging/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 17904384, + "name": "dotty", + "full_name": "dotty-staging/dotty", + "owner": { + "login": "dotty-staging", + "id": 6998674, + "avatar_url": "https://avatars.githubusercontent.com/u/6998674?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dotty-staging", + "html_url": "https://github.com/dotty-staging", + "followers_url": "https://api.github.com/users/dotty-staging/followers", + "following_url": "https://api.github.com/users/dotty-staging/following{/other_user}", + "gists_url": "https://api.github.com/users/dotty-staging/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dotty-staging/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dotty-staging/subscriptions", + "organizations_url": "https://api.github.com/users/dotty-staging/orgs", + "repos_url": "https://api.github.com/users/dotty-staging/repos", + "events_url": "https://api.github.com/users/dotty-staging/events{/privacy}", + "received_events_url": "https://api.github.com/users/dotty-staging/received_events", + "type": "Organization", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/dotty-staging/dotty", + "description": "Research platform for new language concepts and compiler technologies for Scala.", + "fork": true, + "url": "https://api.github.com/repos/dotty-staging/dotty", + "forks_url": "https://api.github.com/repos/dotty-staging/dotty/forks", + "keys_url": "https://api.github.com/repos/dotty-staging/dotty/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/dotty-staging/dotty/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/dotty-staging/dotty/teams", + "hooks_url": "https://api.github.com/repos/dotty-staging/dotty/hooks", + "issue_events_url": "https://api.github.com/repos/dotty-staging/dotty/issues/events{/number}", + "events_url": "https://api.github.com/repos/dotty-staging/dotty/events", + "assignees_url": "https://api.github.com/repos/dotty-staging/dotty/assignees{/user}", + "branches_url": "https://api.github.com/repos/dotty-staging/dotty/branches{/branch}", + "tags_url": "https://api.github.com/repos/dotty-staging/dotty/tags", + "blobs_url": "https://api.github.com/repos/dotty-staging/dotty/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/dotty-staging/dotty/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/dotty-staging/dotty/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/dotty-staging/dotty/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/dotty-staging/dotty/statuses/{sha}", + "languages_url": "https://api.github.com/repos/dotty-staging/dotty/languages", + "stargazers_url": "https://api.github.com/repos/dotty-staging/dotty/stargazers", + "contributors_url": "https://api.github.com/repos/dotty-staging/dotty/contributors", + "subscribers_url": "https://api.github.com/repos/dotty-staging/dotty/subscribers", + "subscription_url": "https://api.github.com/repos/dotty-staging/dotty/subscription", + "commits_url": "https://api.github.com/repos/dotty-staging/dotty/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/dotty-staging/dotty/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/dotty-staging/dotty/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/dotty-staging/dotty/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/dotty-staging/dotty/contents/{+path}", + "compare_url": "https://api.github.com/repos/dotty-staging/dotty/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/dotty-staging/dotty/merges", + "archive_url": "https://api.github.com/repos/dotty-staging/dotty/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/dotty-staging/dotty/downloads", + "issues_url": "https://api.github.com/repos/dotty-staging/dotty/issues{/number}", + "pulls_url": "https://api.github.com/repos/dotty-staging/dotty/pulls{/number}", + "milestones_url": "https://api.github.com/repos/dotty-staging/dotty/milestones{/number}", + "notifications_url": "https://api.github.com/repos/dotty-staging/dotty/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/dotty-staging/dotty/labels{/name}", + "releases_url": "https://api.github.com/repos/dotty-staging/dotty/releases{/id}", + "deployments_url": "https://api.github.com/repos/dotty-staging/dotty/deployments", + "created_at": "2014-03-19T13:06:15Z", + "updated_at": "2016-07-11T14:41:18Z", + "pushed_at": "2017-02-09T09:18:27Z", + "git_url": "git://github.com/dotty-staging/dotty.git", + "ssh_url": "git@github.com:dotty-staging/dotty.git", + "clone_url": "https://github.com/dotty-staging/dotty.git", + "svn_url": "https://github.com/dotty-staging/dotty", + "homepage": "", + "size": 27676, + "stargazers_count": 4, + "watchers_count": 4, + "language": "Scala", + "has_issues": false, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 4, + "default_branch": "master" + } + }, + "base": { + "label": "lampepfl:master", + "ref": "master", + "sha": "75bea8dccce2bc3c0e8298ee71061c9871fd26ac", + "user": { + "login": "lampepfl", + "id": 2684793, + "avatar_url": "https://avatars.githubusercontent.com/u/2684793?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lampepfl", + "html_url": "https://github.com/lampepfl", + "followers_url": "https://api.github.com/users/lampepfl/followers", + "following_url": "https://api.github.com/users/lampepfl/following{/other_user}", + "gists_url": "https://api.github.com/users/lampepfl/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lampepfl/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lampepfl/subscriptions", + "organizations_url": "https://api.github.com/users/lampepfl/orgs", + "repos_url": "https://api.github.com/users/lampepfl/repos", + "events_url": "https://api.github.com/users/lampepfl/events{/privacy}", + "received_events_url": "https://api.github.com/users/lampepfl/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 7035651, + "name": "dotty", + "full_name": "lampepfl/dotty", + "owner": { + "login": "lampepfl", + "id": 2684793, + "avatar_url": "https://avatars.githubusercontent.com/u/2684793?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lampepfl", + "html_url": "https://github.com/lampepfl", + "followers_url": "https://api.github.com/users/lampepfl/followers", + "following_url": "https://api.github.com/users/lampepfl/following{/other_user}", + "gists_url": "https://api.github.com/users/lampepfl/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lampepfl/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lampepfl/subscriptions", + "organizations_url": "https://api.github.com/users/lampepfl/orgs", + "repos_url": "https://api.github.com/users/lampepfl/repos", + "events_url": "https://api.github.com/users/lampepfl/events{/privacy}", + "received_events_url": "https://api.github.com/users/lampepfl/received_events", + "type": "Organization", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/lampepfl/dotty", + "description": "Research platform for new language concepts and compiler technologies for Scala.", + "fork": false, + "url": "https://api.github.com/repos/lampepfl/dotty", + "forks_url": "https://api.github.com/repos/lampepfl/dotty/forks", + "keys_url": "https://api.github.com/repos/lampepfl/dotty/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/lampepfl/dotty/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/lampepfl/dotty/teams", + "hooks_url": "https://api.github.com/repos/lampepfl/dotty/hooks", + "issue_events_url": "https://api.github.com/repos/lampepfl/dotty/issues/events{/number}", + "events_url": "https://api.github.com/repos/lampepfl/dotty/events", + "assignees_url": "https://api.github.com/repos/lampepfl/dotty/assignees{/user}", + "branches_url": "https://api.github.com/repos/lampepfl/dotty/branches{/branch}", + "tags_url": "https://api.github.com/repos/lampepfl/dotty/tags", + "blobs_url": "https://api.github.com/repos/lampepfl/dotty/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/lampepfl/dotty/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/lampepfl/dotty/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/lampepfl/dotty/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/lampepfl/dotty/statuses/{sha}", + "languages_url": "https://api.github.com/repos/lampepfl/dotty/languages", + "stargazers_url": "https://api.github.com/repos/lampepfl/dotty/stargazers", + "contributors_url": "https://api.github.com/repos/lampepfl/dotty/contributors", + "subscribers_url": "https://api.github.com/repos/lampepfl/dotty/subscribers", + "subscription_url": "https://api.github.com/repos/lampepfl/dotty/subscription", + "commits_url": "https://api.github.com/repos/lampepfl/dotty/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/lampepfl/dotty/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/lampepfl/dotty/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/lampepfl/dotty/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/lampepfl/dotty/contents/{+path}", + "compare_url": "https://api.github.com/repos/lampepfl/dotty/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/lampepfl/dotty/merges", + "archive_url": "https://api.github.com/repos/lampepfl/dotty/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/lampepfl/dotty/downloads", + "issues_url": "https://api.github.com/repos/lampepfl/dotty/issues{/number}", + "pulls_url": "https://api.github.com/repos/lampepfl/dotty/pulls{/number}", + "milestones_url": "https://api.github.com/repos/lampepfl/dotty/milestones{/number}", + "notifications_url": "https://api.github.com/repos/lampepfl/dotty/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/lampepfl/dotty/labels{/name}", + "releases_url": "https://api.github.com/repos/lampepfl/dotty/releases{/id}", + "deployments_url": "https://api.github.com/repos/lampepfl/dotty/deployments", + "created_at": "2012-12-06T12:57:33Z", + "updated_at": "2017-02-08T13:10:05Z", + "pushed_at": "2017-02-09T08:51:58Z", + "git_url": "git://github.com/lampepfl/dotty.git", + "ssh_url": "git@github.com:lampepfl/dotty.git", + "clone_url": "https://github.com/lampepfl/dotty.git", + "svn_url": "https://github.com/lampepfl/dotty", + "homepage": "http://dotty.epfl.ch", + "size": 28896, + "stargazers_count": 1452, + "watchers_count": 1452, + "language": "Scala", + "has_issues": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": true, + "forks_count": 213, + "mirror_url": null, + "open_issues_count": 250, + "forks": 213, + "open_issues": 250, + "watchers": 1452, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1958" + }, + "html": { + "href": "https://github.com/lampepfl/dotty/pull/1958" + }, + "issue": { + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1958" + }, + "comments": { + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1958/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1958/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1958/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/lampepfl/dotty/statuses/ce4051743d590721dc969c92c44a35147e1e3abc" + } + }, + "requested_reviewers": [ + + ], + "merged": false, + "mergeable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 1, + "review_comments": 2, + "maintainer_can_modify": true, + "commits": 13, + "additions": 501, + "deletions": 173, + "changed_files": 29 + }, + "before": "a51a963005eb6f5a42a0cef7420a7008956e622a", + "after": "ce4051743d590721dc969c92c44a35147e1e3abc", + "repository": { + "id": 7035651, + "name": "dotty", + "full_name": "lampepfl/dotty", + "owner": { + "login": "lampepfl", + "id": 2684793, + "avatar_url": "https://avatars.githubusercontent.com/u/2684793?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lampepfl", + "html_url": "https://github.com/lampepfl", + "followers_url": "https://api.github.com/users/lampepfl/followers", + "following_url": "https://api.github.com/users/lampepfl/following{/other_user}", + "gists_url": "https://api.github.com/users/lampepfl/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lampepfl/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lampepfl/subscriptions", + "organizations_url": "https://api.github.com/users/lampepfl/orgs", + "repos_url": "https://api.github.com/users/lampepfl/repos", + "events_url": "https://api.github.com/users/lampepfl/events{/privacy}", + "received_events_url": "https://api.github.com/users/lampepfl/received_events", + "type": "Organization", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/lampepfl/dotty", + "description": "Research platform for new language concepts and compiler technologies for Scala.", + "fork": false, + "url": "https://api.github.com/repos/lampepfl/dotty", + "forks_url": "https://api.github.com/repos/lampepfl/dotty/forks", + "keys_url": "https://api.github.com/repos/lampepfl/dotty/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/lampepfl/dotty/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/lampepfl/dotty/teams", + "hooks_url": "https://api.github.com/repos/lampepfl/dotty/hooks", + "issue_events_url": "https://api.github.com/repos/lampepfl/dotty/issues/events{/number}", + "events_url": "https://api.github.com/repos/lampepfl/dotty/events", + "assignees_url": "https://api.github.com/repos/lampepfl/dotty/assignees{/user}", + "branches_url": "https://api.github.com/repos/lampepfl/dotty/branches{/branch}", + "tags_url": "https://api.github.com/repos/lampepfl/dotty/tags", + "blobs_url": "https://api.github.com/repos/lampepfl/dotty/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/lampepfl/dotty/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/lampepfl/dotty/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/lampepfl/dotty/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/lampepfl/dotty/statuses/{sha}", + "languages_url": "https://api.github.com/repos/lampepfl/dotty/languages", + "stargazers_url": "https://api.github.com/repos/lampepfl/dotty/stargazers", + "contributors_url": "https://api.github.com/repos/lampepfl/dotty/contributors", + "subscribers_url": "https://api.github.com/repos/lampepfl/dotty/subscribers", + "subscription_url": "https://api.github.com/repos/lampepfl/dotty/subscription", + "commits_url": "https://api.github.com/repos/lampepfl/dotty/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/lampepfl/dotty/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/lampepfl/dotty/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/lampepfl/dotty/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/lampepfl/dotty/contents/{+path}", + "compare_url": "https://api.github.com/repos/lampepfl/dotty/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/lampepfl/dotty/merges", + "archive_url": "https://api.github.com/repos/lampepfl/dotty/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/lampepfl/dotty/downloads", + "issues_url": "https://api.github.com/repos/lampepfl/dotty/issues{/number}", + "pulls_url": "https://api.github.com/repos/lampepfl/dotty/pulls{/number}", + "milestones_url": "https://api.github.com/repos/lampepfl/dotty/milestones{/number}", + "notifications_url": "https://api.github.com/repos/lampepfl/dotty/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/lampepfl/dotty/labels{/name}", + "releases_url": "https://api.github.com/repos/lampepfl/dotty/releases{/id}", + "deployments_url": "https://api.github.com/repos/lampepfl/dotty/deployments", + "created_at": "2012-12-06T12:57:33Z", + "updated_at": "2017-02-08T13:10:05Z", + "pushed_at": "2017-02-09T08:51:58Z", + "git_url": "git://github.com/lampepfl/dotty.git", + "ssh_url": "git@github.com:lampepfl/dotty.git", + "clone_url": "https://github.com/lampepfl/dotty.git", + "svn_url": "https://github.com/lampepfl/dotty", + "homepage": "http://dotty.epfl.ch", + "size": 28896, + "stargazers_count": 1452, + "watchers_count": 1452, + "language": "Scala", + "has_issues": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": true, + "forks_count": 213, + "mirror_url": null, + "open_issues_count": 250, + "forks": 213, + "open_issues": 250, + "watchers": 1452, + "default_branch": "master" + }, + "organization": { + "login": "lampepfl", + "id": 2684793, + "url": "https://api.github.com/orgs/lampepfl", + "repos_url": "https://api.github.com/orgs/lampepfl/repos", + "events_url": "https://api.github.com/orgs/lampepfl/events", + "hooks_url": "https://api.github.com/orgs/lampepfl/hooks", + "issues_url": "https://api.github.com/orgs/lampepfl/issues", + "members_url": "https://api.github.com/orgs/lampepfl/members{/member}", + "public_members_url": "https://api.github.com/orgs/lampepfl/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/2684793?v=3", + "description": null + }, + "sender": { + "login": "felixmulder", + "id": 1530049, + "avatar_url": "https://avatars.githubusercontent.com/u/1530049?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/felixmulder", + "html_url": "https://github.com/felixmulder", + "followers_url": "https://api.github.com/users/felixmulder/followers", + "following_url": "https://api.github.com/users/felixmulder/following{/other_user}", + "gists_url": "https://api.github.com/users/felixmulder/gists{/gist_id}", + "starred_url": "https://api.github.com/users/felixmulder/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/felixmulder/subscriptions", + "organizations_url": "https://api.github.com/users/felixmulder/orgs", + "repos_url": "https://api.github.com/users/felixmulder/repos", + "events_url": "https://api.github.com/users/felixmulder/events{/privacy}", + "received_events_url": "https://api.github.com/users/felixmulder/received_events", + "type": "User", + "site_admin": false + } +} diff --git a/project/Build.scala b/project/Build.scala index 96ee7cdf7aa3..730632b747d6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -4,6 +4,7 @@ import complete.DefaultParsers._ import java.io.{ RandomAccessFile, File } import java.nio.channels.FileLock import scala.reflect.io.Path +import sbtassembly.AssemblyKeys.assembly import org.scalajs.sbtplugin.ScalaJSPlugin import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ @@ -168,6 +169,31 @@ object DottyBuild extends Build { ). settings(publishing) + lazy val `dotty-bot` = project.in(file("bot")). + settings(sourceStructure). + settings( + resourceDirectory in Test := baseDirectory.value / "test" / "resources", + + // specify main and ignore tests when assembling + mainClass in assembly := Some("dotty.tools.bot.Main"), + test in assembly := {}, + + libraryDependencies ++= { + val circeVersion = "0.7.0" + val http4sVersion = "0.15.3" + Seq( + "com.novocode" % "junit-interface" % "0.11" % "test", + "io.circe" %% "circe-generic" % circeVersion, + "io.circe" %% "circe-parser" % circeVersion, + "ch.qos.logback" % "logback-classic" % "1.1.7", + "org.http4s" %% "http4s-dsl" % http4sVersion, + "org.http4s" %% "http4s-blaze-server" % http4sVersion, + "org.http4s" %% "http4s-blaze-client" % http4sVersion, + "org.http4s" %% "http4s-circe" % http4sVersion + ) + } + ) + // Settings shared between dotty-compiler and dotty-compiler-bootstrapped lazy val dottyCompilerSettings = Seq( // set system in/out for repl diff --git a/project/plugins.sbt b/project/plugins.sbt index 57bd465815b4..71a7ef5b6445 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,3 +8,5 @@ addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.8") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")