├── .editorconfig ├── .env.example ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── config.test.js └── handle.test.js ├── docs ├── deploy.md └── screenshot.png ├── index.js ├── lib ├── config.js └── handle.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # The ID of your GitHub App 2 | APP_ID= 3 | WEBHOOK_SECRET=development 4 | 5 | # Uncomment this to get verbose logging; use `info` to show less 6 | # LOG_LEVEL=trace 7 | 8 | # Go to https://smee.io/new set this to the URL that you are redirected to. 9 | # WEBHOOK_PROXY_URL= 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Git todos (git.io/todos) 61 | .todos.yml 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '9' 6 | cache: 7 | directories: 8 | - ~/.npm 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ah.tajelsir@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [style]: https://standardjs.com/ 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Submitting a pull request 13 | 14 | 1. [Fork][fork] and clone the repository 15 | 1. Configure and install the dependencies: `npm install` 16 | 1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so no need to lint seperately 17 | 1. Create a new branch: `git checkout -b my-branch-name` 18 | 1. Make your change, add tests, and make sure the tests still pass 19 | 1. Push to your fork and [submit a pull request][pr] 20 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged. 21 | 22 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 23 | 24 | - Follow the [style guide][style] which is using standard. Any linting errors should be shown when running `npm test` 25 | - Write and update tests. 26 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 27 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 28 | 29 | Work in Progress pull request are also welcome to get feedback early on, or if there is something blocked you. 30 | 31 | ## Resources 32 | 33 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 34 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 35 | - [GitHub Help](https://help.github.com) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present Ahmed T. Ali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Review Me 2 | 3 | [![Travis](https://img.shields.io/travis/ahmed-taj/probot-review-me.svg)](https://travis-ci.org/ahmed-taj/probot-review-me) 4 | 5 | A GitHub App built with [probot](https://github.com/probot/probot) that helps you to decide when a pull request is ready for review based on its statuses. 6 | 7 | ## Why? 8 | 9 | It's not always necessary for all status checks to pass, sometimes we need more than ✕ or ✓ to determine if a pull request is ready for review or not. 10 | 11 | ## How it works 12 | 13 | The app listens to pull request statuses and adds a review label (`[Review me]` by default) if all statuses matched. Otherwise, it removes the label. 14 | 15 | ![screenshot](./docs/screenshot.png) 16 | 17 | That way, maintainers/reviewers can filter pull requests to look only into ready ones. 18 | 19 | ## Usage 20 | 21 | 1. **[Configure the GitHub App](https://github.com/apps/review-me)** 22 | 2. Create `.github/review-me.yml` based on the following template 23 | 3. It will start to listen to pull request statuses and update them accordingly. 24 | 25 | A `.github/review-me.yml` file is required to enable the app: 26 | 27 | ```yaml 28 | # List of contexts (i.e. apps) and their corresponding statuses 29 | when: 30 | # Allowed statuses are: 31 | # 32 | # - success 33 | # - failure 34 | # - pending 35 | # - error 36 | continuous-integration/travis-ci/pr: success 37 | wip: success 38 | 39 | # Override default label (optional) 40 | label: Review Me 41 | ``` 42 | 43 | ## Development 44 | 45 | ``` 46 | # Install dependencies 47 | npm install 48 | 49 | # Run the bot 50 | npm start 51 | ``` 52 | 53 | ## Deployment 54 | 55 | See [docs/deploy.md](docs/deploy.md) if you would like to run your own instance of this app. 56 | 57 | ## You might also like 58 | 59 | * **[commitlint-bot](https://github.com/ahmed-taj/commitlint-bot):** A GitHub App that runs commitlint for you! 60 | * **[DEP](https://github.com/ahmed-taj/dep):** A Github App that helps managing PR dependencies 61 | 62 | ## License 63 | 64 | MIT © [Ahmed T. Ali](https://github.com/ahmed-taj) 65 | -------------------------------------------------------------------------------- /__tests__/config.test.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const { loadConfig } = require('../lib/config') 3 | 4 | // Mock Robot Context 5 | let ctx 6 | 7 | beforeAll(() => { 8 | ctx = { 9 | repo: jest.fn(), 10 | log: { info: jest.fn(), warn: jest.fn() }, 11 | config: jest 12 | .fn() 13 | .mockReturnValueOnce({}) 14 | .mockReturnValueOnce({ when: { travis: 'success' } }) 15 | .mockReturnValue({ 16 | when: { 17 | travis: 'success', 18 | wip: 'success' 19 | }, 20 | label: 'ready-for-review' 21 | }) 22 | } 23 | }) 24 | 25 | test('Returns `null` when errors occur', async () => { 26 | const config = await loadConfig(ctx) 27 | expect(ctx.config).toBeCalled() 28 | expect(config).toBe(null) 29 | }) 30 | 31 | test('Set default label', async () => { 32 | const config = await loadConfig(ctx) 33 | expect(ctx.config).toBeCalled() 34 | expect(config.label).toEqual('Review Me') 35 | }) 36 | 37 | test('Get file content via GitHub API', async () => { 38 | const config = await loadConfig(ctx) 39 | expect(ctx.config).toBeCalled() 40 | expect(config).toEqual({ 41 | when: { travis: 'success', wip: 'success' }, 42 | label: 'ready-for-review' 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /__tests__/handle.test.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const { execute, handle } = require('../lib/handle') 3 | 4 | // Mock Robot Context 5 | let ctx 6 | 7 | beforeEach(() => { 8 | ctx = { 9 | issue: obj => ({ owner: 'user', repo: 'test', ...obj }), 10 | repo: obj => ({ owner: 'user', repo: 'test', ...obj }), 11 | log: { info: jest.fn(), warn: jest.fn() }, 12 | github: { 13 | issues: { 14 | addLabels: jest.fn(), 15 | removeLabel: jest.fn() 16 | }, 17 | pullRequests: { 18 | getAll: jest.fn().mockReturnValue({ 19 | data: [{ number: 1, head: { sha: 'abcdefg' } }] 20 | }) 21 | }, 22 | repos: { 23 | getStatuses: jest.fn().mockReturnValue({ 24 | data: [ 25 | { context: 'travis', state: 'success' }, 26 | { context: 'dep', state: 'success' } 27 | ] 28 | }) 29 | }, 30 | paginate: jest.fn().mockImplementation((fn, cb) => cb(fn)) 31 | } 32 | } 33 | }) 34 | 35 | describe('execute', () => { 36 | const pull = { number: 1, head: { sha: 'abcdefg' } } 37 | 38 | test('Fetchs all PR statuses', async () => { 39 | await execute(ctx, pull, { when: {} }) 40 | expect(ctx.github.repos.getStatuses).toBeCalledWith( 41 | expect.objectContaining({ 42 | ref: pull.head.sha 43 | }) 44 | ) 45 | }) 46 | 47 | test('Adds the label if all conditions pass', async () => { 48 | await execute(ctx, pull, { when: { travis: 'success', dep: 'success' } }) 49 | expect(ctx.github.issues.addLabels).toBeCalled() 50 | expect(ctx.github.issues.removeLabel).not.toBeCalled() 51 | }) 52 | 53 | test('Removes the label if a condition fails', async () => { 54 | await execute(ctx, pull, { when: { travis: 'success', dep: 'pending' } }) 55 | expect(ctx.github.issues.removeLabel).toBeCalled() 56 | expect(ctx.github.issues.addLabels).not.toBeCalled() 57 | }) 58 | 59 | test('Uses the label from the configs if specified', async () => { 60 | // addLabels 61 | await execute(ctx, pull, { when: { travis: 'success' }, label: 'my-label' }) 62 | 63 | expect(ctx.github.issues.addLabels).toBeCalledWith( 64 | expect.objectContaining({ labels: ['my-label'] }) 65 | ) 66 | 67 | // removeLabel 68 | await execute(ctx, pull, { when: { travis: 'failure' }, label: 'label2' }) 69 | 70 | expect(ctx.github.issues.removeLabel).toBeCalledWith( 71 | expect.objectContaining({ name: 'label2' }) 72 | ) 73 | }) 74 | }) 75 | 76 | describe('handle', () => { 77 | test('paginates all open PRs', async () => { 78 | await handle(ctx, { when: { travis: 'success' } }) 79 | expect(ctx.github.paginate).toBeCalled() 80 | expect(ctx.github.pullRequests.getAll).toBeCalledWith( 81 | expect.objectContaining({ state: 'open' }) 82 | ) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploying 2 | 3 | If you would like to run your own instance of this app, see the [docs for deployment](https://probot.github.io/docs/deployment/). 4 | 5 | This app requires these **Permissions & events** for the GitHub App: 6 | 7 | * Commit statuses - **Read-only** 8 | * [x] Check the box for **status** events 9 | * Pull requests - **Read & Write** 10 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z0al/probot-review-me/ffbdd4ff322f17238304cee1157dba0189ca7072/docs/screenshot.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const { loadConfig } = require('./lib/config') 3 | const { handle } = require('./lib/handle') 4 | 5 | /** 6 | * App starting point 7 | * 8 | * @param {Robot} robot 9 | */ 10 | const app = robot => { 11 | robot.on('status', async ctx => { 12 | // Load config from GitHub 13 | const config = await loadConfig(ctx) 14 | 15 | // Invalid config? 16 | if (!config) return 17 | 18 | return handle(ctx, config) 19 | }) 20 | } 21 | 22 | module.exports = app 23 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const Joi = require('joi') 3 | 4 | /** 5 | * validates given configuration object. Returns `null` if invalid. 6 | * 7 | * @param {Object} config Configuration object 8 | * @returns 9 | */ 10 | function validateConfig (config) { 11 | // Prepare schema 12 | const schema = Joi.object() 13 | .keys({ 14 | // List of apps 15 | when: Joi.object() 16 | .pattern( 17 | /.*/, 18 | Joi.string() 19 | .only('success', 'error', 'failure', 'pending') 20 | .required() 21 | ) 22 | .required(), 23 | // The label to add or remove. Default 'Review Me' 24 | label: Joi.string() 25 | .optional() 26 | .default('Review Me') 27 | }) 28 | .unknown() 29 | 30 | // Run validation against the schema 31 | const res = Joi.validate(config, schema) 32 | 33 | // Error? return null 34 | if (res.error) return null 35 | 36 | return res.value 37 | } 38 | 39 | /** 40 | * Loads config from GitHub 41 | * 42 | * @param {Context} ctx A Probot context 43 | * @returns {object} The config object 44 | * @async 45 | */ 46 | async function loadConfig (ctx) { 47 | ctx.log.info('Loading configs from `.github/review-me.yml`') 48 | let config = await ctx.config('review-me.yml') 49 | 50 | config = validateConfig(config) 51 | if (!config) ctx.log.warn('Invalid configuration found!') 52 | 53 | return config 54 | } 55 | 56 | module.exports = { loadConfig } 57 | -------------------------------------------------------------------------------- /lib/handle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks the given Pull Request's head commit statuses and update its labels 3 | * accordingly. 4 | * 5 | * @param {Context} ctx A Probot context 6 | * @param {Object} pull GitHub Pull Request object 7 | * @param {object} config The App config object 8 | */ 9 | async function execute(ctx, pull, config) { 10 | // Necessary info 11 | const { github, log } = ctx 12 | const issue = ctx.issue({ number: pull.number }) 13 | const repo = ctx.repo({ ref: pull.head.sha, per_page: 100 }) 14 | 15 | // Find specific app status 16 | // Statuses are returned in reverse chronological order. The first status 17 | // in the list will be the latest one. 18 | const find = (term, list) => { 19 | return list.find(st => st.context.toLowerCase() === term.toLowerCase()) 20 | } 21 | 22 | return github.paginate(github.repos.getStatuses(repo), async page => { 23 | for (const app in config.when) { 24 | const result = find(app, page.data) 25 | const status = (result && result.state) || null 26 | 27 | if (status !== config.when[app]) { 28 | log.warn( 29 | `Expected the status of "${app}" to be "${ 30 | config.when[app] 31 | }" but it's "${status}"` 32 | ) 33 | return github.issues.removeLabel({ ...issue, name: config.label }) 34 | } 35 | } 36 | 37 | log.info(`All conditions passed. It's ready!`) 38 | return github.issues.addLabels({ ...issue, labels: [config.label] }) 39 | }) 40 | } 41 | 42 | /** 43 | * Iterates over all open Pull Requests and calls `execute` against each. 44 | * 45 | * @param {Context} ctx A Probot context 46 | * @param {object} config The App config object 47 | */ 48 | async function handle(ctx, config) { 49 | // Extract necessary info 50 | const repo = ctx.repo() 51 | const { github } = ctx 52 | 53 | // Search for open PRs 54 | return github.paginate( 55 | github.pullRequests.getAll({ ...repo, state: 'open', per_page: 100 }), 56 | async page => { 57 | for (const pull of page.data) { 58 | try { 59 | await execute(ctx, pull, config) 60 | } catch (err) { 61 | // Nothing needs to be done. Probably because of `removeLabel`. 62 | } 63 | } 64 | } 65 | ) 66 | } 67 | 68 | module.exports = { handle, execute } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "probot-review-me", 3 | "version": "1.0.0", 4 | "description": 5 | "Decide when a pull request is ready for review based on its statuses", 6 | "author": "Ahmed T. Ali ", 7 | "license": "MIT", 8 | "repository": "https://github.com/ahmed-taj/probot-review-me.git", 9 | "scripts": { 10 | "now-start": "PRIVATE_KEY=$(echo $PRIVATE_KEY | base64 -d) npm start", 11 | "start": "probot run ./index.js", 12 | "precommit": "lint-staged", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "joi": "^13.1.2", 17 | "probot": "^5.0.0" 18 | }, 19 | "devDependencies": { 20 | "husky": "^0.14.3", 21 | "jest": "^21.2.1", 22 | "lint-staged": "^7.0.0", 23 | "prettier-standard": "^8.0.0", 24 | "smee-client": "^1.0.1" 25 | }, 26 | "engines": { 27 | "node": "8.x", 28 | "npm": "5.x" 29 | }, 30 | "lint-staged": { 31 | "*.js": ["prettier-standard", "git add"], 32 | "*.json": ["prettier --write", "git add"] 33 | }, 34 | "jest": { 35 | "testEnvironment": "node" 36 | } 37 | } 38 | --------------------------------------------------------------------------------