Skip to content
GitHub webhook handler for closing pull requests that have been merged using rebase etc.
Python Shell
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
script
.editorconfig
.envrc
.gitignore
.travis.yml
LICENSE
README.adoc
app.py
requirements-dev.txt
requirements.txt
settings.ini.example
setup.cfg

README.adoc

GitHub Pull Requests Closer

Build Status

This is a simple webhook handler that processes push events from GitHub and automatically closes pull requests that have been merged, but GitHub didn’t automatically detect them (i.e. commits has been rebased and/or squashed).

It’s built with Bottle, a WSGI micro web-framework for Python, and requires Python 3.3+. [1]

Why?

When you rebase commits (or just apply plain patches using git am), git re-applies changes introduced in these commits on the HEAD of the branch you’re rebasing onto. This means that the resulting commits have different SHA-1 hash and also different parents, so the relation to the original commits is lost. Well, almost, you can still compare just content (diff) of the commits. But if you also do some changes when rebasing (e.g. squash some commits, fix typos…), then diffs will not help you much.

Hopefully, there’s one thing that remains the same even when rebasing commits (if it’s done right) – author’s name, email and the date written. [2] It’s very unlikely that one person created two commits in the same repository at exactly the same time, so we can use it to find the original commit(s) and the corresponding pull request.

There may not be one-to-one mapping, e.g. when the committer has squashed some commits. We can say that the pull request will be closed when we find at least one commit that has been merged. This may be a problem, what if the committer applied just some of the changes introduced in the pull request? To minimize this issue, we can also compare set of the filenames that have been changed (i.e. added, modified, or deleted). It’s not bullet proof, but it may be sufficient.

You may ask, how the heck is this useful? I created it for Alpine Linux, that uses GitHub just as a mirror and pull requests are always merged into the main repository as a set of patches using git am. It used to be quite often that someone had merged commits from a pull request, but forgot to close it.

Installation

Clone this repository and install dependencies:

git clone git@github.com:jirutka/github-pr-closer.git
cd github-pr-closer
pip install -r requirements.txt

Requirements

Configuration

Configuration is read from the file settings.ini. You may change location of the settings file using the environment variable CONF_FILE.

Each section of this file (except DEFAULT) corresponds to one or more repositories. The section name represents full name (slug) of a repository or regular expression matching full names of repositories you want to process. Push events from repositories that do not match any section are ignored.

Each section may contain the following fields.

branch_regex

A regular expression that specifies branches which should be processed. Push events from branches that don’t match this regular expression will be ignored. To match all branches, use .*. If you want to match branches master and dev, use (master|dev). Default is master.

cache_maxsize

Maximum number of pull requests' metadata to keep in LRU cache. This field may be declared only in the DEFAULT section. Default is 250.

close_comment

Text that will be added as a comment to a pull request being closed. It may contain placeholder {committer} that will be replaced by GitHub login (prefixed with @) or name (if the login is not available) of the committer, and {commits} that will be replaced by a list of hashes of the actually merged commits. This field is required.

github_token

Your GitHub token for communication with GitHub API. It must have scope public_repo. This field is required.

hook_secret

Webhook secret used by GitHub to sign the message. This field is required.

You can also declare default values for all repositories in special section named DEFAULT.

Run it

There are multiple ways how to run it:

Run with the built-in server

Simply execute the app.py:

python3 app.py

Then you can access the app on http://localhost:8080. The port and host may be changed using environment variables HTTP_PORT and HTTP_HOST.

Run with uWSGI and nginx

If you have many micro-apps like this, it’s IMO kinda overkill to run each in a separate uWSGI process, isn’t it? It’s not so well known, but uWSGI allows to “mount” multiple application in a single uWSGI process and with a single socket.

Sample uWSGI configuration:
[uwsgi]
plugins = python34
socket = /run/uwsgi/main.sock
chdir = /var/www/scripts
logger = file:/var/log/uwsgi/main.log
processes = 1
threads = 2
# map URI paths to applications
mount = /hooks/github-pr-closer=github-pr-closer/app.py
#mount = /other/foo=foo/app.py
manage-script-name = true
Sample nginx configuration as a reverse proxy in front of uWSGI:
server {
    listen 443 ssl;
    server_name example.org;

    ssl_certificate /etc/ssl/nginx/nginx.crt;
    ssl_certificate_key /etc/ssl/nginx/nginx.key;

    location /hooks/github-pr-closer {
        uwsgi_pass unix:/run/uwsgi/main.sock;
        include uwsgi_params;
    }
}

License

This project is licensed under MIT License. For the full text of the license, see the LICENSE file.


1. No, it doesn’t work with legacy Python 2.7 and I’m not gonna support it… Just install Python 3.
2. Git distinguishes author, who created the patch, and committer, who committed it to the tree.
You can’t perform that action at this time.