├── .editorconfig ├── .gitignore ├── Aptfile ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── inline-plz-bot.png ├── main.py ├── package.json └── requirements.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | .env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # Intellij 62 | .idea/ 63 | 64 | # Node 65 | node_modules 66 | -------------------------------------------------------------------------------- /Aptfile: -------------------------------------------------------------------------------- 1 | haskell-platform 2 | golang 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:flask 2 | 3 | COPY * /app 4 | 5 | RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 6 | RUN apt-get update && apt-get install -y ruby-full haskell-platform shellcheck nodejs build-essential nodejs-legacy 7 | 8 | RUN pip install -r /app/requirements.txt 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gem 'yaml-lint', '0.0.7' 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | yaml-lint (0.0.7) 5 | 6 | PLATFORMS 7 | x86-mingw32 8 | 9 | DEPENDENCIES 10 | yaml-lint (= 0.0.7) 11 | 12 | BUNDLED WITH 13 | 1.12.3 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Guy Kisel 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python main.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inline-plz-bot 2 | 3 | Web service version of https://github.com/guykisel/inline-plz - lints your Pull Requests and comments inline on the diffs! 4 | 5 | ![Inline lint!](inline-plz-bot.png?raw=true=829x562) 6 | 7 | ## How do I use this 8 | 9 | 1. Settings -> Webhooks -> Add Webhook 10 | 1. Payload URL: `https://inlineplz.herokuapp.com/` 11 | 1. Let me select individual events: select **Pull Request** 12 | 13 | ## Why do I want to use this 14 | 15 | If you use static analysis with your pull requests, you've probably gotten used to this workflow: 16 | 17 | 1. Run static analysis locally, fix issues 18 | 1. Push up a branch 19 | 1. Open a PR 20 | 1. Wait for the PR to pass in your CI tool 21 | 1. Get a little red X on your PR because you forgot to run one of the static analysis tools 22 | 1. Click on the little red X, crawl through console logs, and eventually find a cryptic message referencing a specific line in one of your files 23 | 1. Go back to your code, look up the right file and line, and then go back to the error message because you already forgot what it was 24 | 25 | This bot gives you the static analysis output directly inlined in your PR diffs so you can understand failures more efficiently. 26 | 27 | ## How does it work 28 | 29 | 1. This repo contains a simple little Flask server that listens for Github webhooks 30 | 1. When someone opens a pull request or pushes up some new commits, the repo's webhook POSTs to the Flask server 31 | 1. The Flask server reads the Github PR data (branch, sha, etc.), clones the repo, and shells out to inline-plz 32 | 1. inline-plz runs static analysis tools and uses the Github API to comment on the PR with any errors it finds 33 | 34 | ## This is cool, how can I contribute 35 | 36 | * Report bugs and feature requests! 37 | * Issues for the webservice/bot should go in this repo (inline-plz-bot) 38 | * Issues for the core functionality of inline-plz should go in https://github.com/guykisel/inline-plz 39 | * Add support for more static analysis tools 40 | * Add support for other code review tools besides just Github 41 | * Add documentation 42 | * Add unit tests 43 | * Fix bugs 44 | -------------------------------------------------------------------------------- /inline-plz-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guykisel/inline-plz-bot/75cfab8e77d40261d2905e2d0d82988c4f36fc57/inline-plz-bot.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import shutil 9 | import subprocess 10 | import tempfile 11 | import threading 12 | import time 13 | import traceback 14 | from flask import Flask, request, redirect 15 | 16 | app = Flask(__name__) 17 | 18 | SAFE_ENV = os.environ.copy() 19 | # wipe our token before passing our envvars into inline-plz 20 | SAFE_ENV['TOKEN'] = '' 21 | DOTFILES = 'dotfiles' 22 | STOP_FILE_NAME = '.inlineplzstop' 23 | REVIEWS_IN_PROGRESS = dict() 24 | 25 | 26 | @app.errorhandler(Exception) 27 | def all_exception_handler(): 28 | """Catch, print, then ignore all errors.""" 29 | traceback.print_exc() 30 | 31 | 32 | def clone(url, dir, token, ref=None): 33 | # https://github.com/blog/1270-easier-builds-and-deployments-using-git-over-https-and-oauth 34 | url = url.replace('https://', 'https://{}@'.format(token)) 35 | print('Cloning: {}'.format(url)) 36 | try: 37 | os.makedirs(dir) 38 | except OSError: 39 | pass 40 | try: 41 | subprocess.check_call( 42 | ['git', 'init'], 43 | cwd=dir, env=SAFE_ENV 44 | ) 45 | 46 | pull_cmd = ['git', 'pull', url] 47 | if ref: 48 | pull_cmd.append(ref) 49 | subprocess.check_call( 50 | pull_cmd, 51 | cwd=dir, env=SAFE_ENV 52 | ) 53 | return True 54 | except subprocess.CalledProcessError: 55 | return False 56 | 57 | 58 | def clone_dotfiles(url, org, tempdir, token): 59 | # https://github.com/blog/1270-easier-builds-and-deployments-using-git-over-https-and-oauth 60 | clone_url = '/'.join([url, org, DOTFILES]) + '.git' 61 | print('Cloning: {}'.format(clone_url)) 62 | dotfile_path = os.path.join(tempdir, DOTFILES) 63 | return clone(clone_url, dotfile_path, token) 64 | 65 | 66 | def lint(data): 67 | try: 68 | pull_request = data['pull_request']['number'] 69 | repo_slug = data['repository']['full_name'] 70 | name = data['repository']['name'] 71 | token = os.environ.get('TOKEN') 72 | interface = 'github' 73 | url = os.environ.get('URL', 'https://github.com') 74 | event_type = data['action'] 75 | sha = data['pull_request']['head']['sha'] 76 | ref = data['pull_request']['head']['ref'] 77 | clone_url = data['pull_request']['head']['repo']['clone_url'] 78 | org = data['repository']['owner']['login'] 79 | except KeyError: 80 | traceback.print_exc() 81 | return 'Invalid pull request data.' 82 | trusted = os.environ.get('TRUSTED', '').lower().strip() in ['true', 'yes', '1'] 83 | 84 | print('Starting inline-plz:') 85 | print('Event: {}'.format(event_type)) 86 | print('PR: {}'.format(pull_request)) 87 | print('Repo slug: {}'.format(repo_slug)) 88 | print('Name: {}'.format(name)) 89 | print('SHA: {}'.format(sha)) 90 | print('Clone URL: {}'.format(clone_url)) 91 | 92 | if event_type not in ['opened', 'synchronize']: 93 | return 94 | 95 | # make temp dirs 96 | tempdir = tempfile.mkdtemp() 97 | dotfile_dir = tempfile.mkdtemp() 98 | time.sleep(1) 99 | 100 | # check for existing runs against this PR 101 | pr_name = '{}-{}'.format(repo_slug, pull_request) 102 | REVIEWS_IN_PROGRESS.setdefault(pr_name, set()) 103 | for dir in REVIEWS_IN_PROGRESS[pr_name]: 104 | stopfile_path = os.path.join(dir, STOP_FILE_NAME) 105 | try: 106 | open(stopfile_path, 'w').close() 107 | except (IOError, OSError): 108 | pass 109 | REVIEWS_IN_PROGRESS[pr_name].add(tempdir) 110 | 111 | try: 112 | # git clone into temp dir 113 | clone(clone_url, os.path.join(tempdir, name), token, ref) 114 | time.sleep(1) 115 | 116 | # git checkout our sha 117 | subprocess.check_call( 118 | ['git', 'checkout', sha], 119 | cwd=os.path.join(tempdir, name), env=SAFE_ENV 120 | ) 121 | time.sleep(1) 122 | 123 | args = [ 124 | 'inline-plz', 125 | '--autorun', 126 | '--repo-slug={}'.format(repo_slug), 127 | '--pull-request={}'.format(pull_request), 128 | '--url={}'.format(url), 129 | '--token={}'.format(token), 130 | '--interface={}'.format(interface), 131 | '--zero-exit' 132 | ] 133 | if trusted: 134 | args.append('--trusted') 135 | if clone_dotfiles(url, org, dotfile_dir, token): 136 | args.append('--config-dir={}'.format( 137 | os.path.join(dotfile_dir, 'dotfiles') 138 | )) 139 | time.sleep(1) 140 | 141 | # run inline-plz in temp dir 142 | print('Args: {}'.format(args)) 143 | subprocess.check_call(args, cwd=os.path.join(tempdir, name), env=SAFE_ENV) 144 | time.sleep(1) 145 | finally: 146 | # delete temp dir 147 | time.sleep(1) 148 | shutil.rmtree(tempdir, ignore_errors=True) 149 | shutil.rmtree(dotfile_dir, ignore_errors=True) 150 | REVIEWS_IN_PROGRESS[pr_name].discard(tempdir) 151 | 152 | 153 | @app.route('/', methods=['GET', 'POST']) 154 | def root(): 155 | if request.method == 'GET': 156 | return redirect('https://github.com/guykisel/inline-plz-bot', code=302) 157 | 158 | # https://developer.github.com/v3/activity/events/types/#pullrequestevent 159 | data = request.get_json() 160 | lint_thread = threading.Thread(target=lint, args=(data, )) 161 | lint_thread.start() 162 | return 'Success!' 163 | 164 | 165 | if __name__ == '__main__': 166 | port = int(os.environ.get('PORT', 5000)) 167 | app.run(host='0.0.0.0', port=port, debug=True) 168 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "eslint": "^2.10.1", 4 | "gherkin-lint": "0.0.15", 5 | "jscs": "^3.0.3", 6 | "jshint": "^2.9.2", 7 | "jsonlint": "^1.6.2", 8 | "stylint": "^1.3.10" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.txt 6 | # 7 | astroid==1.4.8 8 | backports.functools-lru-cache==1.2.1 9 | click==6.6 10 | colorama==0.3.7 11 | configparser==3.5.0 12 | docutils==0.12 13 | dodgy==0.1.9 14 | Flask==0.11.1 15 | github3.py==0.9.5 16 | inlineplz==0.23.1 17 | isort==4.2.5 18 | itsdangerous==0.24 19 | Jinja2==2.8 # via flask 20 | lazy-object-proxy==1.2.2 21 | logilab-common==1.2.2 22 | MarkupSafe==0.23 # via jinja2 23 | mccabe==0.5.2 24 | pep8-naming==0.4.1 25 | pep8==1.7.0 26 | prospector==0.12.2 27 | pycodestyle==2.0.0 # via prospector 28 | pydocstyle==1.0.0 29 | pyflakes==1.2.3 30 | pylint-celery==0.3 31 | pylint-common==0.2.2 32 | pylint-django==0.7.2 33 | pylint-flask==0.3 34 | pylint-plugin-utils==0.2.4 35 | pylint==1.6.4 36 | PyYAML==3.11 # via inlineplz, prospector 37 | requests==2.11.1 38 | requirements-detector==0.5.2 39 | restructuredtext-lint==0.17.0 40 | scandir==1.2 41 | setoptconf==0.2.0 42 | six==1.10.0 43 | unidiff==0.5.2 44 | uritemplate.py==2.0.0 45 | Werkzeug==0.11.10 # via flask 46 | wrapt==1.10.8 47 | xmltodict==0.10.2 48 | 49 | # The following packages are commented out because they are 50 | # considered to be unsafe in a requirements file: 51 | # setuptools # via logilab-common 52 | --------------------------------------------------------------------------------