├── .approve-ci ├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Makefile ├── Procfile ├── README.md ├── app.json ├── package.json ├── src ├── approval.js ├── githubWrapper.js └── index.js └── tests ├── fixtures ├── comments.json ├── content.json ├── hooks.json └── pr.json └── index.js /.approve-ci: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approve-ci", 3 | "approvalCount": 1, 4 | "approvalStrings": ["%F0%9F%91%8D", ":+1:", ":thumbsup:"], 5 | "disapprovalStrings": ["%F0%9F%91%8E", ":-1:", ":thumbsdown:"], 6 | "approveString": "The pull request was approved", 7 | "rejectString": "The pull request needs more work", 8 | "pendingString": "Waiting for approval" 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - standard 4 | - plugin:import/warnings 5 | - plugin:import/errors 6 | parser: babel-eslint 7 | rules: 8 | arrow-parens: 0 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | build 3 | 4 | # npm 5 | node_modules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # Optional REPL history 16 | .node_repl_history 17 | 18 | # Optional Env variables 19 | .env 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5.5' 4 | cache: 5 | directories: 6 | - node_modules 7 | before_deploy: 8 | - make build 9 | deploy: 10 | skip_cleanup: true 11 | provider: heroku 12 | api_key: 13 | secure: kOBtyeBNa1gkUyEPb2UiaedZ0vDtyKJJDqZVyBjy3l3vlaRhKhEsaBB4cVBMEQz4XjZFvwxf1D+DUEyF9tb9bJkjNyjUGMmnc2JCPtSYikYpXMDlcCTCBOh77ikhkcrPL6fmSo/Fxlb684dm0A+Dxo9J/PzQxdXdjL9dK2zVI86x/+QsXXQgIdy/f44l9e2CMf5VAtIN3iaiLuTc0E1I+8k6/yqoRHBlfCv2i59LLabuDT4/HxpuQBqmc4k1i+SHin0CXQEefmCd1UP06RU2+5mBm+anNGCGGZFUaDFRcd4mJZehE9R7fy887L+sFxVZOuSmK93tWNY2fE1CGUkKO57NPs4geG68h3HpiA7gkN0mLKTRCY9FGZDR2iLfkqERidEbvIQlZtSEEgU7epH/JWXMW+D7n0gLHlXvcTp0UjUxVT5ujbWQfHDhu1f5au6z2k4zI+0PoTa90HqCxILhXTs4hyjEMxCmZ84JQKkg+b5Tr5wpbd1aRRCuJmtRynRLXRkZyA2uUhQ2fZCqyedOttQLSsZYMbTfSP92leZgtNVPaayz6vaHEFfZSel6Hbm7WhZHHzFwday43BrMi+ZIZeVnrpaayubVL94MDQv/DxEPRQ2tIGkGP8hc+6/z+4+X3IGUnrt3YdSM0Dfubxpoqn4gJhOX0d/0/a48DAD3nX8= 14 | app: approve-ci 15 | on: 16 | repo: enkidevs/approve-ci 17 | branch: master 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4-onbuild 2 | 3 | WORKDIR /usr/src/app 4 | RUN make test 5 | RUN make build 6 | 7 | CMD npm start 8 | 9 | EXPOSE 3000 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include node_modules/@mathieudutour/js-fatigue/Makefile 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node build/index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # approve-ci 2 | 3 | [![build status](https://img.shields.io/travis/enkidevs/approve-ci/master.svg?style=flat-square)](https://travis-ci.org/enkidevs/approve-ci) 4 | [![Heroku](http://heroku-badge.herokuapp.com/?app=approve-ci&style=flat)](http://approve-ci.herokuapp.com/) 5 | 6 | Approve-ci monitors pull requests and checks for approval. Once the number of approvals exceeds the number of disapprovals by a set amount the pull request is marked as 'approved' and is ready to merge. 7 | 8 | ![approve-ci in use](http://i.imgur.com/2aMhuzk.png) 9 | 10 | ## How To Use? 11 | 12 | - Go to 13 | - your project on GitHub > Settings > Webhooks & services > Add Webhook or 14 | - your organization on GitHub > Settings > Webhooks > Add Webhook 15 | - Payload URL: (https://approve-ci.herokuapp.com/) 16 | - Let me select individual events > Check `repo` 17 | - Add Webhook 18 | 19 | And you are done. Next time a pull request is opened, you should see the pending status from approve-ci ;) 20 | 21 | ## Configuration 22 | 23 | The bot can be configured by adding a `.approve-ci` file to the base directory of the repo. Here's a list of the possible options: 24 | 25 | ``` 26 | { 27 | "name": "approve-ci", 28 | "approvalCount": 1, 29 | "approvalStrings": ["%F0%9F%91%8D", ":+1:", ":thumbsup:"], 30 | "disapprovalStrings": ["%F0%9F%91%8E", ":-1:", ":thumbsdown:"], 31 | "approveString": "The pull request was approved", 32 | "rejectString": "The pull request needs more work", 33 | "pendingString": "Waiting for approval" 34 | } 35 | ``` 36 | 37 | When using emojis you must [URI encodeURI](http://pressbin.com/tools/urlencode_urldecode/) them (as shown above for :thumbsup: and :thumbsdown:). 38 | 39 | ## Protected branches 40 | 41 | GitHub allows you protect branches and to require specific tests to pass before pull requests can be merged. You can set this up by visiting [https://github.com/USERNAME/REPO/settings/branches](https://github.com/USERNAME/REPO/settings/branches), selecting the branch you want to protect and then checking the approve-ci bot (the name is defined in the configuration file, see the next section), approval is needed before a request can be merged. 42 | 43 | [![Protected branches](http://i.imgur.com/bpEb9nU.png)](https://github.com/enkidevs/approve-ci/settings/branches) 44 | 45 | ## How To Contribute or Run Your Own Bot? 46 | 47 | If you want to use a different account for the bot, change the message or extend it to provide additional functionality, we've tried to make it super easy: 48 | 49 | ```bash 50 | git clone https://github.com/enkidevs/approve-ci.git 51 | cd approve-ci 52 | npm install 53 | npm start 54 | # Follow the instructions there 55 | ``` 56 | 57 | Alternatively you can deploy the bot using Heroku by pressing the button below: 58 | 59 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 60 | 61 | Or you can build and deploy on your servers using Docker: 62 | 63 | ```bash 64 | docker build -t approve-ci . 65 | docker run -d -p 80:3000 -e GITHUB_TOKEN= approve-ci 66 | ``` 67 | 68 | ## License 69 | 70 | MIT 71 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approve-ci-bot", 3 | "description": "Automatically approve or reject pull requests", 4 | "repository": "https://github.com/enkidevs/approve-ci", 5 | "logo": "https://avatars3.githubusercontent.com/u/18580415", 6 | "keywords": ["node", "express", "github", "pull requests"], 7 | "env": { 8 | "GITHUB_TOKEN": { 9 | "description": "A token generated for the account which will comment on your pull requests (i.e. f7c41472410cacded24090d24f70e98995d8dc55) (needs the `repo` rights)", 10 | "required": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approve-ci", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "make test", 8 | "postinstall": "make build", 9 | "start": "node build/index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/enkidevs/approve-ci.git" 14 | }, 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/enkidevs/approve-ci/issues" 18 | }, 19 | "homepage": "https://github.com/enkidevs/approve-ci#readme", 20 | "devDependencies": { 21 | "@mathieudutour/js-fatigue": "1.0.5" 22 | }, 23 | "dependencies": { 24 | "base-64": "^0.1.0", 25 | "body-parser": "^1.15.0", 26 | "express": "^4.13.4", 27 | "github": "^2.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/approval.js: -------------------------------------------------------------------------------- 1 | import {decode} from 'base-64' 2 | 3 | const defaultConfig = { 4 | name: 'approve-ci', 5 | approvalCount: 1, 6 | approvalStrings: ['👍', ':+1:', ':thumbsup:'], 7 | disapprovalStrings: ['👎', ':-1:', ':thumbsdown:'], 8 | approveString: 'The pull request was approved', 9 | rejectString: 'The pull request needs more work', 10 | pendingString: 'Waiting for approval ({{x}} more needed)' 11 | } 12 | 13 | export function mergeConfigs (remoteConfig) { 14 | const config = defaultConfig 15 | if (remoteConfig && remoteConfig.content) { 16 | var userConfig = JSON.parse(decode(remoteConfig.content)) 17 | Object.keys(userConfig).forEach((key) => { 18 | if (Array.isArray(userConfig[key])) { 19 | config[key] = userConfig[key].map((item) => decodeURIComponent(item)) 20 | } else if (typeof userConfig[key] === 'string') { 21 | config[key] = decodeURIComponent(userConfig[key]) 22 | } else { 23 | config[key] = userConfig[key] 24 | } 25 | }) 26 | } 27 | return config 28 | } 29 | 30 | export function checkApproved ([remoteConfig, comments, pr]) { 31 | const config = mergeConfigs(remoteConfig) 32 | 33 | const commenters = comments 34 | .filter((comment) => comment.user.login !== pr.user.login) 35 | .reduce((ret, comment) => { 36 | if (config.approvalStrings.some((str) => comment.body.includes(str))) { 37 | return { 38 | ...ret, 39 | [comment.user.id]: (ret[comment.user.id] || 0) + 1 40 | } 41 | } 42 | if (config.disapprovalStrings.some((str) => comment.body.includes(str))) { 43 | return { 44 | ...ret, 45 | [comment.user.id]: (ret[comment.user.id] || 0) - 1 46 | } 47 | } 48 | 49 | return ret 50 | }, {}) 51 | 52 | const result = Object.keys(commenters).reduce((ret, commenter) => { 53 | if (commenters[commenter] > 0) { 54 | return ret + 1 55 | } 56 | if (commenters[commenter] < 0) { 57 | return ret - 1 58 | } 59 | return ret 60 | }, 0) 61 | 62 | let state, description 63 | if (result >= config.approvalCount) { 64 | state = 'success' 65 | description = config.approveString 66 | } else if (result < 0) { 67 | state = 'failure' 68 | description = config.rejectString 69 | } else { 70 | state = 'pending' 71 | description = config.pendingString 72 | } 73 | 74 | return { 75 | sha: pr.head.sha, 76 | name: config.name, 77 | state, 78 | description, 79 | approvalLeft: config.approvalCount - result 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/githubWrapper.js: -------------------------------------------------------------------------------- 1 | import GithubAPI from 'github' 2 | 3 | const {GITHUB_TOKEN} = process.env 4 | 5 | const headers = { 6 | 'user-agent': 'approve-ci-bot' 7 | } 8 | 9 | const gh = new GithubAPI({ 10 | version: '3.0.0', 11 | debug: false, 12 | protocol: 'https', 13 | host: 'api.github.com', 14 | pathPrefix: '', 15 | timeout: 5000, 16 | headers 17 | }) 18 | 19 | gh.authenticate({ 20 | type: 'oauth', 21 | token: GITHUB_TOKEN 22 | }) 23 | 24 | function responseHandler (resolve, reject) { 25 | return function (err, response) { 26 | if (err) { 27 | console.error(err) 28 | return reject(err) 29 | } 30 | resolve(response) 31 | } 32 | } 33 | 34 | export function getConfig (user, repo) { 35 | return new Promise((resolve, reject) => { 36 | gh.repos.getContent({ 37 | user, 38 | repo, 39 | headers, 40 | path: '.approve-ci' 41 | }, responseHandler(resolve, reject)) 42 | }) 43 | } 44 | 45 | export function setState ({sha, name, state, description, approvalLeft = '', repo, user}) { 46 | return new Promise((resolve, reject) => { 47 | gh.statuses.create({ 48 | user, 49 | repo, 50 | sha, 51 | state, 52 | description: description.replace('{{x}}', approvalLeft), 53 | context: name, 54 | target_url: 'https://github.com/enkidevs/approve-ci' 55 | }, responseHandler(resolve, reject)) 56 | }) 57 | } 58 | 59 | export function getComments (number, user, repo) { 60 | return new Promise((resolve, reject) => { 61 | console.log('get comments') 62 | gh.issues.getComments({ 63 | user, 64 | repo, 65 | number, 66 | per_page: 100 67 | }, responseHandler(resolve, reject)) 68 | }) 69 | } 70 | 71 | export function getPullRequest (number, user, repo) { 72 | return new Promise((resolve, reject) => { 73 | console.log('get pull request') 74 | gh.pullRequests.get({ 75 | user, 76 | repo, 77 | number 78 | }, responseHandler(resolve, reject)) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import bodyParser from 'body-parser' 3 | 4 | import {getConfig, setState, getComments, getPullRequest} from './githubWrapper' 5 | import {checkApproved, mergeConfigs} from './approval' 6 | 7 | const {GITHUB_TOKEN} = process.env 8 | 9 | if (!GITHUB_TOKEN) { 10 | console.error('The ci was started without env variables') 11 | console.error('To get started:') 12 | if (!GITHUB_TOKEN) { 13 | console.error('* Visit https://github.com/settings/tokens') 14 | console.error('* Create a new token with the repo rights') 15 | } 16 | console.error('* Run the following command:') 17 | console.error('GITHUB_TOKEN=insert_github_token_here npm start') 18 | process.exit(1) 19 | } 20 | 21 | const app = express() 22 | app.use(bodyParser.json()) 23 | 24 | // Default app-alive message 25 | app.get('/', (req, res) => { 26 | res.send('Approve CI Active. ' + 27 | 'Go to https://github.com/enkidevs/approve-ci for more information.') 28 | }) 29 | 30 | // Handler hook event 31 | app.post('/', (req, res) => { 32 | var event = req.body 33 | console.log(event) 34 | 35 | // Pull Request 36 | switch (event.action) { 37 | case 'opened': 38 | case 'reopened': 39 | case 'synchronize': 40 | // Set status to 'pending' 41 | const user = event.repository.owner.login 42 | const repo = event.repository.name 43 | return getConfig(user, repo).then(mergeConfigs).then((config) => { 44 | return setState({ 45 | user, 46 | repo, 47 | sha: event.pull_request.head.sha, 48 | name: config.name, 49 | state: 'pending', 50 | description: config.pendingString, 51 | approvalLeft: config.approvalCount 52 | }) 53 | }).then((response) => { 54 | res.status(200).send({success: true}) 55 | }).catch((err) => res.status(500).send(err)) 56 | } 57 | 58 | // Issue Comment 59 | switch (event.action) { 60 | case 'created': 61 | case 'edited': 62 | case 'deleted': 63 | // Fetch all comments from PR 64 | if ((event.issue || {}).pull_request) { // check if it's a comment on a PR 65 | const user = event.repository.owner.login 66 | const repo = event.repository.name 67 | return Promise.all([ 68 | getConfig(user, repo), 69 | getComments(event.issue.number, user, repo), 70 | getPullRequest(event.issue.number, user, repo) 71 | ]).then(checkApproved) 72 | .then((result) => setState({ 73 | ...result, 74 | user, 75 | repo 76 | })) 77 | .then((response) => { 78 | res.status(200).send({success: true}) 79 | }) 80 | .catch((err) => res.status(500).send(err)) 81 | } 82 | } 83 | return res.status(200).send({success: true}) 84 | }) 85 | 86 | // Start server 87 | app.set('port', process.env.PORT || 3000) 88 | 89 | app.listen(app.get('port'), () => { 90 | console.log('Listening on port', app.get('port')) 91 | }) 92 | -------------------------------------------------------------------------------- /tests/fixtures/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/repos/enkidevs/approve-ci/issues/comments/212410761", 4 | "html_url": "https://github.com/enkidevs/approve-ci/pull/1#issuecomment-212410761", 5 | "issue_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/1", 6 | "id": 212410761, 7 | "user": { 8 | "login": "lukem512", 9 | "id": 2834976, 10 | "avatar_url": "https://avatars.githubusercontent.com/u/2834976?v=3", 11 | "gravatar_id": "", 12 | "url": "https://api.github.com/users/lukem512", 13 | "html_url": "https://github.com/lukem512", 14 | "followers_url": "https://api.github.com/users/lukem512/followers", 15 | "following_url": "https://api.github.com/users/lukem512/following{/other_user}", 16 | "gists_url": "https://api.github.com/users/lukem512/gists{/gist_id}", 17 | "starred_url": "https://api.github.com/users/lukem512/starred{/owner}{/repo}", 18 | "subscriptions_url": "https://api.github.com/users/lukem512/subscriptions", 19 | "organizations_url": "https://api.github.com/users/lukem512/orgs", 20 | "repos_url": "https://api.github.com/users/lukem512/repos", 21 | "events_url": "https://api.github.com/users/lukem512/events{/privacy}", 22 | "received_events_url": "https://api.github.com/users/lukem512/received_events", 23 | "type": "User", 24 | "site_admin": false 25 | }, 26 | "created_at": "2016-04-20T12:49:57Z", 27 | "updated_at": "2016-04-20T12:49:57Z", 28 | "body": ":-1:" 29 | }, 30 | { 31 | "url": "https://api.github.com/repos/enkidevs/approve-ci/issues/comments/212478871", 32 | "html_url": "https://github.com/enkidevs/approve-ci/pull/1#issuecomment-212478871", 33 | "issue_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/1", 34 | "id": 212478871, 35 | "user": { 36 | "login": "lukem512", 37 | "id": 2834976, 38 | "avatar_url": "https://avatars.githubusercontent.com/u/2834976?v=3", 39 | "gravatar_id": "", 40 | "url": "https://api.github.com/users/lukem512", 41 | "html_url": "https://github.com/lukem512", 42 | "followers_url": "https://api.github.com/users/lukem512/followers", 43 | "following_url": "https://api.github.com/users/lukem512/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/lukem512/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/lukem512/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/lukem512/subscriptions", 47 | "organizations_url": "https://api.github.com/users/lukem512/orgs", 48 | "repos_url": "https://api.github.com/users/lukem512/repos", 49 | "events_url": "https://api.github.com/users/lukem512/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/lukem512/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | }, 54 | "created_at": "2016-04-20T15:30:53Z", 55 | "updated_at": "2016-04-20T15:30:53Z", 56 | "body": ":-1:" 57 | }, 58 | { 59 | "url": "https://api.github.com/repos/enkidevs/approve-ci/issues/comments/212496412", 60 | "html_url": "https://github.com/enkidevs/approve-ci/pull/1#issuecomment-212496412", 61 | "issue_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/1", 62 | "id": 212496412, 63 | "user": { 64 | "login": "lukem512", 65 | "id": 2834976, 66 | "avatar_url": "https://avatars.githubusercontent.com/u/2834976?v=3", 67 | "gravatar_id": "", 68 | "url": "https://api.github.com/users/lukem512", 69 | "html_url": "https://github.com/lukem512", 70 | "followers_url": "https://api.github.com/users/lukem512/followers", 71 | "following_url": "https://api.github.com/users/lukem512/following{/other_user}", 72 | "gists_url": "https://api.github.com/users/lukem512/gists{/gist_id}", 73 | "starred_url": "https://api.github.com/users/lukem512/starred{/owner}{/repo}", 74 | "subscriptions_url": "https://api.github.com/users/lukem512/subscriptions", 75 | "organizations_url": "https://api.github.com/users/lukem512/orgs", 76 | "repos_url": "https://api.github.com/users/lukem512/repos", 77 | "events_url": "https://api.github.com/users/lukem512/events{/privacy}", 78 | "received_events_url": "https://api.github.com/users/lukem512/received_events", 79 | "type": "User", 80 | "site_admin": false 81 | }, 82 | "created_at": "2016-04-20T16:15:42Z", 83 | "updated_at": "2016-04-20T16:15:42Z", 84 | "body": ":+1:\r\n" 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /tests/fixtures/content.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": ".approve-ci", 3 | "path": ".approve-ci", 4 | "sha": "4d9e1dfb274b71cd2640a0075a2b1caff8dda6ac", 5 | "size": 178, 6 | "url": "https://api.github.com/repos/enkidevs/approve-ci/contents/.approve-ci?ref=master", 7 | "html_url": "https://github.com/enkidevs/approve-ci/blob/master/.approve-ci", 8 | "git_url": "https://api.github.com/repos/enkidevs/approve-ci/git/blobs/4d9e1dfb274b71cd2640a0075a2b1caff8dda6ac", 9 | "download_url": "https://raw.githubusercontent.com/enkidevs/approve-ci/master/.approve-ci", 10 | "type": "file", 11 | "content": "ewogICJuYW1lIjogImFwcHJvdmUtdGVzdCIsCiAgImFwcHJvdmFsQ291bnQi\nOiAxLAogICJhcHByb3ZhbFN0cmluZ3MiOiBbIiVGMCU5RiU5MSU4RCIsICI6\nKzE6IiwgIjp0aHVtYnN1cDoiXSwKICAiZGlzYXBwcm92YWxTdHJpbmdzIjog\nWyIlRjAlOUYlOTElOEUiLCAiOi0xOiIsICI6dGh1bWJzZG93bjoiXQp9Cg==\n", 12 | "encoding": "base64", 13 | "_links": { 14 | "self": "https://api.github.com/repos/enkidevs/approve-ci/contents/.approve-ci?ref=master", 15 | "git": "https://api.github.com/repos/enkidevs/approve-ci/git/blobs/4d9e1dfb274b71cd2640a0075a2b1caff8dda6ac", 16 | "html": "https://github.com/enkidevs/approve-ci/blob/master/.approve-ci" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/hooks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Repository", 4 | "id": 8131823, 5 | "name": "travis", 6 | "active": true, 7 | "events": [ 8 | "issue_comment", 9 | "member", 10 | "public", 11 | "pull_request", 12 | "push" 13 | ], 14 | "config": { 15 | "domain": "notify.travis-ci.org", 16 | "token": "********", 17 | "user": "mathieudutour" 18 | }, 19 | "updated_at": "2016-04-20T10:21:31Z", 20 | "created_at": "2016-04-20T10:21:31Z", 21 | "url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8131823", 22 | "test_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8131823/test", 23 | "ping_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8131823/pings", 24 | "last_response": { 25 | "code": 200, 26 | "status": "active", 27 | "message": "OK" 28 | } 29 | }, 30 | { 31 | "type": "Repository", 32 | "id": 8132355, 33 | "name": "web", 34 | "active": true, 35 | "events": [ 36 | "issue_comment", 37 | "pull_request", 38 | "pull_request_review_comment" 39 | ], 40 | "config": { 41 | "content_type": "json", 42 | "insecure_ssl": "0", 43 | "url": "https://approve-ci.herokuapp.com/" 44 | }, 45 | "updated_at": "2016-04-20T11:51:39Z", 46 | "created_at": "2016-04-20T11:26:22Z", 47 | "url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8132355", 48 | "test_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8132355/test", 49 | "ping_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8132355/pings", 50 | "last_response": { 51 | "code": 200, 52 | "status": "active", 53 | "message": "OK" 54 | } 55 | }, 56 | { 57 | "type": "Repository", 58 | "id": 8135069, 59 | "name": "web", 60 | "active": true, 61 | "events": [ 62 | "issue_comment", 63 | "pull_request" 64 | ], 65 | "config": { 66 | "content_type": "json", 67 | "url": "http://approve-ci.herokuapp.com/" 68 | }, 69 | "updated_at": "2016-04-20T15:29:01Z", 70 | "created_at": "2016-04-20T15:29:01Z", 71 | "url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8135069", 72 | "test_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8135069/test", 73 | "ping_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks/8135069/pings", 74 | "last_response": { 75 | "code": 200, 76 | "status": "active", 77 | "message": "OK" 78 | } 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /tests/fixtures/pr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2", 3 | "id": 67216187, 4 | "html_url": "https://github.com/enkidevs/approve-ci/pull/2", 5 | "diff_url": "https://github.com/enkidevs/approve-ci/pull/2.diff", 6 | "patch_url": "https://github.com/enkidevs/approve-ci/pull/2.patch", 7 | "issue_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/2", 8 | "number": 2, 9 | "state": "closed", 10 | "locked": false, 11 | "title": "F/promises", 12 | "user": { 13 | "login": "mathieudutour", 14 | "id": 3254314, 15 | "avatar_url": "https://avatars.githubusercontent.com/u/3254314?v=3", 16 | "gravatar_id": "", 17 | "url": "https://api.github.com/users/mathieudutour", 18 | "html_url": "https://github.com/mathieudutour", 19 | "followers_url": "https://api.github.com/users/mathieudutour/followers", 20 | "following_url": "https://api.github.com/users/mathieudutour/following{/other_user}", 21 | "gists_url": "https://api.github.com/users/mathieudutour/gists{/gist_id}", 22 | "starred_url": "https://api.github.com/users/mathieudutour/starred{/owner}{/repo}", 23 | "subscriptions_url": "https://api.github.com/users/mathieudutour/subscriptions", 24 | "organizations_url": "https://api.github.com/users/mathieudutour/orgs", 25 | "repos_url": "https://api.github.com/users/mathieudutour/repos", 26 | "events_url": "https://api.github.com/users/mathieudutour/events{/privacy}", 27 | "received_events_url": "https://api.github.com/users/mathieudutour/received_events", 28 | "type": "User", 29 | "site_admin": false 30 | }, 31 | "body": "", 32 | "created_at": "2016-04-20T16:16:41Z", 33 | "updated_at": "2016-04-20T16:17:57Z", 34 | "closed_at": "2016-04-20T16:17:54Z", 35 | "merged_at": "2016-04-20T16:17:54Z", 36 | "merge_commit_sha": "3daba021c1719f5b72f862dfaf068d17a831a3ea", 37 | "assignee": null, 38 | "milestone": null, 39 | "commits_url": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2/commits", 40 | "review_comments_url": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2/comments", 41 | "review_comment_url": "https://api.github.com/repos/enkidevs/approve-ci/pulls/comments{/number}", 42 | "comments_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/2/comments", 43 | "statuses_url": "https://api.github.com/repos/enkidevs/approve-ci/statuses/a085778b8f028205881c0bdbfc3772edc5563a3d", 44 | "head": { 45 | "label": "enkidevs:f/promises", 46 | "ref": "f/promises", 47 | "sha": "a085778b8f028205881c0bdbfc3772edc5563a3d", 48 | "user": { 49 | "login": "enkidevs", 50 | "id": 12428872, 51 | "avatar_url": "https://avatars.githubusercontent.com/u/12428872?v=3", 52 | "gravatar_id": "", 53 | "url": "https://api.github.com/users/enkidevs", 54 | "html_url": "https://github.com/enkidevs", 55 | "followers_url": "https://api.github.com/users/enkidevs/followers", 56 | "following_url": "https://api.github.com/users/enkidevs/following{/other_user}", 57 | "gists_url": "https://api.github.com/users/enkidevs/gists{/gist_id}", 58 | "starred_url": "https://api.github.com/users/enkidevs/starred{/owner}{/repo}", 59 | "subscriptions_url": "https://api.github.com/users/enkidevs/subscriptions", 60 | "organizations_url": "https://api.github.com/users/enkidevs/orgs", 61 | "repos_url": "https://api.github.com/users/enkidevs/repos", 62 | "events_url": "https://api.github.com/users/enkidevs/events{/privacy}", 63 | "received_events_url": "https://api.github.com/users/enkidevs/received_events", 64 | "type": "Organization", 65 | "site_admin": false 66 | }, 67 | "repo": { 68 | "id": 56676160, 69 | "name": "approve-ci", 70 | "full_name": "enkidevs/approve-ci", 71 | "owner": { 72 | "login": "enkidevs", 73 | "id": 12428872, 74 | "avatar_url": "https://avatars.githubusercontent.com/u/12428872?v=3", 75 | "gravatar_id": "", 76 | "url": "https://api.github.com/users/enkidevs", 77 | "html_url": "https://github.com/enkidevs", 78 | "followers_url": "https://api.github.com/users/enkidevs/followers", 79 | "following_url": "https://api.github.com/users/enkidevs/following{/other_user}", 80 | "gists_url": "https://api.github.com/users/enkidevs/gists{/gist_id}", 81 | "starred_url": "https://api.github.com/users/enkidevs/starred{/owner}{/repo}", 82 | "subscriptions_url": "https://api.github.com/users/enkidevs/subscriptions", 83 | "organizations_url": "https://api.github.com/users/enkidevs/orgs", 84 | "repos_url": "https://api.github.com/users/enkidevs/repos", 85 | "events_url": "https://api.github.com/users/enkidevs/events{/privacy}", 86 | "received_events_url": "https://api.github.com/users/enkidevs/received_events", 87 | "type": "Organization", 88 | "site_admin": false 89 | }, 90 | "private": false, 91 | "html_url": "https://github.com/enkidevs/approve-ci", 92 | "description": "", 93 | "fork": false, 94 | "url": "https://api.github.com/repos/enkidevs/approve-ci", 95 | "forks_url": "https://api.github.com/repos/enkidevs/approve-ci/forks", 96 | "keys_url": "https://api.github.com/repos/enkidevs/approve-ci/keys{/key_id}", 97 | "collaborators_url": "https://api.github.com/repos/enkidevs/approve-ci/collaborators{/collaborator}", 98 | "teams_url": "https://api.github.com/repos/enkidevs/approve-ci/teams", 99 | "hooks_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks", 100 | "issue_events_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/events{/number}", 101 | "events_url": "https://api.github.com/repos/enkidevs/approve-ci/events", 102 | "assignees_url": "https://api.github.com/repos/enkidevs/approve-ci/assignees{/user}", 103 | "branches_url": "https://api.github.com/repos/enkidevs/approve-ci/branches{/branch}", 104 | "tags_url": "https://api.github.com/repos/enkidevs/approve-ci/tags", 105 | "blobs_url": "https://api.github.com/repos/enkidevs/approve-ci/git/blobs{/sha}", 106 | "git_tags_url": "https://api.github.com/repos/enkidevs/approve-ci/git/tags{/sha}", 107 | "git_refs_url": "https://api.github.com/repos/enkidevs/approve-ci/git/refs{/sha}", 108 | "trees_url": "https://api.github.com/repos/enkidevs/approve-ci/git/trees{/sha}", 109 | "statuses_url": "https://api.github.com/repos/enkidevs/approve-ci/statuses/{sha}", 110 | "languages_url": "https://api.github.com/repos/enkidevs/approve-ci/languages", 111 | "stargazers_url": "https://api.github.com/repos/enkidevs/approve-ci/stargazers", 112 | "contributors_url": "https://api.github.com/repos/enkidevs/approve-ci/contributors", 113 | "subscribers_url": "https://api.github.com/repos/enkidevs/approve-ci/subscribers", 114 | "subscription_url": "https://api.github.com/repos/enkidevs/approve-ci/subscription", 115 | "commits_url": "https://api.github.com/repos/enkidevs/approve-ci/commits{/sha}", 116 | "git_commits_url": "https://api.github.com/repos/enkidevs/approve-ci/git/commits{/sha}", 117 | "comments_url": "https://api.github.com/repos/enkidevs/approve-ci/comments{/number}", 118 | "issue_comment_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/comments{/number}", 119 | "contents_url": "https://api.github.com/repos/enkidevs/approve-ci/contents/{+path}", 120 | "compare_url": "https://api.github.com/repos/enkidevs/approve-ci/compare/{base}...{head}", 121 | "merges_url": "https://api.github.com/repos/enkidevs/approve-ci/merges", 122 | "archive_url": "https://api.github.com/repos/enkidevs/approve-ci/{archive_format}{/ref}", 123 | "downloads_url": "https://api.github.com/repos/enkidevs/approve-ci/downloads", 124 | "issues_url": "https://api.github.com/repos/enkidevs/approve-ci/issues{/number}", 125 | "pulls_url": "https://api.github.com/repos/enkidevs/approve-ci/pulls{/number}", 126 | "milestones_url": "https://api.github.com/repos/enkidevs/approve-ci/milestones{/number}", 127 | "notifications_url": "https://api.github.com/repos/enkidevs/approve-ci/notifications{?since,all,participating}", 128 | "labels_url": "https://api.github.com/repos/enkidevs/approve-ci/labels{/name}", 129 | "releases_url": "https://api.github.com/repos/enkidevs/approve-ci/releases{/id}", 130 | "deployments_url": "https://api.github.com/repos/enkidevs/approve-ci/deployments", 131 | "created_at": "2016-04-20T10:00:50Z", 132 | "updated_at": "2016-04-20T15:04:17Z", 133 | "pushed_at": "2016-04-20T16:30:28Z", 134 | "git_url": "git://github.com/enkidevs/approve-ci.git", 135 | "ssh_url": "git@github.com:enkidevs/approve-ci.git", 136 | "clone_url": "https://github.com/enkidevs/approve-ci.git", 137 | "svn_url": "https://github.com/enkidevs/approve-ci", 138 | "homepage": null, 139 | "size": 31, 140 | "stargazers_count": 2, 141 | "watchers_count": 2, 142 | "language": "JavaScript", 143 | "has_issues": true, 144 | "has_downloads": true, 145 | "has_wiki": true, 146 | "has_pages": false, 147 | "forks_count": 0, 148 | "mirror_url": null, 149 | "open_issues_count": 1, 150 | "forks": 0, 151 | "open_issues": 1, 152 | "watchers": 2, 153 | "default_branch": "master" 154 | } 155 | }, 156 | "base": { 157 | "label": "enkidevs:master", 158 | "ref": "master", 159 | "sha": "f545b07f4ee70bc603dea010d2492116899673fb", 160 | "user": { 161 | "login": "enkidevs", 162 | "id": 12428872, 163 | "avatar_url": "https://avatars.githubusercontent.com/u/12428872?v=3", 164 | "gravatar_id": "", 165 | "url": "https://api.github.com/users/enkidevs", 166 | "html_url": "https://github.com/enkidevs", 167 | "followers_url": "https://api.github.com/users/enkidevs/followers", 168 | "following_url": "https://api.github.com/users/enkidevs/following{/other_user}", 169 | "gists_url": "https://api.github.com/users/enkidevs/gists{/gist_id}", 170 | "starred_url": "https://api.github.com/users/enkidevs/starred{/owner}{/repo}", 171 | "subscriptions_url": "https://api.github.com/users/enkidevs/subscriptions", 172 | "organizations_url": "https://api.github.com/users/enkidevs/orgs", 173 | "repos_url": "https://api.github.com/users/enkidevs/repos", 174 | "events_url": "https://api.github.com/users/enkidevs/events{/privacy}", 175 | "received_events_url": "https://api.github.com/users/enkidevs/received_events", 176 | "type": "Organization", 177 | "site_admin": false 178 | }, 179 | "repo": { 180 | "id": 56676160, 181 | "name": "approve-ci", 182 | "full_name": "enkidevs/approve-ci", 183 | "owner": { 184 | "login": "enkidevs", 185 | "id": 12428872, 186 | "avatar_url": "https://avatars.githubusercontent.com/u/12428872?v=3", 187 | "gravatar_id": "", 188 | "url": "https://api.github.com/users/enkidevs", 189 | "html_url": "https://github.com/enkidevs", 190 | "followers_url": "https://api.github.com/users/enkidevs/followers", 191 | "following_url": "https://api.github.com/users/enkidevs/following{/other_user}", 192 | "gists_url": "https://api.github.com/users/enkidevs/gists{/gist_id}", 193 | "starred_url": "https://api.github.com/users/enkidevs/starred{/owner}{/repo}", 194 | "subscriptions_url": "https://api.github.com/users/enkidevs/subscriptions", 195 | "organizations_url": "https://api.github.com/users/enkidevs/orgs", 196 | "repos_url": "https://api.github.com/users/enkidevs/repos", 197 | "events_url": "https://api.github.com/users/enkidevs/events{/privacy}", 198 | "received_events_url": "https://api.github.com/users/enkidevs/received_events", 199 | "type": "Organization", 200 | "site_admin": false 201 | }, 202 | "private": false, 203 | "html_url": "https://github.com/enkidevs/approve-ci", 204 | "description": "", 205 | "fork": false, 206 | "url": "https://api.github.com/repos/enkidevs/approve-ci", 207 | "forks_url": "https://api.github.com/repos/enkidevs/approve-ci/forks", 208 | "keys_url": "https://api.github.com/repos/enkidevs/approve-ci/keys{/key_id}", 209 | "collaborators_url": "https://api.github.com/repos/enkidevs/approve-ci/collaborators{/collaborator}", 210 | "teams_url": "https://api.github.com/repos/enkidevs/approve-ci/teams", 211 | "hooks_url": "https://api.github.com/repos/enkidevs/approve-ci/hooks", 212 | "issue_events_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/events{/number}", 213 | "events_url": "https://api.github.com/repos/enkidevs/approve-ci/events", 214 | "assignees_url": "https://api.github.com/repos/enkidevs/approve-ci/assignees{/user}", 215 | "branches_url": "https://api.github.com/repos/enkidevs/approve-ci/branches{/branch}", 216 | "tags_url": "https://api.github.com/repos/enkidevs/approve-ci/tags", 217 | "blobs_url": "https://api.github.com/repos/enkidevs/approve-ci/git/blobs{/sha}", 218 | "git_tags_url": "https://api.github.com/repos/enkidevs/approve-ci/git/tags{/sha}", 219 | "git_refs_url": "https://api.github.com/repos/enkidevs/approve-ci/git/refs{/sha}", 220 | "trees_url": "https://api.github.com/repos/enkidevs/approve-ci/git/trees{/sha}", 221 | "statuses_url": "https://api.github.com/repos/enkidevs/approve-ci/statuses/{sha}", 222 | "languages_url": "https://api.github.com/repos/enkidevs/approve-ci/languages", 223 | "stargazers_url": "https://api.github.com/repos/enkidevs/approve-ci/stargazers", 224 | "contributors_url": "https://api.github.com/repos/enkidevs/approve-ci/contributors", 225 | "subscribers_url": "https://api.github.com/repos/enkidevs/approve-ci/subscribers", 226 | "subscription_url": "https://api.github.com/repos/enkidevs/approve-ci/subscription", 227 | "commits_url": "https://api.github.com/repos/enkidevs/approve-ci/commits{/sha}", 228 | "git_commits_url": "https://api.github.com/repos/enkidevs/approve-ci/git/commits{/sha}", 229 | "comments_url": "https://api.github.com/repos/enkidevs/approve-ci/comments{/number}", 230 | "issue_comment_url": "https://api.github.com/repos/enkidevs/approve-ci/issues/comments{/number}", 231 | "contents_url": "https://api.github.com/repos/enkidevs/approve-ci/contents/{+path}", 232 | "compare_url": "https://api.github.com/repos/enkidevs/approve-ci/compare/{base}...{head}", 233 | "merges_url": "https://api.github.com/repos/enkidevs/approve-ci/merges", 234 | "archive_url": "https://api.github.com/repos/enkidevs/approve-ci/{archive_format}{/ref}", 235 | "downloads_url": "https://api.github.com/repos/enkidevs/approve-ci/downloads", 236 | "issues_url": "https://api.github.com/repos/enkidevs/approve-ci/issues{/number}", 237 | "pulls_url": "https://api.github.com/repos/enkidevs/approve-ci/pulls{/number}", 238 | "milestones_url": "https://api.github.com/repos/enkidevs/approve-ci/milestones{/number}", 239 | "notifications_url": "https://api.github.com/repos/enkidevs/approve-ci/notifications{?since,all,participating}", 240 | "labels_url": "https://api.github.com/repos/enkidevs/approve-ci/labels{/name}", 241 | "releases_url": "https://api.github.com/repos/enkidevs/approve-ci/releases{/id}", 242 | "deployments_url": "https://api.github.com/repos/enkidevs/approve-ci/deployments", 243 | "created_at": "2016-04-20T10:00:50Z", 244 | "updated_at": "2016-04-20T15:04:17Z", 245 | "pushed_at": "2016-04-20T16:30:28Z", 246 | "git_url": "git://github.com/enkidevs/approve-ci.git", 247 | "ssh_url": "git@github.com:enkidevs/approve-ci.git", 248 | "clone_url": "https://github.com/enkidevs/approve-ci.git", 249 | "svn_url": "https://github.com/enkidevs/approve-ci", 250 | "homepage": null, 251 | "size": 31, 252 | "stargazers_count": 2, 253 | "watchers_count": 2, 254 | "language": "JavaScript", 255 | "has_issues": true, 256 | "has_downloads": true, 257 | "has_wiki": true, 258 | "has_pages": false, 259 | "forks_count": 0, 260 | "mirror_url": null, 261 | "open_issues_count": 1, 262 | "forks": 0, 263 | "open_issues": 1, 264 | "watchers": 2, 265 | "default_branch": "master" 266 | } 267 | }, 268 | "_links": { 269 | "self": { 270 | "href": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2" 271 | }, 272 | "html": { 273 | "href": "https://github.com/enkidevs/approve-ci/pull/2" 274 | }, 275 | "issue": { 276 | "href": "https://api.github.com/repos/enkidevs/approve-ci/issues/2" 277 | }, 278 | "comments": { 279 | "href": "https://api.github.com/repos/enkidevs/approve-ci/issues/2/comments" 280 | }, 281 | "review_comments": { 282 | "href": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2/comments" 283 | }, 284 | "review_comment": { 285 | "href": "https://api.github.com/repos/enkidevs/approve-ci/pulls/comments{/number}" 286 | }, 287 | "commits": { 288 | "href": "https://api.github.com/repos/enkidevs/approve-ci/pulls/2/commits" 289 | }, 290 | "statuses": { 291 | "href": "https://api.github.com/repos/enkidevs/approve-ci/statuses/a085778b8f028205881c0bdbfc3772edc5563a3d" 292 | } 293 | }, 294 | "merged": true, 295 | "mergeable": null, 296 | "mergeable_state": "unknown", 297 | "merged_by": { 298 | "login": "mathieudutour", 299 | "id": 3254314, 300 | "avatar_url": "https://avatars.githubusercontent.com/u/3254314?v=3", 301 | "gravatar_id": "", 302 | "url": "https://api.github.com/users/mathieudutour", 303 | "html_url": "https://github.com/mathieudutour", 304 | "followers_url": "https://api.github.com/users/mathieudutour/followers", 305 | "following_url": "https://api.github.com/users/mathieudutour/following{/other_user}", 306 | "gists_url": "https://api.github.com/users/mathieudutour/gists{/gist_id}", 307 | "starred_url": "https://api.github.com/users/mathieudutour/starred{/owner}{/repo}", 308 | "subscriptions_url": "https://api.github.com/users/mathieudutour/subscriptions", 309 | "organizations_url": "https://api.github.com/users/mathieudutour/orgs", 310 | "repos_url": "https://api.github.com/users/mathieudutour/repos", 311 | "events_url": "https://api.github.com/users/mathieudutour/events{/privacy}", 312 | "received_events_url": "https://api.github.com/users/mathieudutour/received_events", 313 | "type": "User", 314 | "site_admin": false 315 | }, 316 | "comments": 1, 317 | "review_comments": 0, 318 | "commits": 2, 319 | "additions": 178, 320 | "deletions": 160, 321 | "changed_files": 2 322 | } 323 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import {mergeConfigs, checkApproved} from '../src/approval' 4 | 5 | const comments = require('./fixtures/comments.json') 6 | const content = require('./fixtures/content.json') 7 | const pr = require('./fixtures/pr.json') 8 | 9 | const config = { 10 | name: 'approve-test', 11 | approvalCount: 1, 12 | approvalStrings: ['👍', ':+1:', ':thumbsup:'], 13 | disapprovalStrings: ['👎', ':-1:', ':thumbsdown:'], 14 | approveString: 'The pull request was approved', 15 | rejectString: 'The pull request needs more work', 16 | pendingString: 'Waiting for approval ({{x}} more needed)' 17 | } 18 | 19 | test('Check configs merge', t => { 20 | return t.deepEqual(mergeConfigs(content), config) 21 | }) 22 | 23 | test('Check request is approved', t => { 24 | return t.deepEqual(checkApproved([config, comments, pr]), { 25 | sha: 'a085778b8f028205881c0bdbfc3772edc5563a3d', 26 | name: 'approve-test', 27 | state: 'failure', 28 | description: 'The pull request needs more work', 29 | approvalLeft: 2 30 | }) 31 | }) 32 | --------------------------------------------------------------------------------