From 3f06fe9cc2debaacbb889e33c7339457fc5355cd Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Mon, 6 Feb 2017 17:18:26 +0100 Subject: [PATCH 1/7] Add initial steps to dotty-bot This PR will add a bot whose first purpose is to check the CLA of contributing PRs. It improves on the old bot in that it checks each commit individually, and doesn't get upset about 100+ commits. It would be fun to do this PR with you @OlivierBlanvillain, feel free to provide feedback/comments and refactor as you like --- bot/resources/test-pr.json | 432 ++++++++++++++++++ bot/src/dotty/tools/bot/BotServer.scala | 18 + .../dotty/tools/bot/PullRequestService.scala | 120 +++++ project/Build.scala | 18 + 4 files changed, 588 insertions(+) create mode 100644 bot/resources/test-pr.json create mode 100644 bot/src/dotty/tools/bot/BotServer.scala create mode 100644 bot/src/dotty/tools/bot/PullRequestService.scala diff --git a/bot/resources/test-pr.json b/bot/resources/test-pr.json new file mode 100644 index 000000000000..19078ee337cd --- /dev/null +++ b/bot/resources/test-pr.json @@ -0,0 +1,432 @@ +{ + "action": "opened", + "number": 1943, + "pull_request": { + "url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943", + "id": 104705912, + "html_url": "https://github.com/lampepfl/dotty/pull/1943", + "diff_url": "https://github.com/lampepfl/dotty/pull/1943.diff", + "patch_url": "https://github.com/lampepfl/dotty/pull/1943.patch", + "issue_url": "https://api.github.com/repos/lampepfl/dotty/issues/1943", + "number": 1943, + "state": "open", + "locked": false, + "title": "Positioned#initialPos: Union the position of every children", + "user": { + "login": "smarter", + "id": 63430, + "avatar_url": "https://avatars.githubusercontent.com/u/63430?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/smarter", + "html_url": "https://github.com/smarter", + "followers_url": "https://api.github.com/users/smarter/followers", + "following_url": "https://api.github.com/users/smarter/following{/other_user}", + "gists_url": "https://api.github.com/users/smarter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/smarter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/smarter/subscriptions", + "organizations_url": "https://api.github.com/users/smarter/orgs", + "repos_url": "https://api.github.com/users/smarter/repos", + "events_url": "https://api.github.com/users/smarter/events{/privacy}", + "received_events_url": "https://api.github.com/users/smarter/received_events", + "type": "User", + "site_admin": false + }, + "body": "Previously we missed some children, one consequence of this is that the\r\nposition of the typed tree corresponding to the lambda \"z => 1\" did not\r\ncontain the position of \"z\".\r\n\r\nReview by @odersky ", + "created_at": "2017-02-05T16:51:01Z", + "updated_at": "2017-02-05T16:51:01Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "commits_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/commits", + "review_comments_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/comments", + "review_comment_url": "https://api.github.com/repos/lampepfl/dotty/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/lampepfl/dotty/issues/1943/comments", + "statuses_url": "https://api.github.com/repos/lampepfl/dotty/statuses/9e9206dbf3e1cbd378adcb9f65637d2cf331cc10", + "head": { + "label": "dotty-staging:fix/lambda-position", + "ref": "fix/lambda-position", + "sha": "9e9206dbf3e1cbd378adcb9f65637d2cf331cc10", + "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-05T16:50:41Z", + "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": 27650, + "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": "da7d7231b7f21fe1085abc569eb783590074a359", + "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-05T01:04:39Z", + "pushed_at": "2017-02-05T16:50:21Z", + "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": 28511, + "stargazers_count": 1447, + "watchers_count": 1447, + "language": "Scala", + "has_issues": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": true, + "forks_count": 212, + "mirror_url": null, + "open_issues_count": 243, + "forks": 212, + "open_issues": 243, + "watchers": 1447, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1943" + }, + "html": { + "href": "https://github.com/lampepfl/dotty/pull/1943" + }, + "issue": { + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1943" + }, + "comments": { + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1943/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/lampepfl/dotty/statuses/9e9206dbf3e1cbd378adcb9f65637d2cf331cc10" + } + }, + "merged": false, + "mergeable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": true, + "commits": 1, + "additions": 2, + "deletions": 0, + "changed_files": 1 + }, + "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-05T01:04:39Z", + "pushed_at": "2017-02-05T16:50:21Z", + "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": 28511, + "stargazers_count": 1447, + "watchers_count": 1447, + "language": "Scala", + "has_issues": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": true, + "forks_count": 212, + "mirror_url": null, + "open_issues_count": 243, + "forks": 212, + "open_issues": 243, + "watchers": 1447, + "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": "smarter", + "id": 63430, + "avatar_url": "https://avatars.githubusercontent.com/u/63430?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/smarter", + "html_url": "https://github.com/smarter", + "followers_url": "https://api.github.com/users/smarter/followers", + "following_url": "https://api.github.com/users/smarter/following{/other_user}", + "gists_url": "https://api.github.com/users/smarter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/smarter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/smarter/subscriptions", + "organizations_url": "https://api.github.com/users/smarter/orgs", + "repos_url": "https://api.github.com/users/smarter/repos", + "events_url": "https://api.github.com/users/smarter/events{/privacy}", + "received_events_url": "https://api.github.com/users/smarter/received_events", + "type": "User", + "site_admin": false + } +} diff --git a/bot/src/dotty/tools/bot/BotServer.scala b/bot/src/dotty/tools/bot/BotServer.scala new file mode 100644 index 000000000000..5ce8a83e7685 --- /dev/null +++ b/bot/src/dotty/tools/bot/BotServer.scala @@ -0,0 +1,18 @@ +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 { + + /** Services mounted to the server */ + final val services = prService + + override def server(args: List[String]): Task[Server] = + BlazeBuilder + .bindHttp(8080, "localhost") + .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..8b568b134808 --- /dev/null +++ b/bot/src/dotty/tools/bot/PullRequestService.scala @@ -0,0 +1,120 @@ +package dotty.tools.bot + +import org.http4s._ +import org.http4s.client.blaze._ +import org.http4s.client.Client + +import scalaz.concurrent.Task + +import io.circe._ +import io.circe.generic.auto._ +import io.circe.syntax._ +import org.http4s.circe._ +import org.http4s.dsl._ + +import github4s.Github +import github4s.jvm.Implicits._ +import github4s.free.domain.{ Commit, Issue } + +trait PullRequestService { + + val prService = HttpService { + case request @ POST -> Root => + request.as(jsonOf[Issue]).flatMap(checkPullRequest) + } + + private case class CLASignature( + user: String, + signed: Boolean, + version: String, + currentVersion: String + ) + + private case class Status( + state: String, + target_url: String, + description: String, + context: String = "continuous-integration/CLA" + ) + + 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" + + def toUri(url: String): Task[Uri] = + Uri.fromString(url).fold(Task.fail, Task.now) + + def getRequest(endpoint: Uri): Task[Request] = Task.now { + Request(uri = endpoint, method = Method.GET) + } + + def postRequest(endpoint: Uri): Task[Request] = Task.now { + Request(uri = endpoint, method = Method.POST) + } + + def shutdownClient(client: Client): Task[Unit] = Task.now { + client.shutdownNow() + } + + def users(xs: List[Commit]): Task[Set[String]] = Task.now { + xs.map(_.login).flatten.toSet + } + + sealed trait CommitStatus { + def commit: Commit + def isValid: Boolean + } + final case class Valid(commit: Commit) extends CommitStatus { def isValid = true } + final case class Invalid(commit: Commit) extends CommitStatus { def isValid = false } + + /** Partitions invalid and valid commits */ + def checkCLA(xs: List[Commit], httpClient: Client): Task[List[CommitStatus]] = { + def checkUser(commit: Commit): Task[CommitStatus] = for { + endpoint <- toUri(claUrl(commit.login.get)) + claReq <- getRequest(endpoint) + claRes <- httpClient.expect(claReq)(jsonOf[CLASignature]) + res = if (claRes.signed) Valid(commit) else Invalid(commit) + } yield res + + Task.gatherUnordered(xs.filter(_.login.isDefined).map(checkUser)) + } + + def sendStatuses(xs: List[CommitStatus], httpClient: Client): Task[Unit] = { + def setStatus(cm: CommitStatus): Task[Unit] = for { + endpoint <- toUri(cm.commit.url.replaceAll("git\\/commits", "statuses")) + + target = claUrl(cm.commit.login.getOrElse("")) + state = if (cm.isValid) "success" else "failure" + desc = + if (cm.isValid) "User signed CLA" + else "User needs to sign cla: https://www.lightbend.com/contribute/cla/scala" + + statusReq <- postRequest(endpoint).map(_.withBody(Status(state, target, desc).asJson)) + statusRes <- httpClient.expect(statusReq)(jsonOf[String]) + print <- Task.now(println(statusRes)) + } yield print + + Task.gatherUnordered(xs.map(setStatus)).map(_ => ()) + } + + def checkPullRequest(issue: Issue): Task[Response] = { + val httpClient = PooledHttp1Client() + + for { + // First get all the commits from the PR + endpoint <- toUri(commitsUrl(issue.number)) + commitsReq <- getRequest(endpoint) + commitsRes <- httpClient.expect(commitsReq)(jsonOf[List[Commit]]) + + // Then get check the CLA of each commit + statuses <- checkCLA(commitsRes, httpClient) + + // Send statuses to Github and exit + _ <- sendStatuses(statuses, httpClient) + _ <- shutdownClient(httpClient) + resp <- Ok("All statuses checked") + } yield resp + } +} diff --git a/project/Build.scala b/project/Build.scala index 96ee7cdf7aa3..242223ba2141 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -168,6 +168,24 @@ object DottyBuild extends Build { ). settings(publishing) + lazy val `dotty-bot` = project.in(file("bot")). + settings(sourceStructure). + settings( + libraryDependencies ++= { + val github4sVersion = "0.9.0" + val http4sVersion = "0.15.3" + Seq( + "io.circe" %% "circe-generic" % "0.6.1", + "ch.qos.logback" % "logback-classic" % "1.1.7", + "com.fortysevendeg" %% "github4s" % "0.10.0", + "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 From 43f1d800b92241d86703b5518aab171e039fde4e Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Thu, 9 Feb 2017 21:54:46 +0100 Subject: [PATCH 2/7] Add test for unmarshalling github issue form JSON --- bot/resources/test-pr.json | 175 +++++++++++---------- bot/src/dotty/tools/bot/model/Github.scala | 15 ++ bot/test/PRServiceTests.scala | 34 ++++ project/Build.scala | 7 +- 4 files changed, 143 insertions(+), 88 deletions(-) create mode 100644 bot/src/dotty/tools/bot/model/Github.scala create mode 100644 bot/test/PRServiceTests.scala diff --git a/bot/resources/test-pr.json b/bot/resources/test-pr.json index 19078ee337cd..599215f8c9a6 100644 --- a/bot/resources/test-pr.json +++ b/bot/resources/test-pr.json @@ -1,56 +1,56 @@ { - "action": "opened", - "number": 1943, + "action": "synchronize", + "number": 1958, "pull_request": { - "url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943", - "id": 104705912, - "html_url": "https://github.com/lampepfl/dotty/pull/1943", - "diff_url": "https://github.com/lampepfl/dotty/pull/1943.diff", - "patch_url": "https://github.com/lampepfl/dotty/pull/1943.patch", - "issue_url": "https://api.github.com/repos/lampepfl/dotty/issues/1943", - "number": 1943, + "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": "Positioned#initialPos: Union the position of every children", + "title": "WIP Add \"enum\" construct", "user": { - "login": "smarter", - "id": 63430, - "avatar_url": "https://avatars.githubusercontent.com/u/63430?v=3", + "login": "odersky", + "id": 795990, + "avatar_url": "https://avatars.githubusercontent.com/u/795990?v=3", "gravatar_id": "", - "url": "https://api.github.com/users/smarter", - "html_url": "https://github.com/smarter", - "followers_url": "https://api.github.com/users/smarter/followers", - "following_url": "https://api.github.com/users/smarter/following{/other_user}", - "gists_url": "https://api.github.com/users/smarter/gists{/gist_id}", - "starred_url": "https://api.github.com/users/smarter/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/smarter/subscriptions", - "organizations_url": "https://api.github.com/users/smarter/orgs", - "repos_url": "https://api.github.com/users/smarter/repos", - "events_url": "https://api.github.com/users/smarter/events{/privacy}", - "received_events_url": "https://api.github.com/users/smarter/received_events", + "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": "Previously we missed some children, one consequence of this is that the\r\nposition of the typed tree corresponding to the lambda \"z => 1\" did not\r\ncontain the position of \"z\".\r\n\r\nReview by @odersky ", - "created_at": "2017-02-05T16:51:01Z", - "updated_at": "2017-02-05T16:51:01Z", + "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": null, + "merge_commit_sha": "7100d31c76a0317b8cd3445970c463b133215252", "assignee": null, "assignees": [ ], "milestone": null, - "commits_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/commits", - "review_comments_url": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/comments", + "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/1943/comments", - "statuses_url": "https://api.github.com/repos/lampepfl/dotty/statuses/9e9206dbf3e1cbd378adcb9f65637d2cf331cc10", + "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:fix/lambda-position", - "ref": "fix/lambda-position", - "sha": "9e9206dbf3e1cbd378adcb9f65637d2cf331cc10", + "label": "dotty-staging:add-enum", + "ref": "add-enum", + "sha": "ce4051743d590721dc969c92c44a35147e1e3abc", "user": { "login": "dotty-staging", "id": 6998674, @@ -136,13 +136,13 @@ "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-05T16:50:41Z", + "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": 27650, + "size": 27676, "stargazers_count": 4, "watchers_count": 4, "language": "Scala", @@ -162,7 +162,7 @@ "base": { "label": "lampepfl:master", "ref": "master", - "sha": "da7d7231b7f21fe1085abc569eb783590074a359", + "sha": "75bea8dccce2bc3c0e8298ee71061c9871fd26ac", "user": { "login": "lampepfl", "id": 2684793, @@ -247,68 +247,73 @@ "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-05T01:04:39Z", - "pushed_at": "2017-02-05T16:50:21Z", + "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": 28511, - "stargazers_count": 1447, - "watchers_count": 1447, + "size": 28896, + "stargazers_count": 1452, + "watchers_count": 1452, "language": "Scala", "has_issues": true, "has_downloads": true, "has_wiki": false, "has_pages": true, - "forks_count": 212, + "forks_count": 213, "mirror_url": null, - "open_issues_count": 243, - "forks": 212, - "open_issues": 243, - "watchers": 1447, + "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/1943" + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1958" }, "html": { - "href": "https://github.com/lampepfl/dotty/pull/1943" + "href": "https://github.com/lampepfl/dotty/pull/1958" }, "issue": { - "href": "https://api.github.com/repos/lampepfl/dotty/issues/1943" + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1958" }, "comments": { - "href": "https://api.github.com/repos/lampepfl/dotty/issues/1943/comments" + "href": "https://api.github.com/repos/lampepfl/dotty/issues/1958/comments" }, "review_comments": { - "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1943/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/1943/commits" + "href": "https://api.github.com/repos/lampepfl/dotty/pulls/1958/commits" }, "statuses": { - "href": "https://api.github.com/repos/lampepfl/dotty/statuses/9e9206dbf3e1cbd378adcb9f65637d2cf331cc10" + "href": "https://api.github.com/repos/lampepfl/dotty/statuses/ce4051743d590721dc969c92c44a35147e1e3abc" } }, + "requested_reviewers": [ + + ], "merged": false, "mergeable": null, "mergeable_state": "unknown", "merged_by": null, - "comments": 0, - "review_comments": 0, + "comments": 1, + "review_comments": 2, "maintainer_can_modify": true, - "commits": 1, - "additions": 2, - "deletions": 0, - "changed_files": 1 + "commits": 13, + "additions": 501, + "deletions": 173, + "changed_files": 29 }, + "before": "a51a963005eb6f5a42a0cef7420a7008956e622a", + "after": "ce4051743d590721dc969c92c44a35147e1e3abc", "repository": { "id": 7035651, "name": "dotty", @@ -374,27 +379,27 @@ "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-05T01:04:39Z", - "pushed_at": "2017-02-05T16:50:21Z", + "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": 28511, - "stargazers_count": 1447, - "watchers_count": 1447, + "size": 28896, + "stargazers_count": 1452, + "watchers_count": 1452, "language": "Scala", "has_issues": true, "has_downloads": true, "has_wiki": false, "has_pages": true, - "forks_count": 212, + "forks_count": 213, "mirror_url": null, - "open_issues_count": 243, - "forks": 212, - "open_issues": 243, - "watchers": 1447, + "open_issues_count": 250, + "forks": 213, + "open_issues": 250, + "watchers": 1452, "default_branch": "master" }, "organization": { @@ -411,21 +416,21 @@ "description": null }, "sender": { - "login": "smarter", - "id": 63430, - "avatar_url": "https://avatars.githubusercontent.com/u/63430?v=3", + "login": "felixmulder", + "id": 1530049, + "avatar_url": "https://avatars.githubusercontent.com/u/1530049?v=3", "gravatar_id": "", - "url": "https://api.github.com/users/smarter", - "html_url": "https://github.com/smarter", - "followers_url": "https://api.github.com/users/smarter/followers", - "following_url": "https://api.github.com/users/smarter/following{/other_user}", - "gists_url": "https://api.github.com/users/smarter/gists{/gist_id}", - "starred_url": "https://api.github.com/users/smarter/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/smarter/subscriptions", - "organizations_url": "https://api.github.com/users/smarter/orgs", - "repos_url": "https://api.github.com/users/smarter/repos", - "events_url": "https://api.github.com/users/smarter/events{/privacy}", - "received_events_url": "https://api.github.com/users/smarter/received_events", + "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/bot/src/dotty/tools/bot/model/Github.scala b/bot/src/dotty/tools/bot/model/Github.scala new file mode 100644 index 000000000000..c089f0cf2e92 --- /dev/null +++ b/bot/src/dotty/tools/bot/model/Github.scala @@ -0,0 +1,15 @@ +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] + ) +} diff --git a/bot/test/PRServiceTests.scala b/bot/test/PRServiceTests.scala new file mode 100644 index 000000000000..a8fdba6e2f28 --- /dev/null +++ b/bot/test/PRServiceTests.scala @@ -0,0 +1,34 @@ +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") + } +} diff --git a/project/Build.scala b/project/Build.scala index 242223ba2141..3cc10b6e498c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -172,12 +172,13 @@ object DottyBuild extends Build { settings(sourceStructure). settings( libraryDependencies ++= { - val github4sVersion = "0.9.0" + val circeVersion = "0.7.0" val http4sVersion = "0.15.3" Seq( - "io.circe" %% "circe-generic" % "0.6.1", + "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", - "com.fortysevendeg" %% "github4s" % "0.10.0", "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-blaze-server" % http4sVersion, "org.http4s" %% "http4s-blaze-client" % http4sVersion, From c1836497af19e809ffb60017b28416a68467fa10 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Thu, 9 Feb 2017 22:01:57 +0100 Subject: [PATCH 3/7] Implement relevant functionality for bot, pagination! --- bot/src/dotty/tools/bot/BotServer.scala | 6 +- .../dotty/tools/bot/PullRequestService.scala | 155 ++++++++++++------ bot/src/dotty/tools/bot/model/Github.scala | 28 ++++ bot/test/PRServiceTests.scala | 58 +++++++ 4 files changed, 200 insertions(+), 47 deletions(-) diff --git a/bot/src/dotty/tools/bot/BotServer.scala b/bot/src/dotty/tools/bot/BotServer.scala index 5ce8a83e7685..5f38be15bda1 100644 --- a/bot/src/dotty/tools/bot/BotServer.scala +++ b/bot/src/dotty/tools/bot/BotServer.scala @@ -7,12 +7,16 @@ import scalaz.concurrent.Task object Main extends ServerApp with PullRequestService { + val user = sys.env("USER") + val token = sys.env("TOKEN") + /** Services mounted to the server */ final val services = prService - override def server(args: List[String]): Task[Server] = + override def server(args: List[String]): Task[Server] = { BlazeBuilder .bindHttp(8080, "localhost") .mountService(services, "/api") .start + } } diff --git a/bot/src/dotty/tools/bot/PullRequestService.scala b/bot/src/dotty/tools/bot/PullRequestService.scala index 8b568b134808..1d215cb4f3b2 100644 --- a/bot/src/dotty/tools/bot/PullRequestService.scala +++ b/bot/src/dotty/tools/bot/PullRequestService.scala @@ -1,102 +1,167 @@ package dotty.tools.bot -import org.http4s._ +import org.http4s.{ Status => _, _ } import org.http4s.client.blaze._ import org.http4s.client.Client +import org.http4s.headers.Authorization import scalaz.concurrent.Task +import scala.util.control.NonFatal 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 github4s.Github -import github4s.jvm.Implicits._ -import github4s.free.domain.{ Commit, Issue } +import model.Github._ 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 case class CLASignature( + 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 ) - private case class Status( - state: String, - target_url: String, - description: String, - context: String = "continuous-integration/CLA" - ) - 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" + 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): Task[Request] = Task.now { - Request(uri = endpoint, method = Method.GET) + Request(uri = endpoint, method = Method.GET).putHeaders(authHeader) } def postRequest(endpoint: Uri): Task[Request] = Task.now { - Request(uri = endpoint, method = Method.POST) + Request(uri = endpoint, method = Method.POST).putHeaders(authHeader) } def shutdownClient(client: Client): Task[Unit] = Task.now { client.shutdownNow() } - def users(xs: List[Commit]): Task[Set[String]] = Task.now { - xs.map(_.login).flatten.toSet - } - sealed trait CommitStatus { def commit: Commit def isValid: Boolean } - final case class Valid(commit: Commit) extends CommitStatus { def isValid = true } - final case class Invalid(commit: Commit) extends CommitStatus { def isValid = false } + 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(commit: Commit): Task[CommitStatus] = for { - endpoint <- toUri(claUrl(commit.login.get)) - claReq <- getRequest(endpoint) - claRes <- httpClient.expect(claReq)(jsonOf[CLASignature]) - res = if (claRes.signed) Valid(commit) else Invalid(commit) - } yield res - - Task.gatherUnordered(xs.filter(_.login.isDefined).map(checkUser)) + def checkUser(user: String, commit: Commit): Task[CommitStatus] = { + val claStatus = for { + endpoint <- toUri(claUrl(user)) + claReq <- getRequest(endpoint) + claRes <- httpClient.expect(claReq)(jsonOf[CLASignature]) + res = if (claRes.signed) Valid(user, commit) else Invalid(user, commit) + } yield res + + claStatus.handleWith { + case NonFatal(e) => + println(e) + Task.now(CLAServiceDown(user, commit)) + } + } + + def checkCommit(commit: Commit, author: Author): Task[CommitStatus] = + author.login.map(checkUser(_, commit)).getOrElse(Task.now(MissingUser(commit))) + + Task.gatherUnordered { + xs.flatMap { + case c @ Commit(_, author, commiter, _) => + if (author == commiter) List(checkCommit(c, author)) + else List( + checkCommit(c, author), + checkCommit(c, commiter) + ) + } + } } - def sendStatuses(xs: List[CommitStatus], httpClient: Client): Task[Unit] = { - def setStatus(cm: CommitStatus): Task[Unit] = for { - endpoint <- toUri(cm.commit.url.replaceAll("git\\/commits", "statuses")) - - target = claUrl(cm.commit.login.getOrElse("")) - state = if (cm.isValid) "success" else "failure" - desc = + 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" - statusReq <- postRequest(endpoint).map(_.withBody(Status(state, target, desc).asJson)) - statusRes <- httpClient.expect(statusReq)(jsonOf[String]) - print <- Task.now(println(statusRes)) - } yield print + 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).map(_.withBody(stat.asJson)) + 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) + 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 - Task.gatherUnordered(xs.map(setStatus)).map(_ => ()) + makeRequest(commitsUrl(issueNbr)) } def checkPullRequest(issue: Issue): Task[Response] = { @@ -104,12 +169,10 @@ trait PullRequestService { for { // First get all the commits from the PR - endpoint <- toUri(commitsUrl(issue.number)) - commitsReq <- getRequest(endpoint) - commitsRes <- httpClient.expect(commitsReq)(jsonOf[List[Commit]]) + commits <- getCommits(issue.number, httpClient) - // Then get check the CLA of each commit - statuses <- checkCLA(commitsRes, 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) diff --git a/bot/src/dotty/tools/bot/model/Github.scala b/bot/src/dotty/tools/bot/model/Github.scala index c089f0cf2e92..fafa2b86a37a 100644 --- a/bot/src/dotty/tools/bot/model/Github.scala +++ b/bot/src/dotty/tools/bot/model/Github.scala @@ -12,4 +12,32 @@ object Github { 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 index a8fdba6e2f28..202c721e652e 100644 --- a/bot/test/PRServiceTests.scala +++ b/bot/test/PRServiceTests.scala @@ -31,4 +31,62 @@ class PRServiceTests extends PullRequestService { 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() + } } From 35337eabfa00eb3db09438623aaf825076121ff1 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 13 Feb 2017 10:08:01 +0100 Subject: [PATCH 4/7] Use `.pure` at use site instead of warpping with Task.now --- .../dotty/tools/bot/PullRequestService.scala | 27 ++++++++++++------- bot/test/PRServiceTests.scala | 1 - 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/bot/src/dotty/tools/bot/PullRequestService.scala b/bot/src/dotty/tools/bot/PullRequestService.scala index 1d215cb4f3b2..c0745acc8ca2 100644 --- a/bot/src/dotty/tools/bot/PullRequestService.scala +++ b/bot/src/dotty/tools/bot/PullRequestService.scala @@ -5,6 +5,7 @@ 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 @@ -17,6 +18,15 @@ 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 */ @@ -55,17 +65,14 @@ trait PullRequestService { def toUri(url: String): Task[Uri] = Uri.fromString(url).fold(Task.fail, Task.now) - def getRequest(endpoint: Uri): Task[Request] = Task.now { + def getRequest(endpoint: Uri): Request = Request(uri = endpoint, method = Method.GET).putHeaders(authHeader) - } - def postRequest(endpoint: Uri): Task[Request] = Task.now { + def postRequest(endpoint: Uri): Request = Request(uri = endpoint, method = Method.POST).putHeaders(authHeader) - } - def shutdownClient(client: Client): Task[Unit] = Task.now { + def shutdownClient(client: Client): Unit = client.shutdownNow() - } sealed trait CommitStatus { def commit: Commit @@ -81,7 +88,7 @@ trait PullRequestService { def checkUser(user: String, commit: Commit): Task[CommitStatus] = { val claStatus = for { endpoint <- toUri(claUrl(user)) - claReq <- getRequest(endpoint) + claReq <- getRequest(endpoint).pure[Task] claRes <- httpClient.expect(claReq)(jsonOf[CLASignature]) res = if (claRes.signed) Valid(user, commit) else Invalid(user, commit) } yield res @@ -127,7 +134,7 @@ trait PullRequestService { for { endpoint <- toUri(statusUrl(cm.commit.sha)) - req <- postRequest(endpoint).map(_.withBody(stat.asJson)) + req <- postRequest(endpoint).withBody(stat.asJson).pure[Task] res <- httpClient.expect(req)(jsonOf[StatusResponse]) } yield res } @@ -152,7 +159,7 @@ trait PullRequestService { def makeRequest(url: String): Task[List[Commit]] = for { endpoint <- toUri(url) - req <- getRequest(endpoint) + 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)) @@ -176,7 +183,7 @@ trait PullRequestService { // Send statuses to Github and exit _ <- sendStatuses(statuses, httpClient) - _ <- shutdownClient(httpClient) + _ <- shutdownClient(httpClient).pure[Task] resp <- Ok("All statuses checked") } yield resp } diff --git a/bot/test/PRServiceTests.scala b/bot/test/PRServiceTests.scala index 202c721e652e..08155d59dfd9 100644 --- a/bot/test/PRServiceTests.scala +++ b/bot/test/PRServiceTests.scala @@ -21,7 +21,6 @@ class PRServiceTests extends PullRequestService { .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 { From dea3d5e2e32587c378353c9ba326fc50cc6e5a13 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 13 Feb 2017 10:08:40 +0100 Subject: [PATCH 5/7] Only make one request per author --- .../dotty/tools/bot/PullRequestService.scala | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/bot/src/dotty/tools/bot/PullRequestService.scala b/bot/src/dotty/tools/bot/PullRequestService.scala index c0745acc8ca2..5ae0b37d0411 100644 --- a/bot/src/dotty/tools/bot/PullRequestService.scala +++ b/bot/src/dotty/tools/bot/PullRequestService.scala @@ -8,6 +8,7 @@ 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._ @@ -85,34 +86,32 @@ trait PullRequestService { /** Partitions invalid and valid commits */ def checkCLA(xs: List[Commit], httpClient: Client): Task[List[CommitStatus]] = { - def checkUser(user: String, commit: Commit): Task[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]) - res = if (claRes.signed) Valid(user, commit) else Invalid(user, commit) - } yield res + } yield { (commit: Commit) => + if (claRes.signed) Valid(user, commit) + else Invalid(user, commit) + } claStatus.handleWith { case NonFatal(e) => println(e) - Task.now(CLAServiceDown(user, commit)) + Task.now((commit: Commit) => CLAServiceDown(user, commit)) } } - def checkCommit(commit: Commit, author: Author): Task[CommitStatus] = - author.login.map(checkUser(_, commit)).getOrElse(Task.now(MissingUser(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 { - xs.flatMap { - case c @ Commit(_, author, commiter, _) => - if (author == commiter) List(checkCommit(c, author)) - else List( - checkCommit(c, author), - checkCommit(c, commiter) - ) - } - } + 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]] = { @@ -176,15 +175,15 @@ trait PullRequestService { for { // First get all the commits from the PR - commits <- getCommits(issue.number, httpClient) + commits <- getCommits(issue.number, httpClient) // Then check the CLA of each commit for both author and committer - statuses <- checkCLA(commits, httpClient) + statuses <- checkCLA(commits, httpClient) // Send statuses to Github and exit - _ <- sendStatuses(statuses, httpClient) - _ <- shutdownClient(httpClient).pure[Task] - resp <- Ok("All statuses checked") + _ <- sendStatuses(statuses, httpClient) + _ <- shutdownClient(httpClient).pure[Task] + resp <- Ok("All statuses checked") } yield resp } } From 04c1ce2c9a81e79191ce34d15a439112eefb0c83 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Mon, 13 Feb 2017 10:42:01 +0100 Subject: [PATCH 6/7] Move test resources to test --- bot/{ => test}/resources/test-pr.json | 0 project/Build.scala | 2 ++ 2 files changed, 2 insertions(+) rename bot/{ => test}/resources/test-pr.json (100%) diff --git a/bot/resources/test-pr.json b/bot/test/resources/test-pr.json similarity index 100% rename from bot/resources/test-pr.json rename to bot/test/resources/test-pr.json diff --git a/project/Build.scala b/project/Build.scala index 3cc10b6e498c..6e021a2e34e9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -171,6 +171,8 @@ object DottyBuild extends Build { lazy val `dotty-bot` = project.in(file("bot")). settings(sourceStructure). settings( + resourceDirectory in Test := baseDirectory.value / "test" / "resources", + libraryDependencies ++= { val circeVersion = "0.7.0" val http4sVersion = "0.15.3" From b760a506934fe134222be0a41786f913b53a9008 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Mon, 13 Feb 2017 10:51:56 +0100 Subject: [PATCH 7/7] Add sbt-assembly for dotty-bot --- bot/src/dotty/tools/bot/{BotServer.scala => Main.scala} | 3 ++- project/Build.scala | 5 +++++ project/plugins.sbt | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) rename bot/src/dotty/tools/bot/{BotServer.scala => Main.scala} (87%) diff --git a/bot/src/dotty/tools/bot/BotServer.scala b/bot/src/dotty/tools/bot/Main.scala similarity index 87% rename from bot/src/dotty/tools/bot/BotServer.scala rename to bot/src/dotty/tools/bot/Main.scala index 5f38be15bda1..00b4a2735f94 100644 --- a/bot/src/dotty/tools/bot/BotServer.scala +++ b/bot/src/dotty/tools/bot/Main.scala @@ -9,13 +9,14 @@ 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(8080, "localhost") + .bindHttp(port, "0.0.0.0") .mountService(services, "/api") .start } diff --git a/project/Build.scala b/project/Build.scala index 6e021a2e34e9..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._ @@ -173,6 +174,10 @@ object DottyBuild extends Build { 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" 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")