Hoff is a bot for GitHub that enforces a clean history, and ensures that master always builds.
Hoff intends to replace the merge button in the GitHub UI. Hoff integrates changes into master using a rebase. This keeps the history clean, free of random fork points and merge commits. (TODO: In the future Hoff will also enforce a commit message format.)
Furthermore, Hoff implements the Not Rocket Science Principle of software engineering:
Automatically maintain a repository of code that always passes all the tests.
The application watches a repository for new pull requests. Once a pull request
has been approved (through an @hoffbot merge
comment left by the PR author or
reviewer), it integrates the changes into master, and pushes those to a testing
branch. When CI reports a successful build for this branch, master is forwarded
to it. If the build fails, the commits never make it into master, keeping the
build green at all times.
Merge trains: if a second PR receives a merge comment while a build is still happening, Hoff starts a speculative rebase on top of the previous build. The same goes for a third PR. Assuming all of the three builds pass, the third PR is merged after the time of a single build.
Supposing Hoff is set up to listen for the comment prefix @hoffbot
with a
matching GitHub user, you use it by commenting on a PR with any of the
following commands:
@hoffbot merge
: rebase then merge;@hoffbot merge and tag
: rebase, merge then tag.@hoffbot merge and deploy
: rebase, merge, tag then deploy to the default environment;@hoffbot merge and deploy to <env>
: rebase, merge, tag then deploy to the specified environment;@hoffbot retry
: retry the last operation with the same parameters after the CI build has failed. Equivalent to closing and reopening the PR and then tagging the bot again with the same command.
For all the commands, Hoff will wait for the builds to pass after rebasing and before merging. When the PR is merged, GitHub closes the PR as merged and, when configured to automatically do so, deletes the PR branch.
Hoff does not actually do the deploying. It just adds a special marker to the tag message indicating to the CI job that the tag should be deployed.
On Fridays, by default, Hoff refuses to do the above actions. To force merges
on Fridays, simply add on friday
at the end of your commands, like so:
@hoffbot merge on friday
;@hoffbot merge and tag on friday
.@hoffbot merge and deploy on friday
;
See the installation guide if you want to run a self-hosted version of Hoff.
TODO: Write a proper guide to build a package. TODO: Publish official deb packages?
Hoff is written in Haskell and builds with Cabal:
$ cabal build -j
$ cabal test -j
To run the application locally
you first need to create an appropriate config.json
file:
$ cp doc/example-dev-config.json config.json
Edit config.json
to match your required settings.
You can generate a personal access token in the
"personal access tokens" tab of GitHub settings.
Give it a 7 days expiration and access to just "repo"s.
$ mkdir -p run/state
$ cabal run hoff config.json
Hoff also comes with an .envrc
file that can be used by direnv. You have to run
direnv allow
once to give permission. From then on you will always be in the right
nix develop environment when cd'ing to the Hoff project.
You can then access http://localhost:1979 to see the open PRs and build queue.
The build queue is fetched through GitHub's web interface, so you will be able to see the full list right away.
Comments and build statuses are only sent in though a webhook.
While running without a public IP address,
webhooks will have to be forwarded to your local instance somehow.
You can use the official gh webhook forward
tool for this, by running
$ gh webhook forward --repo=REPO --events='*' --url="http://localhost:1979/hook/github" --secret="SECRET"
where REPO
is the repo you're testing with (in the OWNER/NAME
format), and
SECRET
should be the value of secret
in your config.json
.
Tip
If you have jq
installed, you can avoid manually copying the webhook secret
around by passing --secret=$(jq -r '.secret' config.json)
.
Note
If your testing repo doesn't have CI that marks commits' build status, you will still need to manually trigger a build status change for the speculative rebase commit that Hoff produces, as described below.
Alternatively, to manually trigger webhooks for a local Hoff instance,
you can use some of the scripts in the tools/
folder to simulate them:
$ ./tools/comment deckard 31337 @hoffbot merge
$ ./tools/build-status c033170123456789abcdef0123456789abcdef01
The tests of Hoff are extensive, you may be able to get by just by running them
when making changes to the code. To run a specific test, use --match
giving
part of the test title:
$ nix develop --file default.nix -c cabal run spec -- --match "part of the test title"
The implementation uses free monads and some of the tests replace lower level functionality with mocks.
To run Hoff on a server, you can build a self-contained squashfs file system image with Nix:
$ nix build --out-link hoff.img
$ cp package/example-config.json config.json
$ vi config.json # edit the file appropriately
$ sudo systemd-nspawn \
--ephemeral \
--image hoff.img \
--bind-ro=$PWD:/etc \
-- /usr/bin/hoff /etc/config.json
The image includes Hoff and all of its dependencies (Git, SSH). You can run it with systemd. TODO: Make it work with portablectl.
You can also build Hoff as a deb package by running:
./package/build-and-ship.sh
The main Hoff interface is through GitHub comments, here is an example:
Channable's Hoff repository is tracked itself using Hoff (@OpsBotPrime), so you can see many examples of use in past merged PRs (#173, #167, #138, #108, ...).
In addition to the comment interface, you can check the status of running and failed builds through a web interface:
Hoff supports parallel speculative builds of subsequently approved PRs. Without merge trains, here is how a timeline of interaction with Hoff would go:
Supposing the build time is of ≈10 minutes:
- the authors of PR#1 wait ≈10 minutes for their PR to be merged;
- the authors of PR#2 wait ≈16 minutes for their PR to be merged;
- the authors of PR#3 wait ≈22 minutes for their PR to be merged.
With merge trains, Hoff reduces the waiting time for merges. Here is the same timeline of interaction for Hoff with merge trains active:
The first PR is merged and rebased immediatelly as usual.
The second and third PRs are now merged and rebased immediatelly
on top of the first and second respectively.
Assuming all builds eventually pass,
the authors of all PRs only have to wait 10 minutes each
for their PRs to reach master
.
The waiting time for authors of the third PR is reduced by 12 minutes!
Failing merge trains. If at some point in the train a build fails, subsequent PRs are (speculatively) reintegrated and their builds are restarted:
-
Three merge commands are issued in a short period for PR#1, PR#2 and PR#3.
-
PR#2 fails while PR#1 and PR#3 are still building.
- Since PR#2 is built on top of PR#1, Hoff cannot assume PR#2 is the culprit of the failure so Hoff delays reporting this until PR#1 finishes.
- Since PR#3 has been built on top of PR#2,
its build is restarted with a rebase and merge on top of PR#1.
This is done by pushing to the
testing/2
branch.
-
PR#1 build passes,
testing/1
is pushed (promoted) to be the new master. The speculative failure of PR#2 is now real so this is reported. -
PR#3 build passes,
testing/3
is pushed (promoted) to be the new master. -
At this point, the authors of PR#1 have fixed their PR and issue a new merge command. Hoff carries on as usual.
When PRs are closed or receive a new commit, the behaviour is similar to a build failing for that PR with the only difference being the comment posted by Hoff.
Builds started later finishing earlier. Builds results do not always come in the same order as they were started in the train. When the build result of a later PR arrives before the result of an earlier PR Hoff waits before merging:
Hoff is conservative in what it allows to be pushed to master, it tries to guarantee that the builds of all PRs pass by themselves alone. Hoff will not merge a sequence of two PRs where the second fixes the build of the first. This is a choice that trades CI time for confidence that individual PRs are buildable points in the project history as opposed to rollups that include multiple PRs in a single test build.
Complex scenario. Here is a more complex scenario involving builds arriving in the unexpected order and a failure of a build of the first PR in a merge train. This is similar two the two previous scenarios, the only difference being that PR#1 alone causes the build to fail.
-
Hoff receives the command to merge the three PRs in a short period of time.
-
Hoff rebases and merges accordingly.
-
PR#2 build fails first:
- The build of PR#3 is restarted on top of PR#1 because of this.
- We have to wait for the result of PR#1 to decide if the failure of PR#2 is a real failure.
-
PR#1 eventually fails (it was the culprit!). This is reported. The train is restarted with PR#2 first then PR#3 integrated on top of PR#2.
-
PR#2 and PR#3 builds eventually pass and they are pushed to
master
in sequence. -
A developer eventually fixes PR#1 and issues a second merge command. Hoff proceeds as usual.
Rebase failures.
When Hoff fails to rebase a branch in a train due to a conflict,
it does not know whether this is a conflict with master
or any other PRs in the train.
We have to wait for the build results of the last PR it is based on
before doing anything.
If the build eventually passes for the parent PR,
Hoff reports a rebase failure as the parent PR has become the new master
:
If the build eventually fails for the parent PR, Hoff does a new rebase and merge:
When the rebase failure is because of an orphaned fixup!
commit,
the rebase failure is reported immediatelly
as this is not dependent on previous PRs in the train.
More information is available in the doc directory:
- Background: My original intention was more ambitious than building a GitHub bot. This document gives some background about what I want to build.
- Installing: The installation guide.
Hoff is free software. It is licensed under the Apache 2.0 license. It may be used both for commercial and non-commercial use under the conditions given in the license.